3、鉴权拦截器设计模式:Spring Boot Interceptor/Filter原理、自定义注解实现鉴权、拦截器链的执行顺序与异常处理
在Redis缓存后端鉴权架构中,拦截器(Interceptor)与过滤器(Filter)是承载鉴权逻辑的核心入口。本章深入剖析两者的底层原理、执行顺序、自定义注解鉴权实现,以及拦截器链中的异常处理策略,帮助你在高并发场景下构建稳定、可扩展的鉴权拦截层。
3.1 Filter与Interceptor:核心原理与差异对比
Filter基于Servlet规范,由Servlet容器管理,作用于Web请求的最外层;Interceptor基于Spring AOP,由Spring IoC容器管理,作用于Handler执行前后。两者在鉴权场景中的职责边界如下:
| 对比维度 | Filter(过滤器) | Interceptor(拦截器) |
|---|---|---|
| 底层规范 | Servlet 2.3+ 规范 | Spring MVC 框架 |
| 容器依赖 | Servlet 容器(Tomcat/Jetty) | Spring IoC 容器 |
| 执行时机 | 请求进入 Servlet 之前 / 响应返回客户端之前 | HandlerMapping 之后 / HandlerAdapter 之前 |
| 可访问资源 | HttpServletRequest / HttpServletResponse | HandlerMethod、ModelAndView、异常对象 |
| 鉴权适用场景 | 全局字符编码、跨域、IP黑名单、基础安全过滤 | 业务鉴权、权限校验、日志记录、性能监控 |
| 链式调用 | FilterChain.doFilter() 手动放行 | preHandle / postHandle / afterCompletion 三阶段 |
架构决策建议: 在Redis鉴权场景中,推荐使用Interceptor承载核心鉴权逻辑。原因有三:① 可获取HandlerMethod,便于解析自定义注解;② 支持afterCompletion阶段释放Redis连接资源;③ 异常处理更精细,可捕获Controller层抛出的鉴权异常。
3.2 自定义注解鉴权:从定义到解析
通过自定义注解 + Interceptor的组合,可以实现声明式鉴权,避免在每个Controller方法中重复编写鉴权代码。以下为完整实现链路:
3.2.1 定义鉴权注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireAuth {
String[] roles() default {};
boolean checkRedisToken() default true;
long tokenExpireSeconds() default 3600;
}
3.2.2 实现鉴权拦截器
@Component
public class AuthInterceptor implements HandlerInterceptor {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true; // 静态资源直接放行
}
HandlerMethod method = (HandlerMethod) handler;
RequireAuth annotation = method.getMethodAnnotation(RequireAuth.class);
if (annotation == null) {
annotation = method.getBeanType().getAnnotation(RequireAuth.class);
}
if (annotation == null) {
return true; // 无注解则放行
}
// 1. 从请求头提取Token
String token = request.getHeader("Authorization");
if (token == null || token.isEmpty()) {
throw new AuthException("缺少鉴权令牌");
}
// 2. 校验Redis中Token有效性
String redisKey = "auth:token:" + token;
if (Boolean.FALSE.equals(redisTemplate.hasKey(redisKey))) {
throw new AuthException("令牌已过期或无效");
}
// 3. 角色校验(若注解指定了角色)
if (annotation.roles().length > 0) {
String userRole = (String) redisTemplate.opsForValue()
.get("auth:role:" + token);
boolean matched = Arrays.stream(annotation.roles())
.anyMatch(role -> role.equals(userRole));
if (!matched) {
throw new AuthException("权限不足");
}
}
// 4. 续期Token(滑动过期策略)
if (annotation.checkRedisToken()) {
redisTemplate.expire(redisKey,
annotation.tokenExpireSeconds(), TimeUnit.SECONDS);
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler, Exception ex) {
// 清理线程绑定的Redis连接资源
RedisConnectionUtils.unbindConnection(
redisTemplate.getConnectionFactory());
}
}
性能要点: 在preHandle中完成Token校验后,建议将用户ID、角色等鉴权结果存入ThreadLocal或Request Attribute,避免后续业务逻辑重复查询Redis。同时,afterCompletion阶段务必释放Redis连接,防止连接泄漏。
3.3 拦截器链的执行顺序与异常处理
当系统中存在多个拦截器时,执行顺序由注册顺序决定。以下为典型的多拦截器鉴权链:
3.3.1 执行顺序规则
- preHandle 正向执行: 按注册顺序依次执行,若某个拦截器返回 false,后续拦截器及 Controller 不再执行。
- postHandle 逆向执行: Controller 执行完毕后,按注册逆序执行 postHandle。
- afterCompletion 始终执行: 无论是否发生异常,均按注册逆序执行,适合资源清理。
3.3.2 异常处理策略
在鉴权拦截器中抛出异常时,推荐使用全局异常处理器统一捕获,避免在拦截器内直接处理响应:
@ControllerAdvice
public class GlobalAuthExceptionHandler {
@ExceptionHandler(AuthException.class)
@ResponseBody
public ResponseEntity<Map<String, Object>> handleAuthException(
AuthException e) {
Map<String, Object> result = new HashMap<>();
result.put("code", 401);
result.put("message", e.getMessage());
result.put("timestamp", System.currentTimeMillis());
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(result);
}
@ExceptionHandler(Exception.class)
@ResponseBody
public ResponseEntity<Map<String, Object>> handleGenericException(
Exception e) {
Map<String, Object> result = new HashMap<>();
result.put("code", 500);
result.put("message", "服务器内部错误");
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(result);
}
}
避坑指南: 不要在拦截器preHandle中直接调用 response.getWriter().write() 返回错误信息,这会导致后续异常处理器无法正常工作。正确的做法是抛出业务异常,由 @ControllerAdvice 统一处理,保持异常处理链路完整。
3.4 注册拦截器与配置顺序
通过实现 WebMvcConfigurer 接口注册拦截器,注册顺序即执行顺序:
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private AuthInterceptor authInterceptor;
@Autowired
private LogInterceptor logInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 先注册的拦截器先执行 preHandle
registry.addInterceptor(logInterceptor)
.addPathPatterns("/api/**")
.order(1); // 显式指定顺序
registry.addInterceptor(authInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns("/api/auth/login", "/api/auth/register")
.order(2);
}
}
| 配置项 | 说明 | 推荐值 |
|---|---|---|
| addPathPatterns | 拦截的URL路径 | /api/**(仅拦截API接口) |
| excludePathPatterns | 排除的URL路径 | 登录、注册、健康检查等无需鉴权的接口 |
| order | 拦截器执行顺序(数值越小优先级越高) | 日志拦截器 order=1,鉴权拦截器 order=2 |
性能调优要点: 在Redis鉴权拦截器中,建议对Token校验结果添加本地缓存(如Caffeine),减少对Redis的重复查询。对于静态资源路径(/static/、/favicon.ico等),务必通过excludePathPatterns排除,避免不必要的鉴权开销。实测表明,合理配置排除路径可降低拦截器CPU开销约30%。