在前面的简单登录验证,我们简单整合了SpringSecurity的登录,可以通过自定义设置或者从数据库中读取用户权限类。接下来我们实现一些简单的验证码相关的登录验证。

1、图片验证码登录

其实这里和最初的登录验证没啥区别,只是多了一个验证码的验证过程。我们首先需要清楚认识到SpringSecurity的整个登录认证流程

springboot和redis手机验证码 springboot验证码登录_验证码

  1. Spring Security使用UsernamePasswordAuthenticationFilter过滤器来拦截用户名密码认证请求
  2. 将用户名和密码封装成一个UsernamePasswordToken对象交给AuthenticationManager处理。
  3. AuthenticationManager将挑出一个支持处理该类型Token的AuthenticationProvider(这里默认为DaoAuthenticationProvider,AuthenticationProvider的其中一个实现类)来进行认证
  4. 认证过程中DaoAuthenticationProvider将调用UserDetailService的loadUserByUsername方法来处理认证(可以自定义UserDetailService的实现类)
  5. 如果认证通过(即UsernamePasswordToken中的用户名和密码相符)则返回一个UserDetails类型对象,并将认证信息保存到Session中,认证后我们便可以通过Authentication对象获取到认证的信息了。

那么我们添加验证码验证则有如下几种思路:

1.1、登录表单提交前发送 AJAX 验证验证码

这种方式和SpringSecurity毫无关系,其实就是表单提交前先发个 HTTP 请求验证验证码。

1.2、和用户名、密码一起发送到后台,在 Springsecurity中进行验证

最开始我是采用的这种方式,这种方式也是和Spring security 结合的最紧密的方式。
首先需要清楚的是security默认只处理用户名和密码信息。所以我们需要自定义实现WebAuthenticationDetails向其中加入验证码。

public class CustomWebAuthenticationDetails extends WebAuthenticationDetails {
    private static final long serialVersionUID = 6975601077710753878L;
    private final String verifyCode;

    public CustomWebAuthenticationDetails(HttpServletRequest request) {
        super(request);
        // verifyCode为页面中验证码的name
        verifyCode = request.getParameter("verifyCode");
    }

    public String getVerifyCode() {
        return this.verifyCode;
    }
}

在这个方法中,我们将前台 form 表单中的 verifyCode 获取到,并通过 get 方法方便被调用。这样我们就在验证信息类中添加了验证码的相关信息。自定义了WebAuthenticationDetails,我i们还需要将其放入 AuthenticationDetailsSource 中来替换原本的 WebAuthenticationDetails ,因此还得实现自定义 AuthenticationDetailsSource :

@Component("authenticationDetailsSource")
public class CustomAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {
    @Override
    public WebAuthenticationDetails buildDetails(HttpServletRequest request) {
        return new CustomWebAuthenticationDetails(request);
    }
}

该类内容将原本的 WebAuthenticationDetails 替换为了我们的 CustomWebAuthenticationDetails。
然后我们将 CustomAuthenticationDetailsSource 注入Spring Security中,替换掉默认的 AuthenticationDetailsSource。
修改 WebSecurityConfig,将其注入,然后在config()中使用 authenticationDetailsSource(authenticationDetailsSource)方法来指定它。

@Autowired
private AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> authenticationDetailsSource;
@Override
protected void configure(HttpSecurity http) throws Exception {                         
             http...
                  // 指定authenticationDetailsSource
                  .authenticationDetailsSource(authenticationDetailsSource)
                  ...
}

至此我们通过自定义WebAuthenticationDetails和AuthenticationDetailsSource将验证码和用户名、密码一起带入了Spring Security中,下面我们需要将它取出来验证。
这里需要我们自定义AuthenticationProvider,需要注意的是,如果是我们自己实现AuthenticationProvider,那么我们就需要自己做密码校验了。

@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
    @Autowired
    private CustomUserDetailsService customUserDetailsService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // 获取用户输入的用户名和密码
        String inputName = authentication.getName();
        String inputPassword = authentication.getCredentials().toString();

        CustomWebAuthenticationDetails details = (CustomWebAuthenticationDetails) authentication.getDetails();

        String verifyCode = details.getVerifyCode();
        if(!validateVerify(verifyCode)) {
            throw new DisabledException("验证码输入错误");
        }

        // userDetails为数据库中查询到的用户信息
        UserDetails userDetails = customUserDetailsService.loadUserByUsername(inputName);

        // 如果是自定义AuthenticationProvider,需要手动密码校验
        if(!userDetails.getPassword().equals(inputPassword)) {
            throw new BadCredentialsException("密码错误");
        }

        return new UsernamePasswordAuthenticationToken(inputName, inputPassword, userDetails.getAuthorities());
    }

    private boolean validateVerify(String inputVerify) {
        //获取当前线程绑定的request对象
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        // 不分区大小写
        // 这个validateCode是在servlet中存入session的名字
        String validateCode = ((String) request.getSession().getAttribute("validateCode")).toLowerCase();
        inputVerify = inputVerify.toLowerCase();

        System.out.println("验证码:" + validateCode + "用户输入:" + inputVerify);

        return validateCode.equals(inputVerify);
    }

    @Override
    public boolean supports(Class<?> authentication) {
        // 这里不要忘记,和UsernamePasswordAuthenticationToken比较
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}

最后在 WebSecurityConfig 中将其注入,并在 config 方法中通过 auth.authenticationProvider() 指定使用。

@Autowired
private CustomAuthenticationProvider customAuthenticationProvider;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.authenticationProvider(customAuthenticationProvider);
}

但是后面添加手机验证短信验证功能的时候就出问题了,主要是用CustomWebAuthenticationDetails替换了AuthenticationDetailsSource后,添加短信验证链又不能使用该CustomWebAuthenticationDetails。所以后面我还是改成了自定义过滤器来验证。

1.3、使用自定义过滤器(Filter),在 Spring security 校验前验证验证码合法性

使用过滤器的思路是:在 Spring Security 处理登录验证请求前,验证验证码,如果正确,放行;如果不正确,调到异常。其实这里简单添加一个过滤器就好了,我主要是想和短信验证板块向对照,也为了方便编写不同验证方式配置类。所以就自定义实现了整个验证链。

springboot和redis手机验证码 springboot验证码登录_自定义_02

  • 首先自定义实现一个只经过一次的过滤器
@Component
public class CustomValidateFilter extends OncePerRequestFilter {
      //自定义的公共登录失败后的处理逻辑
    @Autowired
    private PublicAuthenticationFailureHandler publicAuthenticationFailureHandler;
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
            throws IOException, ServletException {
            //设置过滤请求url。(MyContants.CUST_FILTER_URL自定义的)
        if (StringUtils.equals(MyContants.CUST_FILTER_URL, request.getRequestURI())
                && StringUtils.equalsIgnoreCase(request.getMethod(), MyContants.REQUEST_MAPPING_POST)) {

            try {
                //验证谜底与用户输入是否匹配
                validate(new ServletWebRequest(request));
            } catch (AuthenticationException e) {
                publicAuthenticationFailureHandler.onAuthenticationFailure(request, response, e);
                return;
            }

        }
        filterChain.doFilter(request, response);

    }

    private void validate(ServletWebRequest request) throws SessionAuthenticationException {
        Logger logger = LoggerFactory.getLogger(getClass());
        HttpSession session = request.getRequest().getSession();
        String sessionValidateCode = (String) session.getAttribute("validateCode");
        String parameterVerifyCode = request.getParameter("verifyCode");
        logger.info("验证码",sessionValidateCode,parameterVerifyCode);

        if (StringUtils.isEmpty(parameterVerifyCode)) {
            throw new SessionAuthenticationException("验证码不能为空");
        }

        if (!StringUtils.equalsAnyIgnoreCase(sessionValidateCode,parameterVerifyCode)) {
            throw new SessionAuthenticationException("验证码不正确");
        }
        session.removeAttribute("validateCode");
    }
}
  • 然后就是自定义AbstractAuthenticationProcessingFilter进行请求验证.(模仿UsernamePasswordAuthenticationFilter 实现)
public class CustomAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
      //这里还是用户名加密码
    private String usernameParameter = MyContants.CUST_FORM_USERNAME_KEY;
    private String passwordParameter = MyContants.CUST_FORM_PASSWORD_KEY;
    private boolean postOnly = true;

    public CustomAuthenticationFilter() {
        super(new AntPathRequestMatcher(MyContants.CUST_FILTER_URL, MyContants.REQUEST_MAPPING_POST));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (this.postOnly && !MyContants.REQUEST_MAPPING_POST.equals(request.getMethod())) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
            String username = this.obtainUsername(request);
            String password = this.obtainPassword(request);
            if (username == null) {
                username = "";
            }

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

            username = username.trim();
            CustomAuthenticationToken authRequest = new CustomAuthenticationToken(username, password);
            setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }

    @Nullable
    protected String obtainPassword(HttpServletRequest request) {
        return request.getParameter(this.passwordParameter);
    }

    @Nullable
    protected String obtainUsername(HttpServletRequest request) {
        return request.getParameter(this.usernameParameter);
    }

    protected void setDetails(HttpServletRequest request, CustomAuthenticationToken authRequest) {
        authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
    }

    public void setUsernameParameter(String usernameParameter) {
        Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
        this.usernameParameter = usernameParameter;
    }

    public void setPasswordParameter(String passwordParameter) {
        Assert.hasText(passwordParameter, "Password parameter must not be empty or null");
        this.passwordParameter = passwordParameter;
    }

    public void setPostOnly(boolean postOnly) {
        this.postOnly = postOnly;
    }

    public final String getUsernameParameter() {
        return this.usernameParameter;
    }

    public final String getPasswordParameter() {
        return this.passwordParameter;
    }
}
  • 自定义AbstractAuthenticationToken (模仿 UsernamePasswordAuthenticationToken 实现)
public class CustomAuthenticationToken extends AbstractAuthenticationToken {
    private static final long serialVersionUID = 520L;
    private final Object principal;
    private Object credentials;

    public CustomAuthenticationToken(Object principal, Object credentials) {
        super(null);
        this.principal = principal;
        this.credentials = credentials;
        this.setAuthenticated(false);
    }

    public CustomAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true);
    }

    @Override
    public Object getCredentials() {
        return this.credentials;
    }

    @Override
    public Object getPrincipal() {
        return this.principal;
    }

    @Override
    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");
        } else {
            super.setAuthenticated(false);
        }
    }

    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
        this.credentials = null;
    }
}
  • 自定义AuthenticationProvider进行登录验证
public class CustomAuthenticationProvider implements AuthenticationProvider {
    private UserDetailsService userDetailsService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        CustomAuthenticationToken customAuthenticationToken = (CustomAuthenticationToken) authentication;

        // userDetails为数据库中查询到的用户信息
        UserDetails userDetails = userDetailsService.loadUserByUsername((String) customAuthenticationToken.getPrincipal());

        if(userDetails == null){
            throw new InternalAuthenticationServiceException("无法根据名字获取用户信息");
        }
        // 如果是自定义AuthenticationProvider,需要手动密码校验
        if(!userDetails.getPassword().equals(customAuthenticationToken.getCredentials())) {
            throw new BadCredentialsException("密码错误");
        }
        // 此时鉴权成功后,应当重新 new 一个拥有鉴权的 authenticationResult 返回

        return new CustomAuthenticationToken(userDetails.getUsername(), userDetails.getPassword(), userDetails.getAuthorities());

    }


    @Override
    public boolean supports(Class<?> authentication) {
        return CustomAuthenticationToken.class.isAssignableFrom(authentication);
    }
    public UserDetailsService getUserDetailService() {
        return userDetailsService;
    }

    public void setUserDetailService(UserDetailsService userDetailService) {
        this.userDetailsService = userDetailService;
    }
}
  • 自定义UserDetailsService返回userdetails
@Service("customUserDetailsService")
public class CustomUserDetailsService implements UserDetailsService {
    @Autowired
    private UserService userService;
    @Autowired
    private RoleService roleService;
    @Autowired
    private UserRoleService userRoleService;
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        // 从数据库中取出用户信息
        User user = userService.queryByUsername(s);
        // 判断用户是否存在
        if(user == null) {
            throw new UsernameNotFoundException("用户名不存在");
        }

        // 添加权限
        List<UserRole> userRoles = userRoleService.listByUserId(user.getId());
        for (UserRole userRole : userRoles) {
            Role role = roleService.selectById(userRole.getRoleId());
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        // 返回UserDetails实现类 这里因为我自己也存在一个user类。所以前面加了全类名。(以后干啥都不要图简单自定义user这种万金油名字)
        return new org.springframework.security.core.userdetails.User(user.getUsername(),user.getPassword(), authorities);
    }
}
  • 然后实现配置类CustomAuthenticationSecurityConfig
@Component("customAuthenticationSecurityConfig")
public class CustomAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    @Autowired
    private AuthenticationSuccessHandler authenticationSuccessHandler;
    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;
    @Autowired
    @Qualifier("customUserDetailsService")
    private CustomUserDetailsService customUserDetailsService;
    @Override
    public void configure(HttpSecurity http) {
        CustomAuthenticationFilter customAuthenticationFilter = new CustomAuthenticationFilter();
        customAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        customAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
        customAuthenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler);

        CustomAuthenticationProvider customAuthenticationProvider = new CustomAuthenticationProvider();
        customAuthenticationProvider.setUserDetailService(customUserDetailsService);
        http.authenticationProvider(customAuthenticationProvider)
                .addFilterAfter(customAuthenticationFilter,UsernamePasswordAuthenticationFilter.class);
    }
}
  • 添加到认证链
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private CustomValidateFilter customValidateFilter;

    @Autowired
    private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;

    @Override
    protected void configure(HttpSecurity http) throws Exception {                
        http...
            // 添加验证码校验过滤器
            .addFilterBefore(customValidateFilter,UsernamePasswordAuthenticationFilter.class)
            ...                
            .and()
            // 添加验证码校验过滤器
            .apply(customAuthenticationSecurityConfig);
    }  
}

再次声明,我这里主要是想习惯一种类似于模板的代码格式才采用自定义整个验证链来进行验证,实际上单单是一个验证码验证的话后面自定义验证链完全不需要,就只是添加一个过滤器就行了。相关内容手机短信验证的代码可能更加清楚。