深入剖析Spring Boot拦截器:从原理到实战
本文深入剖析Spring Boot 3.x拦截器的核心原理与调用链机制,通过一个基于自定义注解@AuthRequired的权限拦截器完整案例,演示从定义、注册到请求处理的全流程,并总结常见踩坑与性能优化建议,适合后端开发与架构设计人员阅读。
概述
Spring Boot 3.2.0(基于Spring Framework 6.x)中的拦截器(Interceptor)是Spring MVC提供的一种轻量级面向切面编程实现,用于在请求进入Controller前后执行通用逻辑,典型场景包括登录认证、权限校验、接口幂等判断、访问日志采集等。本文以权限认证为业务背景,分三个层次展开:先剖析拦截器链的底层执行机制,再基于自定义注解实现一套通用鉴权方案,最后通过curl命令演示完整的请求验证过程,并汇总常见问题与性能优化建议。
核心原理
1. 拦截器所处的位置
在Servlet体系中,一次HTTP请求会依次经过Filter(过滤器)与Interceptor(拦截器)。Filter由Servlet容器管理,作用于Servlet层面,无法感知Spring MVC中的处理器方法;而Interceptor由Spring容器管理,位于DispatcherServlet分发阶段,可获取运行时的HandlerMethod对象,从而拿到精确的方法参数和注解元数据,实现业务级别的横切控制。
其核心数据结构是HandlerExecutionChain——DispatcherServlet通过HandlerMapping获取它,该对象内部持有两个关键成员:
- handler:目标Controller的方法封装(即HandlerMethod)。
- interceptorList:当前请求需要经过的拦截器列表,按注册顺序排列。
DispatcherServlet中的doDispatch方法会顺序驱动拦截器链中每个拦截器的三个回调方法,形成完整的请求处理闭环。
2. HandlerInterceptor的三阶段回调
Spring提供了HandlerInterceptor接口,全部方法带默认实现,开发者只需覆写关心的环节:
public interface HandlerInterceptor {
// 请求到达Controller之前执行;返回false则中断请求链
default boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
return true;
}
// 处理器方法执行后、视图渲染前执行,可修改ModelAndView
default void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler,
@Nullable ModelAndView modelAndView) throws Exception {
}
// 整个请求结束(含视图渲染完毕)后的回调,常用于资源清理
default void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
@Nullable Exception ex) throws Exception {
}
}
三个回调的调用时机与顺序规则如下:
preHandle:按注册顺序正序执行。任一拦截器返回false,后续拦截器和Controller方法均不再执行。postHandle:按注册顺序逆序执行。仅在Controller方法成功返回后触发。afterCompletion:按注册顺序逆序执行。无论Controller方法是否抛出异常,只要对应的preHandle已返回true,该方法必然被回调。
这个正序进、逆序出的行为,与栈结构完全一致,能够在控制器执行完毕后,按依赖的反方向完成资源释放。
3. 基于注解的拦截器设计原理
传统拦截器常用request.getRequestURI()结合配置表判断接口权限,但这种方式难以应对Spring Cloud微服务或RESTful API的动态路由。更优雅的方案是通过自定义注解声明式地标记权限需求,拦截器借助HandlerMethod反射获取目标方法和目标类的注解元数据,在preHandle阶段完成鉴权。
其核心原理是:请求进入拦截器时,handler参数实际上是HandlerMethod实例,通过handlerMethod.getMethodAnnotation()能够直接拿到方法上标注的注解对象,配合@Target、@Retention等元注解即构成一套可配置的鉴权体系。
下面给出完整实现。首先定义权限注解:
package com.example.interceptor.annotation;
import java.lang.annotation.*;
/**
* 自定义权限认证注解
* 标注在Controller方法或类上,表示访问该资源需要登录及指定角色
*/
@Documented
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthRequired {
/**
* 允许访问的角色列表;为空时仅要求登录,不校验角色
*/
String[] roles() default {};
}
然后实现核心拦截器:
package com.example.interceptor.config;
import com.example.interceptor.annotation.AuthRequired;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.Arrays;
import java.util.Map;
/**
* 权限认证拦截器
* 核心职责:在请求进入Controller之前完成登录态校验与角色鉴权
*/
@Component
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// 1. 仅拦截控制器方法(静态资源等无HandlerMethod元数据,直接放行)
if (!(handler instanceof HandlerMethod handlerMethod)) {
return true;
}
// 2. 优先取方法级注解,其次取类级注解
AuthRequired authRequired = handlerMethod.getMethodAnnotation(AuthRequired.class);
if (authRequired == null) {
authRequired = handlerMethod.getBeanType().getAnnotation(AuthRequired.class);
}
// 3. 未标注@AuthRequired的接口视为公开接口
if (authRequired == null) {
return true;
}
// 4. 校验登录态:从请求头中解析Bearer Token
String token = request.getHeader("Authorization");
if (token == null || !token.startsWith("Bearer ")) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"message\":\"未登录或登录已过期\"}");
return false; // 返回false中断请求链
}
// 5. 从Token中解析会话数据(生产环境应查Redis或SSO服务)
Map<String, String> session = resolveSession(token);
request.setAttribute("userId", session.get("userId"));
request.setAttribute("userRole", session.get("role"));
// 6. 角色细粒度鉴权
String[] roles = authRequired.roles();
if (roles.length > 0 && !Arrays.asList(roles).contains(session.get("role"))) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":403,\"message\":\"权限不足\"}");
return false;
}
return true;
}
/**
* 模拟从Token中解析会话信息
* 演示逻辑:token包含"admin"关键字对应管理员角色,否则为普通用户
*/
private Map<String, String> resolveSession(String token) {
if (token.contains("admin")) {
return Map.of("userId", "10001", "role", "ADMIN");
}
return Map.of("userId", "10002", "role", "USER");
}
}
最后通过WebMvcConfigurer注册拦截器,并配置拦截路径:
package com.example.interceptor.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 拦截器注册配置
* 以声明式方式注册拦截器并定义拦截范围
*/
浙公网安备 33010602011771号