spring securiry 登录 添加验证码过滤器

闲来无事,断断续续看了半个月的spring security。寻思着怎么在登录的时候验证更多的用户数据,例如:验证码。

在spring security官方文档中的第十章节,阐述了spring security验证的步骤。

在spring security中默认的验证方法都是通过调用ProviderManager,从而轮训authenticationProviders来一个个验证,达到验证的目的。但是不支持默认验证以外的验证。于是就有了以下的思路。

我们也自定义个Filter,把他添加到spring security中的filterChain中去,按照spring secutiry的验证结构来扩展一个验证机制。

笔者以下代码参考了spring security中的UsernamePasswordAuthenticationFilter来实现的。

  • 具体步骤如下
  1. 自定义一个验证码token类
    用来存放验证码验证数据
public class CodeAuthenticationToken extends AbstractAuthenticationToken{

    /**
     * 验证码
     */
    private Object captcha;

    /**
     * 验证码验证标识
     * true:通过
     * false:错误
     */
    private boolean flag;

    public CodeAuthenticationToken() {
        super(null);
    }

    public CodeAuthenticationToken(Object captcha) {
        super(null);
        this.captcha = captcha;
        this.flag = false;
        setAuthenticated(false);
    }

    public CodeAuthenticationToken(Collection authorities, Object captcha) {
        super(authorities);
        this.captcha = captcha;
        this.flag = false;
        super.setAuthenticated(true); // must use super, as we override
    }

    @Override
    public Object getCredentials() {
        return null;
    }

    @Override
    public Object getPrincipal() {
        return null;
    }

    public Object getCaptcha() {
        return captcha;
    }

    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        if (isAuthenticated) {
            throw new IllegalArgumentException(
                    "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        }

        super.setAuthenticated(false);
    }


    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}
  1. 自定义一个认证失败异常类
    注意,这个类必须继承AuthenticationException 类
public class CodeAuthenticationException extends AuthenticationException {

    public CodeAuthenticationException(String msg, Throwable t) {
        super(msg, t);
    }

    public CodeAuthenticationException(String msg) {
        super(msg);
    }
}
  1. 提供一个验证码验证器
public class CodeAuthenticationProvider implements AuthenticationProvider {

    /** Logger available to subclasses */
    protected final Log logger = LogFactory.getLog(getClass());

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        logger.debug("custom captcha authentication");

        Assert.isInstanceOf(CodeAuthenticationToken.class, authentication,"错误的类");

        CodeAuthenticationToken custCaptchaToken = (CodeAuthenticationToken) authentication;
        String captcha = custCaptchaToken.getCaptcha().toString();

        if(captcha.equals("")){
            logger.debug("验证码为空");
            throw new CodeAuthenticationException("验证码错误!");
        }

        //写死一个验证码,具体可以自己修改
        if(!captcha.equals("1000")){
            logger.debug("验证码错误");
            throw new CodeAuthenticationException("验证码错误!");
        }

        //返回验证成功对象
        custCaptchaToken.setFlag(true);
        return custCaptchaToken;
    }

    @Override
    public boolean supports(Class authentication) {
        return (CodeAuthenticationToken.class.isAssignableFrom(authentication));
    }
}
  1. 自定义一个认证管理器
    这里管理器可以省略,这里因为是按照spring security的结构来实现的,所以没有省略。
public class CodeAuthenticationManager implements AuthenticationManager {

    /**
     * 自己实现的验证码认证器
     */
    private AuthenticationProvider provider;

    public CodeAuthenticationManager(AuthenticationProvider provider) {
        Assert.notNull(provider, "provider cannot be null");
        this.provider = provider;
    }

    public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
        return this.provider.authenticate(authentication);
    }
}
  1. 我们自己的filter
public class CodeFilter extends GenericFilterBean {

    /** Logger available to subclasses */
    protected final Log logger = LogFactory.getLog(getClass());

    //验证码拦截路径
    private static final String CODE_ANT_URL = "/login";
    private static final String SPRING_SECURITY_FORM_CAPTCHA_KEY = "code";

    private String captchaParameter = SPRING_SECURITY_FORM_CAPTCHA_KEY;

    private boolean postOnly = true;

    //请求路径匹配
    private RequestMatcher requiresAuthenticationRequestMatcher;

    private RememberMeServices rememberMeServices = new NullRememberMeServices();
    private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler(CODE_ANT_URL); //设置验证失败重定向路径

    public CodeFilter() {
        this.requiresAuthenticationRequestMatcher = new AntPathRequestMatcher(CODE_ANT_URL, "POST");
    }

    public CodeFilter(RequestMatcher requiresAuthenticationRequestMatcher) {
        Assert.notNull(requiresAuthenticationRequestMatcher,"requiresAuthenticationRequestMatcher cannot be null");
        this.requiresAuthenticationRequestMatcher = requiresAuthenticationRequestMatcher;
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        // 不是需要过滤的路径,执行下一个过滤器
        if (!requiresAuthentication(request, response)) {
            filterChain.doFilter(request, response);
            return;
        }

        if (logger.isDebugEnabled()) {
            logger.debug("Request is to process authentication");
        }

        Authentication authResult;
        try {
            authResult = this.attemptAuthentication(request, response);
            if (authResult == null) {
                logger.error("Authentication is null!");
                // return immediately as subclass has indicated that it hasn't completed
                // authentication
                return;
            }

        } catch (InternalAuthenticationServiceException failed) {
            logger.error("An internal error occurred while trying to authenticate the user.",failed);
            return;
        } catch (AuthenticationException failed) {
            logger.error("Authentication failed.", failed);
            //Authentication failed
            unsuccessfulAuthentication(request, response, failed);
            return;
        }

        //认证成功,执行下个过滤器
        filterChain.doFilter(request, response);
    }

    private Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response) throws AuthenticationException {
        if (postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }
        //获取验证码
        String captcha = request.getParameter(captchaParameter);

        if (captcha == null) {
            captcha = "";
        }

        captcha = captcha.trim();

        CodeAuthenticationToken authRequest = new CodeAuthenticationToken(captcha);

        //这里可以直接省略掉,用provider直接验证
        CodeAuthenticationManager manager = new CodeAuthenticationManager(new CodeAuthenticationProvider());
        return manager.authenticate(authRequest);
    }

    /**
     * 比较需要过滤的请求路径
     *
     * @param request
     * @param response
     * @return
     */
    protected boolean requiresAuthentication(HttpServletRequest request,
                                             HttpServletResponse response) {
        return requiresAuthenticationRequestMatcher.matches(request);
    }

    /**
     * 处理验证码认证失败
     * 参考 {@link org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter}
     * Default behaviour for unsuccessful authentication.
     * 
    *
  1. Clears the {@link SecurityContextHolder}
  2. *
  3. Stores the exception in the session (if it exists or * allowSesssionCreation is set to true)
  4. *
  5. Informs the configured RememberMeServices of the failed login
  6. *
  7. Delegates additional behaviour to the {@link AuthenticationFailureHandler}.
  8. *
*/ protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { SecurityContextHolder.clearContext(); if (logger.isDebugEnabled()) { logger.debug("Authentication request failed: " + failed.toString(), failed); logger.debug("Updated SecurityContextHolder to contain null Authentication"); logger.debug("Delegating to authentication failure handler " + failureHandler); } rememberMeServices.loginFail(request, response); failureHandler.onAuthenticationFailure(request, response, failed); } }

以上是需要自己实现的。下面笔者在spring boot集成spring security中的demo中稍作修改,来实现验证码登录。

步骤如下:

  • 将上面自己实现五个类添加到工程中去
  • 修改WebSecurityConfig
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.addFilterBefore(getFilter(), UsernamePasswordAuthenticationFilter.class) //在认证用户名之前认证验证码
            .authorizeRequests()
                .antMatchers("/", "/home").permitAll()
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login")
                .permitAll()
                .and()
            .logout()
                .permitAll();
    }


    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        //基于内存来验证用户
        auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
    }


    //注入自定义的验证码过滤器
    @Bean
    public CodeFilter getFilter(){
        return new CodeFilter();
    }
}
  • 修改登录视图控制器
    注释掉registry.addViewController("/login").setViewName("login");
@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {
    
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/home").setViewName("home");
        registry.addViewController("/").setViewName("home");
        registry.addViewController("/hello").setViewName("hello");
        //registry.addViewController("/login").setViewName("login");
    }

}
  • 新建一个登录视图控制器
@Controller
@RequestMapping("/")
public class LoginController {

    //认证失败抛出来的异常
    private static final String LAST_EXCEPTION_KEY = "SPRING_SECURITY_LAST_EXCEPTION";

    @RequestMapping("/login")
    public String login(HttpServletRequest request, HttpSession session){
        //spring security默认会把异常存到session中。
        AuthenticationException authenticationException = (AuthenticationException) session.getAttribute(LAST_EXCEPTION_KEY);

        //判断异常是否是我们自定义的验证码认证异常
       if(authenticationException != null && authenticationException instanceof CodeAuthenticationException){
            CodeAuthenticationException c = (CodeAuthenticationException) authenticationException;
            //验证码认证错误标识,存入request中只针对本次请求。不影响整个会话
            request.setAttribute("codeError",true);
        }
        return "login";
    }
}
  • 最后在登录界面添加一个验证码输入框


    
        Spring Security Example 
    
    
        
Invalid username and password.
You have been logged out.
验证码错误!

本文内容基于spring boot 1.5.10和spring security 4.2.4来实现的spring security 添加登录验证码验证,在原有验证机制的情况下,添加了验证码验证机制。

测试数据 用户名为:user 密码:password 验证码:1000
例子比较简单,验证码直接写死了。感兴趣的可以自行修改

本例子源码:https://gitee.com/longguiyunjosh/spring-security-demo/tree/master

不足之处,欢迎补充
如需转载,请注明出处

你可能感兴趣的:(spring securiry 登录 添加验证码过滤器)