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<? extends GrantedAuthority> 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.
     * <ol>
     * <li>Clears the {@link SecurityContextHolder}</li>
     * <li>Stores the exception in the session (if it exists or
     * <tt>allowSesssionCreation</tt> is set to <tt>true</tt>)</li>
     * <li>Informs the configured <tt>RememberMeServices</tt> of the failed login</li>
     * <li>Delegates additional behaviour to the {@link AuthenticationFailureHandler}.</li>
     * </ol>
     */
    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";
    }
}
  • 最后在登录界面添加一个验证码输入框
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Spring Security Example </title>
    </head>
    <body>
        <div th:if="${param.error}">
            Invalid username and password.
        </div>
        <div th:if="${param.logout}">
            You have been logged out.
        </div>
        <div th:if="${#httpServletRequest.getAttribute('codeError')} == true">
            验证码错误!
        </div>
        <form th:action="@{/login}" method="post">
            <div><label> User Name : <input type="text" name="username"/> </label></div>
            <div><label> Password: <input type="password" name="password"/> </label></div>
            <div><label> code: <input type="text" name="code"/> </label></div>
            <div><input type="submit" value="Sign In"/></div>
        </form>
    </body>
</html>

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

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

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,133评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,682评论 3 390
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,784评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,508评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,603评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,607评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,604评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,359评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,805评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,121评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,280评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,959评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,588评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,206评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,442评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,193评论 2 367
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,144评论 2 352

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,647评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,800评论 6 342
  • 驾校隔一天练一次车真是让我很心急又很无奈。刚刚去浏览了一遍这个日志列表发现我明明还看了很多的...影视作品,但是我...
    圈圈又青青阅读 128评论 0 0
  • 吃过晚饭,正要收拾碗筷,楼下突然传来激烈的争吵声。仔细听听,原来是两口子准备离婚,为孩子,房子,车子和存款这达不成...
    优美阅读阅读 276评论 2 1
  • 本文导读:padding margin都是边距的含义,关键问题得明白是什么相对什么的边距.padding是控件的内...
    随风化作雨阅读 6,937评论 0 3