Spring Security - 表单登录自定义验证码逻辑

October . 15 . 2020

记录一下 Spring Secyrity 表单登陆的相关配置, 以及如何添加自定义逻辑。

  • 环境配置

项目 GitHub 地址: sign-demo

  1. 下载项目
  2. 使用mvn导入相关依赖
  3. 配置 application.yaml 相关配置

image.png

  

  新建 persistent_logins 表, 用作记住密码的记录, 建表语句如下

create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null)"


    4. 运行 MainApplication

    5. 访问 http://localhost:7016/my-login.html 

image.png

默认用户 admin 默认密码 admin


  • 具体实现

简单说一下相关配置及实现

security 配置类 

SecurityConfig.java
package com.niu.sign.config;

import com.niu.sign.handler.CustomLogoutSuccessHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;

import javax.sql.DataSource;

/**
 * security 配置
 *
 * @author [nza]
 * @version 1.0 [2020/10/10 15:59]
 * @createTime [2020/10/10 15:59]
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private AuthenticationSuccessHandler customAuthenticationSuccessHandler;

    @Autowired
    private AuthenticationFailureHandler customAuthenticationFailureHandler;

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private DataSource dataSource;

    /**
     * URL 白名单
     */
    private static final String[] URL_WHITE_LIST = new String[]{"/authentication/require", "/my-login.html", "/code/*"};

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                // 登录页
                .loginPage("/authentication/require")
                // 登录处理url
                .loginProcessingUrl("/authentication/form")
                // 登录成功处理器
                .successHandler(customAuthenticationSuccessHandler)
                // 登录失败处理器
                .failureHandler(customAuthenticationFailureHandler)
                .and()
                .rememberMe()
                // 记住我存储器
                .tokenRepository(persistentTokenRepository())
                .tokenValiditySeconds(3600)
                .userDetailsService(userDetailsService)
                .and()
                .authorizeRequests()
                // 白名单
                .antMatchers(URL_WHITE_LIST)
                .permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .csrf().disable()
                .logout()
                // 退出登录url
                .logoutUrl("/signOut")
                // 退出登录成功处理器
                .logoutSuccessHandler(new CustomLogoutSuccessHandler("/signOut"))
                .deleteCookies("JSESSIONID");
    }

    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        // 记住我存储器
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        return jdbcTokenRepository;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}


由于 Spring Security 的默认登录逻辑没有实现验证码相关的逻辑, 所以需要自己来实现, 

这里的思路是在Sping Security 的过滤器链的最前面添加自定义的验证码过滤器。


ValidateCodeFilter.java
package com.niu.sign.validate;

import com.niu.sign.exception.ValidateCodeException;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * 验证码校验Filter
 *
 * @author [nza]
 * @version 1.0 [2020/10/12 15:18]
 * @createTime [2020/10/12 15:18]
 */
@Component("validateCodeFilter")
public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {

    /**
     * 验证码校验失败处理器
     */
    @Autowired
    private AuthenticationFailureHandler customAuthenticationFailureHandler;

    /**
     * 存放所有需要校验验证码的url
     */
    private Map<String, ValidateCodeType> urlMap = new HashMap<>();

    /**
     * 验证请求url与配置的url是否匹配的工具类
     */
    private AntPathMatcher pathMatcher = new AntPathMatcher();

    /**
     * 系统中的校验码处理器
     */
    @Autowired
    private ValidateCodeProcessorHolder validateCodeProcessorHolder;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        ValidateCodeType type = getValidateCodeType(request);
        if (type != null) {
            logger.info("校验请求(" + request.getRequestURI() + ")中的验证码,验证码类型" + type);
            try {
                validateCodeProcessorHolder.findValidateCodeProcessor(type)
                        .validate(new ServletWebRequest(request, response));
                logger.info("验证码校验通过");
            } catch (ValidateCodeException exception) {
                customAuthenticationFailureHandler.onAuthenticationFailure(request, response, exception);
                return;
            }
        }
        chain.doFilter(request, response);
    }

    @Override
    public void afterPropertiesSet() throws ServletException {
        super.afterPropertiesSet();
        // 图片验证码url
        urlMap.put("/authentication/form", ValidateCodeType.IMAGE);
        // 短信验证码url
        urlMap.put("/authentication/mobile", ValidateCodeType.SMS);
    }

    /**
     * 获取校验码的类型,如果当前请求不需要校验,则返回null
     *
     * @param request 请求
     * @return {@link ValidateCodeType}
     */
    private ValidateCodeType getValidateCodeType(HttpServletRequest request) {
        ValidateCodeType result = null;
        if (!StringUtils.equalsIgnoreCase(request.getMethod(), "get")) {
            Set<String> urls = urlMap.keySet();
            for (String url : urls) {
                if (pathMatcher.match(url, request.getRequestURI())) {
                    result = urlMap.get(url);
                }
            }
        }
        return result;
    }
}


配置自定义验证码过滤器

ValidateCodeSecurityConfig.java
package com.niu.sign.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
import org.springframework.stereotype.Component;

import javax.servlet.Filter;

/**
 * 验证码过滤器配置类
 *
 * @author [nza]
 * @version 1.0 [2020/10/13 13:39]
 * @createTime [2020/10/13 13:39]
 */
@Component("validateCodeSecurityConfig")
public class ValidateCodeSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    @Autowired
    private Filter validateCodeFilter;

    @Override
    public void configure(HttpSecurity http) {
        // 将自定义Filter添加到过滤器链
        http.addFilterBefore(validateCodeFilter, AbstractPreAuthenticatedProcessingFilter.class);
    }
}


  • 总结
     为 spring security 添加验证码逻辑只需要两步
  1.  实现自定义验证码过滤器
  2. 将自定义验证码过滤器配置到过滤器链的最前面即可