内部系统要求高安全性,原始的账号密码 BASIC
认证方式已无法满足需求,现设计手机号、密码、验证码方式登录,静态密码与动态验证码保证安全性。
安全模块使用了 Spring Security
,为了满足验证码的需求,需自定义认证过滤器进行认证。
Spring Security
本质就是一组官方提供的认证 Filter
,通过 Filter
的权限判断,决定当前请求是否能执行到 Servlet
,如果对 Filter
还不了解的话,请参考: Servlet 过滤器 - 菜鸟教程
设计方案如下:
设计自定义的 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(); } }
开发,越来越简单。