转载

Spring Security 自定义认证过滤器

引言

内部系统要求高安全性,原始的账号密码 BASIC 认证方式已无法满足需求,现设计手机号、密码、验证码方式登录,静态密码与动态验证码保证安全性。

安全模块使用了 Spring Security ,为了满足验证码的需求,需自定义认证过滤器进行认证。

Spring Security 自定义认证过滤器

实现

认证流程

Spring Security 本质就是一组官方提供的认证 Filter ,通过 Filter 的权限判断,决定当前请求是否能执行到 Servlet ,如果对 Filter 还不了解的话,请参考: Servlet 过滤器 - 菜鸟教程

Spring Security 自定义认证过滤器

设计

设计方案如下:

Spring Security 自定义认证过滤器

设计自定义的 Yunzhi Filter ,该过滤器再 Basic 认证过滤器之前执行,如果验证码错误,直接 401 返回,如果验证码正确,再执行后续过滤器链。

学习

怎么编写过滤器呢?

Spring Security 中, Basic 认证采用 BasicAuthenticationFilter ,研读核心源码。

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
    final boolean debug = this.logger.isDebugEnabled();
    try {
        // 使用 basic 方式从 request 中截取用户名密码认证令牌
        UsernamePasswordAuthenticationToken authRequest = authenticationConverter.convert(request);

        // 如果令牌为空,表示不是该种认证方式,终止执行,继续执行后续过滤器链
        if (authRequest == null) {
            chain.doFilter(request, response);
            return;
        }

        // 从令牌中获取用户名
        String username = authRequest.getName();

        if (debug) {
            this.logger
                    .debug("Basic Authentication Authorization header found for user '"
                            + username + "'");
        }

        // 判断该用户是否需要身份认证
        if (authenticationIsRequired(username)) {
            // 需要认证,进行身份认证
            Authentication authResult = this.authenticationManager
                    .authenticate(authRequest);

            if (debug) {
                this.logger.debug("Authentication success: " + authResult);
            }

            // 认证成功,结果存储到 Security 上下文中
            SecurityContextHolder.getContext().setAuthentication(authResult);

            // 调用 loginSuccess 方法
            this.rememberMeServices.loginSuccess(request, response, authResult);

            // 执行认证成功后的回调方法,protected,方便子类重写
            onSuccessfulAuthentication(request, response, authResult);
        }

    }
    catch (AuthenticationException failed) {
        // 认证失败 清空 Security 上下文
        SecurityContextHolder.clearContext();

        if (debug) {
            this.logger.debug("Authentication request for failed: " + failed);
        }

        // 调用 loginFail 方法
        this.rememberMeServices.loginFail(request, response);

        // 执行认证失败后的回调方法,protected,方便子类重写
        onUnsuccessfulAuthentication(request, response, failed);

        if (this.ignoreFailure) {
            // 忽略失败,继续执行过滤器链
            chain.doFilter(request, response);
        }
        else {
            this.authenticationEntryPoint.commence(request, response, failed);
        }

        // 发生异常,终止后续过滤器链执行
        return;
    }

    // 认证结束,继续执行后续过滤器链
    chain.doFilter(request, response);
}

编码

编码其实是最简单的一步。

此过滤器与 Basic 最大的区别就是,当前过滤器是判断哪些请求是允许执行后续认证过滤器链的,相当于一个小保安,只负责拦截,但不具备颁发认证令牌的功能。

@Component
public class YunzhiAuthenticationFilter extends OncePerRequestFilter {

    private static final Logger logger = LoggerFactory.getLogger(YunzhiAuthenticationFilter.class);

    // basic 认证转换器
    private final BasicAuthenticationConverter authenticationConverter = new BasicAuthenticationConverter();

    private final ValidationCodeService validationCodeService;

    public YunzhiAuthenticationFilter(ValidationCodeService validationCodeService) {
        this.validationCodeService = validationCodeService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        logger.debug("从请求中截取 basic 认证信息");
        UsernamePasswordAuthenticationToken authRequest = authenticationConverter.convert(request);

        logger.debug("如果没有认证信息,跳过该过滤器,直接执行后续过滤器链");
        if (authRequest == null) {
            chain.doFilter(request, response);
            return;
        }

        logger.debug("截取用户名");
        String username = authRequest.getName();

        logger.debug("如果不需要认证,终止当前过滤器,直接执行后续过滤器链");
        if (!authenticationIsRequired(username)) {
            chain.doFilter(request, response);
            return;
        }

        logger.debug("截取验证码");
        String verificationCode = request.getHeader("verificationCode");

        logger.debug("验证码无效,返回 401,中断过滤器链");
        if (!this.validationCodeService.validateCode(username, verificationCode)) {
            response.sendError(HttpStatus.UNAUTHORIZED.value(), "验证码无效");
            return;
        }

        logger.debug("验证成功,执行之后的过滤器链");
        chain.doFilter(request, response);
    }

    /**
     * 该用户是否需要认证
     * @param username 用户名
     */
    private boolean authenticationIsRequired(String username) {
        logger.debug("查询已有认证信息");
        Authentication existingAuth = SecurityContextHolder.getContext()
                .getAuthentication();

        logger.debug("如果不存在认证信息 或 认证信息失效 则需要认证");
        if (existingAuth == null || !existingAuth.isAuthenticated()) {
            return true;
        }

        logger.debug("如果是用户名密码令牌 认证信息不匹配 需要认证");
        if (existingAuth instanceof UsernamePasswordAuthenticationToken
                && !existingAuth.getName().equals(username)) {
            return true;
        }

        logger.debug("如果是匿名令牌 需要认证");
        if (existingAuth instanceof AnonymousAuthenticationToken) {
            return true;
        }

        logger.debug("令牌合法 无需认证");
        return false;
    }
}

Basic 过滤器前加入自定义过滤器:

@Component
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final YunzhiAuthenticationFilter yunzhiAuthenticationFilter;

    public SecurityConfig(YunzhiAuthenticationFilter yunzhiAuthenticationFilter) {
        this.yunzhiAuthenticationFilter = yunzhiAuthenticationFilter;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                // 在 basic 认证过滤器前加入自定义过滤器
                .addFilterBefore(yunzhiAuthenticationFilter, BasicAuthenticationFilter.class)
                // basic 认证
                .httpBasic()
                // 设置授权配置
                .and().authorizeRequests()
                // 开发发送验证码接口
                .antMatchers("/user/sendVerificationCode").permitAll()
                // 其余任何请求都需要认证
                .anyRequest().authenticated()
                // 禁用 csrf
                .and().csrf().disable();
    }
}

总结

开发,越来越简单。

原文  https://segmentfault.com/a/1190000021292065
正文到此结束
Loading...