最近一个项目做权限管理使用了 Spring boot+ Spring security +oauth2.0 ,但是产品设计登录界面时添加了 图片验证码功能,

要知道 spring security 在登录时 只需要输入账号密码即可,所以刚开始为了方便在自定义的登录页面使用jQuery.submit()方法拦截了表单提交时间,在其中进行验证码的验证,但是测试人员测试后说存在暴力破解和撞库风险,需要账号,密码与验证码同时提交验证,并作出登录错误次数限制,所以只能想其他解决办法。

这里简单叙述一下spring security的登录验证机制:

Spring security使用众多的过滤器对url进行拦截,以此来进行权限管理。Spring security不允许我们修改默认的filter实现,但是可以加入自己的filter。登录验证的流程是,用户登陆会被AuthenticationProcessingFilter拦截,调用AuthenticationManager的实现,而AuthenticationManager会调用ProviderManager来获取用户验证信息。如果验证通过会将用户的权限信息封装成User对象放到spring的全局缓存SecurityContextHolder中,以备后面访问资源时使用。忽略验证用户信息的部分,我们可以通过AuthenticationProcessingFilter来检验验证码,并达到验证失败时拒绝用户登录的目的。

自定义过滤器:

@Log
public class CaptchaAuthenticationFilter extends AbstractAuthenticationProcessingFilter {


    private String processUrl;

    private MyAuthenctiationFailureHandler myAuthenctiationFailureHandler;

    public CaptchaAuthenticationFilter(String defaultFilterProcessesUrl,MyAuthenctiationFailureHandler myAuthenctiationFailureHandler) {
        super(defaultFilterProcessesUrl);
        this.processUrl = defaultFilterProcessesUrl;
        this.myAuthenctiationFailureHandler = myAuthenctiationFailureHandler;
        setAuthenticationFailureHandler(myAuthenctiationFailureHandler);
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse res = (HttpServletResponse) response;

        if (processUrl.equals(req.getServletPath()) && "POST".equalsIgnoreCase(req.getMethod())) {
            Object expect = req.getSession().getAttribute(Oauth2Const.VERIFYCODE_SESSION_KEY);
            String code = req.getParameter("code");
            log.info("========expect: " + expect + " code:" + code);

            try {
                validImage(req, res, code, expect);
            } catch (AuthenticationException e) {
                myAuthenctiationFailureHandler.onAuthenticationFailure(req, res, e);
                return;
            }
        }
        chain.doFilter(request, response);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse)
            throws AuthenticationException {
        return null;
    }

    /**
     * @param
     * @param
     * @param code    获取的验证码参数
     * @param verCode session中保存的验证码
     * @throws IOException
     * @throws ServletException
     */
    public void validImage(HttpServletRequest req, HttpServletResponse res, String code, Object verCode)  {


        String verCodeStr;
        if (null == verCode) {
            throw new InsufficientAuthenticationException(Oauth2Const.VERIFYCODE_FAILURE);
        } else {
            verCodeStr = verCode.toString();
        }
        LocalDateTime localDateTime = (LocalDateTime) req.getSession().getAttribute("codeTime");

        long past = localDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
        long now = LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();

        if (verCodeStr == null || code == null || code.isEmpty() || !verCodeStr.equalsIgnoreCase(code)) {
            throw new InsufficientAuthenticationException(Oauth2Const.VERIFYCODE_ERROR);
        } else if ((now - past) / 1000 / 60 > 2) {//两分钟
            throw new InsufficientAuthenticationException(Oauth2Const.VERIFYCODE_EXPIRED);
        } else {
            //验证成功,删除存储的验证码
            req.getSession().removeAttribute(Oauth2Const.VERIFYCODE_SESSION_KEY);
        }
    }
}
}

processUrl是spring security拦截的请求地址,failureUrl是验证失败时的跳转地址。在生成验证码的时候,要将验证码存到session中。验证时从session中获取验证码,并将session中的验证码移除,否则可以重复登录(使用浏览器的重发功能)。

MyAuthenctiationFailureHandler :自定义登录失败处理器。

void validImage(HttpServletRequest req, HttpServletResponse res, String code, Object verCode)该方法是图片验证码验证方法。

unsuccessfulAuthentication(req, res, new InsufficientAuthenticationException(Oauth2Const.VERIFYCODE_ERROR)); 验证失败会执行MyAuthenctiationFailureHandler 中的 onAuthenticationFailure ()方法。

然后将自己的过滤器加到spring security的过滤器链中。如:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
      http.addFilterBefore(new CaptchaAuthenticationFilter("/login", myAuthenctiationFailureHandler), UsernamePasswordAuthenticationFilter.class);

        http.authorizeRequests()
                .antMatchers("/").hasRole("USER")
                .antMatchers("/index").hasRole("USER")
                .antMatchers("/message/*").hasRole("USER")
                .anyRequest().permitAll()
                .and().formLogin().loginPage("/login").defaultSuccessUrl("/index").failureUrl("/login?error1").permitAll()
                .and().rememberMe().tokenValiditySeconds(60*60*7).key("message")
                .and().logout().logoutUrl("/logout").logoutSuccessUrl("/login").permitAll();
    }
}

贴一张图片大家就一目了然了,我们是在UsernamePasswordAuthenticationFilter 之前插入了我们自定义的过滤器,先进行图片验证码的验证。验证成功 后继续进行账号密码的登录认证。

springsecurity验证码 redis springboot security 验证码_spring

 

账号锁定这里给一个实现思路吧,在自定义登录失败处理器MyAuthenctiationFailureHandler中,记录账号异常信息 “Bad credentials”  的次数,可以保存在redis中设置过期时间,超过一定次数 修改 用户状态,进行锁定。这样可以结局暴力撞库的问题,提高系统安全性。