内部系统要求高安全性,原始的账号密码 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();
}
}
开发,越来越简单。