1. 为什么需要登录验证拦截器
在Web应用开发中,登录验证是最基础的安全防护措施。想象一下你家的防盗门——拦截器就是那个在每次有人敲门时先通过猫眼确认访客身份的保安。传统做法是在每个Controller方法里重复编写验证逻辑,就像给每个房间都单独配个保安,这显然既浪费资源又难以维护。
SpringMVC拦截器的核心价值在于提供AOP式的统一处理能力。通过实现HandlerInterceptor接口,我们可以在三个关键节点插入逻辑:
- preHandle:请求到达Controller前(相当于检查邀请函)
- postHandle:Controller执行后,视图渲染前(相当于记录访客信息)
- afterCompletion:整个请求完成后(相当于送客后的清理工作)
实际项目中常见误区:很多开发者会把所有权限校验都堆在preHandle,这会导致方法臃肿。合理的做法是按安全等级分层处理,基础验证(如登录态)放拦截器,细粒度权限(如RBAC)用注解+切面实现。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 拦截器实现三部曲
2.1 基础拦截器骨架
java复制public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
// 1. 排除静态资源路径
if (isStaticResource(request.getRequestURI())) {
return true;
}
// 2. 检查会话中的登录标记
HttpSession session = request.getSession(false);
if (session == null || session.getAttribute("user") == null) {
response.sendRedirect("/login");
return false; // 中断请求链
}
return true;
}
private boolean isStaticResource(String uri) {
return uri.startsWith("/static/") || uri.endsWith(".ico");
}
}
2.2 注册拦截器配置
Spring Boot中的配置方式(注意排除Swagger等调试接口):
java复制@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AuthInterceptor())
.addPathPatterns("/**")
.excludePathPatterns(
"/login",
"/register",
"/swagger**/**",
"/webjars/**",
"/v3/api-docs"
);
}
}
2.3 会话管理增强方案
单纯依赖Session存在分布式环境下的扩展性问题。推荐组合方案:
java复制// 使用Redis存储会话信息
String sessionKey = "user:session:" + request.getSession().getId();
UserDTO user = redisTemplate.opsForValue().get(sessionKey);
if (user == null) {
// 尝试通过token恢复会话
String token = request.getHeader("X-Auth-Token");
if (StringUtils.isNotBlank(token)) {
user = tokenService.validateToken(token);
if (user != null) {
request.getSession().setAttribute("user", user);
redisTemplate.opsForValue().set(sessionKey, user, 30, TimeUnit.MINUTES);
}
}
}
3. 执行流程深度解析
当请求到达SpringMVC时,拦截器的工作时序如下(以登录验证为例):
-
DispatcherServlet接收请求
- 根据HandlerMapping找到对应Controller方法
- 生成HandlerExecutionChain(包含拦截器列表)
-
preHandle阶段
mermaid复制sequenceDiagram participant Client participant DispatcherServlet participant Interceptor participant Controller Client->>DispatcherServlet: HTTP Request DispatcherServlet->>Interceptor: preHandle() alt 验证通过 Interceptor-->>DispatcherServlet: true DispatcherServlet->>Controller: 执行方法 else 验证失败 Interceptor-->>Client: 重定向响应 end -
postHandle阶段
- 只有preHandle返回true的拦截器会执行
- 可修改ModelAndView对象(如添加公共用户信息)
-
afterCompletion阶段
- 无论Controller是否抛出异常都会执行
- 适合资源清理、请求耗时统计等操作
性能陷阱:拦截器链过长会影响吞吐量。实测显示每增加一个拦截器,QPS下降约3-5%。建议通过@Order控制必要拦截器的顺序,非核心逻辑考虑用过滤器(Filter)实现。
4. 实战中的六个避坑指南
4.1 路径匹配的隐藏规则
Spring的Ant路径匹配有时会有反直觉行为:
java复制.addPathPatterns("/api/**")
// 能匹配 /api/user 但不会匹配 /api
// 需要显式添加 /api 和 /api/
.excludePathPatterns("/public/*")
// 仅匹配单级路径如 /public/test
// 多级路径需用 /**
4.2 异步请求的特殊处理
当Controller返回Callable或DeferredResult时:
- preHandle在主线程执行
- postHandle和afterCompletion在异步线程执行
- 需要额外处理线程上下文传递问题
解决方案:
java复制@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
// 保存当前线程变量
RequestContextHolder.setRequestAttributes(
RequestContextHolder.getRequestAttributes(),
true); // 开启继承模式
return true;
}
4.3 拦截器与过滤器的抉择
| 特性 | 拦截器(Interceptor) | 过滤器(Filter) |
|---|---|---|
| 依赖容器 | Spring MVC | Servlet API |
| 获取Spring Bean | 支持 | 不支持 |
| 执行位置 | DispatcherServlet内 | Servlet容器层 |
| 处理时机 | Controller方法前后 | 请求进入Servlet前 |
| 异常处理 | 可捕获Controller异常 | 只能全局处理 |
经验法则:需要Spring上下文参与的用拦截器,纯HTTP层处理的用过滤器。
4.4 性能监控实现示例
利用拦截器实现API耗时统计:
java复制@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
long startTime = System.currentTimeMillis();
request.setAttribute("startTime", startTime);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler, Exception ex) {
long endTime = System.currentTimeMillis();
long duration = endTime - (long)request.getAttribute("startTime");
metricsService.recordLatency(request.getRequestURI(), duration);
}
4.5 多拦截器协作模式
典型的安全拦截器链配置:
java复制registry.addInterceptor(new RateLimitInterceptor()).order(1);
registry.addInterceptor(new AuthInterceptor()).order(2);
registry.addInterceptor(new LoggingInterceptor()).order(3);
执行顺序:
code复制preHandle 1 → preHandle 2 → preHandle 3
→ Controller
→ postHandle 3 → postHandle 2 → postHandle 1
→ afterCompletion 3 → afterCompletion 2 → afterCompletion 1
4.6 单元测试技巧
测试拦截器的正确姿势:
java复制@SpringBootTest
@AutoConfigureMockMvc
class AuthInterceptorTest {
@Autowired
private MockMvc mockMvc;
@Test
void shouldRedirectWhenUnauthenticated() throws Exception {
mockMvc.perform(get("/dashboard"))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrlPattern("**/login"));
}
@Test
void shouldPassWithValidSession() throws Exception {
MockHttpSession session = new MockHttpSession();
session.setAttribute("user", new User("test"));
mockMvc.perform(get("/dashboard").session(session))
.andExpect(status().isOk());
}
}
5. 进阶:动态权限拦截方案
对于需要RBAC的场景,可以扩展拦截器:
java复制@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
// 基础登录验证
if (!checkLogin(request)) {
return false;
}
// 方法级权限检查
if (handler instanceof HandlerMethod) {
HandlerMethod hm = (HandlerMethod)handler;
RequiredPermission perm = hm.getMethodAnnotation(
RequiredPermission.class);
if (perm != null) {
User user = getCurrentUser(request);
if (!permissionService.check(user, perm.value())) {
throw new AccessDeniedException("权限不足");
}
}
}
return true;
}
配合注解使用:
java复制@RestController
@RequestMapping("/admin")
public class AdminController {
@RequiredPermission("user:delete")
@DeleteMapping("/users/{id}")
public ResponseEntity<?> deleteUser(@PathVariable Long id) {
// ...
}
}
这种方案相比Shiro等框架的优势在于:
- 与Spring Security解耦
- 配置更轻量级
- 适合已有成熟权限体系的改造项目
6. 拦截器与AOP的协同作战
虽然拦截器能处理很多横切关注点,但与Spring AOP配合能达到更好效果:
| 场景 | 适用技术 | 原因 |
|---|---|---|
| URL级别访问控制 | 拦截器 | 在进入Controller前拦截 |
| 方法调用参数校验 | @Valid+AOP | 利用JSR-303规范 |
| 业务操作日志记录 | @Aspect | 需要获取方法参数和返回值 |
| 全局异常处理 | @ControllerAdvice | 统一错误响应格式 |
典型组合案例——操作日志:
java复制@Aspect
@Component
public class OperationLogAspect {
@Autowired
private HttpServletRequest request;
@Around("@annotation(loggable)")
public Object around(ProceedingJoinPoint pjp, Loggable loggable)
throws Throwable {
User user = (User)request.getSession().getAttribute("user");
String operation = loggable.value();
long start = System.currentTimeMillis();
try {
Object result = pjp.proceed();
logService.save(
user.getId(),
operation,
System.currentTimeMillis() - start,
"SUCCESS");
return result;
} catch (Exception e) {
logService.save(
user.getId(),
operation,
System.currentTimeMillis() - start,
"FAIL: " + e.getMessage());
throw e;
}
}
}
7. 现代架构中的拦截器演进
随着前后端分离架构普及,拦截器的使用模式也在变化:
7.1 JWT无状态验证
java复制@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
String token = request.getHeader("Authorization");
if (StringUtils.isBlank(token)) {
sendJsonError(response, 401, "Missing token");
return false;
}
try {
Claims claims = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token.replace("Bearer ", ""))
.getBody();
request.setAttribute("userId", claims.getSubject());
return true;
} catch (JwtException e) {
sendJsonError(response, 403, "Invalid token");
return false;
}
}
private void sendJsonError(HttpServletResponse response,
int status,
String message) throws IOException {
response.setContentType("application/json");
response.setStatus(status);
response.getWriter().write(
"{\"code\":" + status + ",\"message\":\"" + message + "\"}");
}
7.2 GraphQL特殊处理
GraphQL请求的拦截需要解析query内容:
java复制@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
if (!"/graphql".equals(request.getRequestURI())) {
return true;
}
String query = getGraphQLQuery(request);
if (query.contains("mutation") && !checkAuth(request)) {
throw new AccessDeniedException("需要管理员权限");
}
return true;
}
private String getGraphQLQuery(HttpServletRequest request) {
try {
String body = request.getReader()
.lines()
.collect(Collectors.joining());
JsonNode node = objectMapper.readTree(body);
return node.path("query").asText();
} catch (IOException e) {
return "";
}
}
7.3 响应包装模式
统一响应格式的拦截器实现:
java复制@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView) {
if (isApiRequest(request)) {
Object body = modelAndView.getModel().get("body");
modelAndView.getModel().clear();
modelAndView.addObject(
"response",
new StandardResponse(200, "success", body));
}
}
private boolean isApiRequest(HttpServletRequest request) {
return request.getRequestURI().startsWith("/api/")
|| "application/json".equals(
request.getHeader("Accept"));
}
