龙空技术网

Spring Security 6 使用自定义认证实现图形验证码

波波源源by 62

前言:

现时看官们对“消息认证的实现过程包括”大致比较关心,咱们都想要知道一些“消息认证的实现过程包括”的相关内容。那么小编同时在网摘上汇集了一些对于“消息认证的实现过程包括””的相关资讯,希望看官们能喜欢,咱们快快来学习一下吧!

前面使用过滤器的方式实现了带图形验证码的验证功能,属于Servlet层面,简单、易理解。其实,Spring Security还提供了一种更优雅的实现图形验证码的方式,即自定义认证。

1 认识AuthenticationProvider

在学习Spring Security的自定义认证之前,有必要了解Spring Security是如何灵活集成多种认证技术的。

我们所面对的系统中的用户,在Spring Security中被称为主体(principal)。主体包含了所有能够经过验证而获得系统访问权限的用户、设备或其他系统。主体的概念实际上来自Java Security,Spring Security通过一层包装将其定义为一个Authentication。

package org.springframework.security.core;import java.io.Serializable;import java.security.Principal;import java.util.Collection;public interface Authentication extends Principal, Serializable {    Collection<? extends GrantedAuthority> getAuthorities();    Object getCredentials();    Object getDetails();    Object getPrincipal();    boolean isAuthenticated();    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;}

Authentication中包含主体权限列表、主体凭据、主体详细信息,以及主体是否验证成功等信息。由于大部分场景下身份验证都是基于用户名和密码进行的,所以Spring Security提供了一个UsernamePasswordAuthenticationToken用于代指这一类证明(例如,用SSH KEY也可以登录,但它不属于用户名和密码登录这个范畴,如有必要,也可以自定义提供)。在前面使用的表单登录中,每一个登录用户都被包装为一个 UsernamePasswordAuthenticationToken,从而在Spring Security的各个AuthenticationProvider中流动。

AuthenticationProvider被Spring Security定义为一个验证过程。

package org.springframework.security.authentication;import org.springframework.security.core.Authentication;import org.springframework.security.core.AuthenticationException;public interface AuthenticationProvider {    Authentication authenticate(Authentication authentication) throws AuthenticationException;    boolean supports(Class<?> authentication);}

一次完整的认证可以包含多个AuthenticationProvider,一般由ProviderManager管理。

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {       private List<AuthenticationProvider> providers;        public Authentication authenticate(Authentication authentication) throws AuthenticationException {        Class<? extends Authentication> toTest = authentication.getClass();        AuthenticationException lastException = null;        AuthenticationException parentException = null;        Authentication result = null;        Authentication parentResult = null;        int currentPosition = 0;        int size = this.providers.size();        Iterator var9 = this.getProviders().iterator();        while(var9.hasNext()) {            AuthenticationProvider provider = (AuthenticationProvider)var9.next();            if (provider.supports(toTest)) {                if (logger.isTraceEnabled()) {                    Log var10000 = logger;                    String var10002 = provider.getClass().getSimpleName();                    ++currentPosition;                    var10000.trace(LogMessage.format("Authenticating request with %s (%d/%d)", var10002, currentPosition, size));                }                try {                    result = provider.authenticate(authentication);                    if (result != null) {                        this.copyDetails(authentication, result);                        break;                    }                } catch (InternalAuthenticationServiceException | AccountStatusException var14) {                    this.prepareException(var14, authentication);                    throw var14;                } catch (AuthenticationException var15) {                    AuthenticationException ex = var15;                    lastException = ex;                }            }        }        if (result == null && this.parent != null) {            try {                parentResult = this.parent.authenticate(authentication);                result = parentResult;            } catch (ProviderNotFoundException var12) {            } catch (AuthenticationException var13) {                parentException = var13;                lastException = var13;            }        }        if (result != null) {            if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {                ((CredentialsContainer)result).eraseCredentials();            }            if (parentResult == null) {                this.eventPublisher.publishAuthenticationSuccess(result);            }            return result;        } else {            if (lastException == null) {                lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}"));            }            if (parentException == null) {                this.prepareException((AuthenticationException)lastException, authentication);            }            throw lastException;        }    }}
2 自定义AuthenticationProvider

Spring Security提供了多种常见的认证技术,包括但不限于以下几种:

◎ HTTP层面的认证技术,包括HTTP基本认证和HTTP摘要认证两种。

◎ 基于LDAP的认证技术(Lightweight Directory Access Protocol,轻量目录访问协议)。

◎ 聚焦于证明用户身份的OpenID认证技术。

◎ 聚焦于授权的OAuth认证技术。

◎ 系统内维护的用户名和密码认证技术。

其中,使用最为广泛的是由系统维护的用户名和密码认证技术,通常会涉及数据库访问。为了更好地按需定制,Spring Security 并没有直接糅合整个认证过程,而是提供了一个抽象的AuthenticationProvider。

public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {   //附加认证过程   protected abstract void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException;   //检索用户   protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException;   //认证过程   public Authentication authenticate(Authentication authentication) throws AuthenticationException {        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> {            return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported");        });        String username = this.determineUsername(authentication);        boolean cacheWasUsed = true;        UserDetails user = this.userCache.getUserFromCache(username);        if (user == null) {            cacheWasUsed = false;            try {                user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);            } catch (UsernameNotFoundException var6) {                UsernameNotFoundException ex = var6;                this.logger.debug("Failed to find user '" + username + "'");                if (!this.hideUserNotFoundExceptions) {                    throw ex;                }                throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));            }            Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");        }        try {            this.preAuthenticationChecks.check(user);            this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);        } catch (AuthenticationException var7) {            AuthenticationException ex = var7;            if (!cacheWasUsed) {                throw ex;            }            cacheWasUsed = false;            user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);            this.preAuthenticationChecks.check(user);            this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);        }        this.postAuthenticationChecks.check(user);        if (!cacheWasUsed) {            this.userCache.putUserInCache(user);        }        Object principalToReturn = user;        if (this.forcePrincipalAsString) {            principalToReturn = user.getUsername();        }        return this.createSuccessAuthentication(principalToReturn, authentication, user);    }}

在 AbstractUserDetailsAuthenticationProvider中实现了基本的认证流程,通过继承AbstractUserDetailsAuthenticationProvider,并实现retrieveUser和additionalAuthenticationChecks两个抽象方法即可自定义核心认证过程,灵活性非常高。

Spring Security同样提供一个继承自AbstractUserDetailsAuthenticationProvider 的Authenti cationProvider。

DaoAuthenticationProvider的用户信息来源于UserDetailsService,并且整合了密码编码的实现,在前面章节中学习的表单认证就是由DaoAuthenticationProvider提供的。

3 实现图形验证码的AuthenticationProvider

前面我们已经基本了解了Spring Security的认证流程,现在重新回到自定义认证实现图形验证码登录这个具体案例中。由于只是在常规的认证之上增加了图形验证码的校验,其他流程并没有变化,所以只需继承DaoAuthenticationProvider并稍作增添即可。

package com.by.security.provider;import com.by.security.exception.VerificationCodeException;import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;import org.springframework.security.authentication.dao.DaoAuthenticationProvider;import org.springframework.security.core.AuthenticationException;import org.springframework.security.core.userdetails.UserDetails;import org.springframework.security.core.userdetails.UserDetailsService;import org.springframework.security.crypto.password.PasswordEncoder;import org.springframework.stereotype.Component;/** * @Author: Cyz * @Description: * @Date: create in 2024/6/4 16:06 */@Componentpublic class MyDaoAuthenticationProvider extends DaoAuthenticationProvider{    public MyDaoAuthenticationProvider(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder){        super.setUserDetailsService(userDetailsService);        super.setPasswordEncoder(passwordEncoder);    }    //在这里获取request 通过request来获取验证码 作比对    @Override    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {        MyWebAuthenticationDetails details = (MyWebAuthenticationDetails) authentication.getDetails();        if(!details.isImageCodeIsRight()){            throw new VerificationCodeException();        }        super.additionalAuthenticationChecks(userDetails, authentication);    }}

在验证流程中添加新的逻辑后似乎有些问题。在additionalAuthenticationChecks中,我们可以得到的参数是来自UserDetailsService的UserDetails,以及根据用户提交的账号信息封装而来的UsernamePasswordAuthenticationToken,而图形验证码的校验必须要有HttpServletRequest对象,因为用户提交的验证码和session存储的验证码都需要从用户的请求中获取,这是否意味着这种实现方式不可行呢?并非如此,Authentication实际上还可以携带账号信息之外的数据。

package org.springframework.security.core;public interface Authentication extends Principal, Serializable {    //允许携带任意对象	Object getDetails();}

如果这个数据可以利用,那么难题自然就迎刃而解了。前面提到过,一次完整的认证可以包含多个AuthenticationProvider,这些AuthenticationProvider都是由ProviderManager管理的,而ProviderManager是由UsernamePasswordAuthenticationFilter调用的。也就是说,所有的AuthenticationProvider包含的Authentication都来源于UsernamePasswordAuthenticationFilter。

AbstractAuthenticationProcessingFilter本身并没有设置用户详细信息的流程,而且是通过标准接口AuthenticationDetailsSource构建的,这意味着它是一个允许定制的特性。

在UsernamePasswordAuthenticationFilter中使用的AuthenticationDetailsSource是一个标准的Web认证源,携带的是用户的sessionId和IP地址。

有了HttpServletRequest之后,一切都将变得非常顺畅。基于图形验证码的场景,我们可以继承

WebAuthenticationDetails,并扩展需要的信息。

package com.by.security.provider;import jakarta.servlet.http.HttpServletRequest;import jakarta.servlet.http.HttpSession;import org.springframework.security.web.authentication.WebAuthenticationDetails;/** * @Author: Cyz * @Description: * @Date: create in 2024/6/4 16:07 */public class MyWebAuthenticationDetails extends WebAuthenticationDetails {    private boolean imageCodeIsRight;    public boolean isImageCodeIsRight() {        return imageCodeIsRight;    }    public MyWebAuthenticationDetails(HttpServletRequest request) {        super(request);        String requestCode = request.getParameter("captcha");        HttpSession session = request.getSession();        String sessionCode = (String) session.getAttribute("captcha");        if(sessionCode!=null){            session.removeAttribute("captcha");            if(sessionCode.equals(requestCode)){                this.imageCodeIsRight = true;            }        }    }    public MyWebAuthenticationDetails(String remoteAddress, String sessionId) {        super(remoteAddress, sessionId);    }}

将它提供给一个自定义的AuthenticationDetailsSource。

package com.by.security.provider;import jakarta.servlet.http.HttpServletRequest;import org.springframework.security.web.authentication.WebAuthenticationDetails;import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;/** * @Author: Cyz * @Description: * @Date: create in 2024/6/4 16:08 */public class MyWebAuthenticationDetailsSource extends WebAuthenticationDetailsSource{    @Override    public WebAuthenticationDetails buildDetails(HttpServletRequest context) {        return new MyWebAuthenticationDetails(context);    }}

接下来实现我们自定义的AuthenticationProvider。

package com.by.security.provider;import com.by.security.exception.VerificationCodeException;import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;import org.springframework.security.authentication.dao.DaoAuthenticationProvider;import org.springframework.security.core.AuthenticationException;import org.springframework.security.core.userdetails.UserDetails;import org.springframework.security.core.userdetails.UserDetailsService;import org.springframework.security.crypto.password.PasswordEncoder;import org.springframework.stereotype.Component;/** * @Author: Cyz * @Description: * @Date: create in 2024/6/4 16:06 */@Componentpublic class MyDaoAuthenticationProvider extends DaoAuthenticationProvider{    public MyDaoAuthenticationProvider(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder){        super.setUserDetailsService(userDetailsService);        super.setPasswordEncoder(passwordEncoder);    }    //在这里获取request 通过request来获取验证码 作比对    @Override    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {        MyWebAuthenticationDetails details = (MyWebAuthenticationDetails) authentication.getDetails();        if(!details.isImageCodeIsRight()){            throw new VerificationCodeException();        }        super.additionalAuthenticationChecks(userDetails, authentication);    }}

想要应用自定义的AuthenticationProvider 和AuthenticationDetailsSource,还需在WebSecurityConfig中完成剩余的配置。

         http.formLogin(formLogin->                formLogin.authenticationDetailsSource(new MyWebAuthenticationDetailsSource())                        .loginPage("/login").permitAll() //登录页面                        .loginProcessingUrl("/login") //登录接口可以匿名访问                        .defaultSuccessUrl("/index") //登录成功访问/index页面        );

参考个人博客:cyz

标签: #消息认证的实现过程包括