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 拦截器链的执行顺序与异常处理

当系统中存在多个拦截器时,执行顺序由注册顺序决定。以下为典型的多拦截器鉴权链:

拦截器链执行顺序与异常处理流程 HTTP 请求 Filter: 字符编码 / 跨域 Interceptor1: 请求日志记录 Interceptor2: Redis Token 鉴权 Controller 业务处理 异常处理分支 AuthException 抛出 → @ControllerAdvice 捕获 → 返回 401/403 JSON preHandle 返回 false 则中断链 afterCompletion 始终执行

3.3.1 执行顺序规则

  1. preHandle 正向执行: 按注册顺序依次执行,若某个拦截器返回 false,后续拦截器及 Controller 不再执行。
  2. postHandle 逆向执行: Controller 执行完毕后,按注册逆序执行 postHandle。
  3. 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%。