摘要: 在上一章,我们学习了如何使用Filter作为请求的“前哨”来处理一些底层的、与业务无关的横切任务。然而,Filter的一大局限是它无法感知到Spring MVC的内部世界,比如它不知道请求将由哪个Controller的哪个方法来处理。当我们想实现更精细的控制,例如“只有管理员角色的用户才能调用
deleteUser方法”时,Filter就显得力不从心了。为此,Spring MVC提供了自己的拦截机制——拦截器(HandlerInterceptor)。本章,我们将深入学习这个强大的组件,了解它如何工作在Spring MVC的核心流程中,并最终通过实战,用它来构建一套优雅、灵活的API认证与授权体系。
引言:当Filter“不够聪明”时
Filter像是一个忠诚但“眼神不太好”的门卫,他能检查所有进出的人(HTTP请求),但看不清他们具体要去哪个房间(Controller方法)。如果我们的需求是:“所有进入大楼的人都必须佩戴工牌”,Filter可以胜任。但如果需求变成:“只有VIP客户才能进入888号房间”,门卫就没辙了,因为不认识房间号。
这就是拦截器(Interceptor)大显身手的舞台。拦截器是Spring MVC框架自己提供的“楼层管理员”,他工作在DispatcherServlet之后,能清楚地知道每个请求的目的地——即哪个HandlerMethod(控制器方法)将会被执行。这使得拦截器能够实现基于业务逻辑的、更智能的拦截与处理。
一、拦截器 vs. 过滤器:更深层次的剖析
在投入实战之前,我们必须清晰地理解拦截器和过滤器的核心区别。
| 特性 | 过滤器 (Filter) | 拦截器 (Interceptor) |
|---|---|---|
| 归属 | Java Servlet规范,Web容器管理 | Spring MVC框架一部分,Spring容器管理 |
| 执行时机 | 在DispatcherServlet之前 | 在DispatcherServlet之后,Controller方法执行前后 |
| 上下文感知 | 只能访问原始的HttpServletRequest/Response | 能访问HandlerMethod、ModelAndView等Spring上下文 |
| 控制粒度 | 粗粒度,基于URL Pattern拦截 | 细粒度,可基于处理器方法、注解等进行拦截 |
| 典型用途 | 字符编码、GZIP压缩、低级别安全过滤 | 认证、授权、日志记录、修改模型数据 |
HandlerInterceptor接口的三个方法
preHandle(req, res, handler): 执行于Controller方法之前。handler参数是关键,我们可以从中获取即将执行的方法信息。此方法返回true则继续执行,返回false则中断请求。认证授权的核心逻辑在此实现。postHandle(req, res, handler, mv): 执行于Controller方法之后,视图渲染之前。可以用来修改ModelAndView对象,向页面添加公共数据。afterCompletion(req, res, handler, ex): 在整个请求处理完毕后(包括视图渲染)执行。通常用于资源清理工作。无论是否发生异常,只要preHandle返回true,此方法总会被调用。
二、实战:使用拦截器实现Token认证
假设我们的API需要一个简单的Token认证:所有/api/v1/secure/**路径下的请求,都必须在HTTP头中携带一个有效的X-Auth-Token。
1. 创建认证拦截器
package com.example.myfirstapp.interceptor;
import com.example.myfirstapp.model.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
@Component // 虽然拦截器本身需要注册,但声明为Bean可以方便地在其中注入其他服务
public class AuthInterceptor implements HandlerInterceptor {
private static final String VALID_TOKEN = "secret-token-12345";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
String token = request.getHeader("X-Auth-Token");
if (!StringUtils.hasText(token) || !VALID_TOKEN.equals(token)) {
// 认证失败
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401
response.setContentType("application/json;charset=UTF-8");
// 手动构造一个标准的错误响应
Result<Void> errorResult = Result.error(401, "Unauthorized: Invalid or missing token.");
// 使用Jackson将对象转换为JSON字符串并写入响应
ObjectMapper objectMapper = new ObjectMapper();
response.getWriter().write(objectMapper.writeValueAsString(errorResult));
return false; // 中断请求
}
// 认证成功,放行
return true;
}
}
2. 注册拦截器
与Filter不同,拦截器需要在一个实现了WebMvcConfigurer的配置类中显式注册。
package com.example.myfirstapp.config;
import com.example.myfirstapp.interceptor.AuthInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private AuthInterceptor authInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor)
.addPathPatterns("/api/v1/users/**") // 1. 指定要拦截的路径
.excludePathPatterns("/api/v1/users/login"); // 2. 指定要排除的路径
}
}
代码解读:
addPathPatterns: 定义了此拦截器生效的URL模式。这里我们拦截所有用户相关的API。excludePathPatterns: 定义了需要排除在外的URL模式。通常,登录、注册等接口是不需要认证的。
3. 效果演示
- 不带Token访问:
curl -i http://localhost:8080/api/v1/usersHTTP/1.1 401 Content-Type: application/json;charset=UTF-8 ... {"code":401,"message":"Unauthorized: Invalid or missing token.","data":null} - 带正确Token访问:
curl -i -H "X-Auth-Token: secret-token-12345" http://localhost:8080/api/v1/usersHTTP/1.1 200 ... {"code":200,"message":"Success","data":[{"id":1,...}]}
三、进阶:结合自定义注解实现动态授权
认证(Authentication)解决了“你是谁”的问题,而授权(Authorization)解决的是“你能做什么”的问题。让我们实现一个更高级的功能:只有特定角色的用户才能调用某些方法。
1. 创建自定义注解@RequiresRole
package com.example.myfirstapp.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresRole {
String value(); // 用于指定需要的角色,例如 "ADMIN"
}
2. 改造AuthInterceptor以支持授权
// 在AuthInterceptor中添加...
import org.springframework.web.method.HandlerMethod;
import com.example.myfirstapp.annotation.RequiresRole;
// ... preHandle方法修改如下
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
// ... (前面的Token认证逻辑保持不变)
// 检查是否需要角色授权
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
RequiresRole requiresRole = handlerMethod.getMethodAnnotation(RequiresRole.class);
if (requiresRole != null) {
// 此方法需要角��验证
String requiredRole = requiresRole.value();
// 伪代码:从Token中解析用户角色,实际应从数据库或缓存获取
String userRole = getUserRoleFromToken(token);
if (!requiredRole.equals(userRole)) {
// 角色不匹配,授权失败
response.setStatus(HttpServletResponse.SC_FORBIDDEN); // 403
response.setContentType("application/json;charset=UTF-8");
Result<Void> errorResult = Result.error(403, "Forbidden: Insufficient privileges.");
response.getWriter().write(new ObjectMapper().writeValueAsString(errorResult));
return false;
}
}
}
return true; // 放行
}
// 伪代码方法
private String getUserRoleFromToken(String token) {
// 在真实应用中,这里会解析JWT或查询数据库来获取用户角色
// 为演示方便,我们简单处理
if (VALID_TOKEN.equals(token)) {
return "ADMIN"; // 假设这个token对应的用户是ADMIN
}
return "GUEST";
}
3. 在Controller中使用注解
// 在UserController的deleteUser方法上添加注解
@DeleteMapping("/{id}")
@RequiresRole("ADMIN") // 只有ADMIN角色的用户才能删除
public Result<Void> deleteUser(@PathVariable Long id) {
// ... 删除逻辑
}
现在,即使用户提供了正确的Token,只要其角色不是ADMIN,也无法调用deleteUser方法,实现了精细到方法级别的授权控制。
总结
Spring MVC拦截器是实现动态、上下文感知拦截的利器,是构建Web应用安全体系的核心组件。
- 拦截器 vs. 过滤器: 拦截器更“聪明”,能感知到Spring MVC的上下文,适合处理与业务相关的拦截,如认证和授权。
- 核心方法:
preHandle是实现拦截逻辑的关键,可以通过返回false来中断请求。 - 注册与配置: 需要通过
WebMvcConfigurer来注册,并可以灵活地配置拦截和排除的路径。 - 高级用法: 结合自定义注解,可以实现极其灵活和声明式的授权控制,大大提升了代码的可读性和可维护性。
预告: 我们已经掌握了请求处理的“前置”和“环绕”技术(Filter和Interceptor)。但Spring MVC的灵活性远不止于此。如果我们想自定义Controller方法的参数本身是如何被创建和注入的呢?例如,能否创建一个
@CurrentUser注解,直接在方法参数中获取当前登录的用户对象,而无需每次都从request或SecurityContext中手动提取?下一章,我们将探索另一个强大的Web组件:掌握Web组件(三):使用HandlerMethodArgumentResolver自定义参数解析。
754

被折叠的 条评论
为什么被折叠?



