一、拦截器与过滤器的区别
一句话总结:
Filter(过滤器)属于 Servlet 规范,由 Servlet 容器管理,作用于所有请求;Interceptor(拦截器)属于 Spring MVC,由 Spring 容器管理,只作用于 Controller 请求,能拿到更多 Spring MVC 上下文信息。
| 对比维度 | Filter 过滤器 | Interceptor 拦截器 |
|---|---|---|
| 所属规范/框架 | Servlet 规范 | Spring MVC |
| 管理容器 | Servlet 容器,如 Tomcat | Spring 容器 |
| 作用范围 | 所有匹配 URL 的请求,包括静态资源、Servlet、JSP | 只拦截进入 Spring MVC 的 Controller 请求 |
| 触发位置 | 在 DispatcherServlet 之前/之后 | 在 DispatcherServlet 内部,Handler 前后 |
| 核心接口/方法 | Filter 的 doFilter | HandlerInterceptor 的 preHandle、postHandle、afterCompletion |
| 依赖注入 | 默认不能直接注入 Spring Bean,需桥接 | 可以直接注入 Spring Bean |
| 可访问对象 | ServletRequest、ServletResponse | HttpServletRequest、HttpServletResponse、Handler、ModelAndView |
| 修改请求/响应 | 可通过包装类修改请求体、响应内容 | 一般不直接修改原始请求,更偏向业务拦截 |
| 异常处理 | 在 Spring MVC 之外,难以被 Spring 异常处理器处理 | 在 Spring MVC 内,可结合异常处理器 |
| 典型用途 | 编码设置、CORS、XSS 过滤、日志、压缩 | 登录校验、权限控制、日志、性能监控、统一处理 |
执行顺序大致如下:
客户端请求
-> Filter 前置处理
-> DispatcherServlet
-> Interceptor preHandle
-> Controller
-> Interceptor postHandle
-> 视图渲染
-> Interceptor afterCompletion
-> Filter 后置处理
-> 响应客户端
怎么选?
- 需要更底层、更广范围、能修改请求/响应,比如编码、跨域、请求包装:用 Filter。
- 需要 Spring 上下文、依赖注入、获取 Controller 方法信息、做登录权限和业务拦截:用 Interceptor。
简单记:Filter 更底层、范围更大;Interceptor 更贴近 Spring MVC、更偏业务。
二、如何在拦截器中修改 Request
严格来说:在 Spring MVC 的拦截器里,不能直接修改 HttpServletRequest 的参数、请求头、请求体,也不能通过重新赋值替换 request 对象让后续 Controller 使用。
拦截器里最方便能做的是:修改 request attribute。如果要改参数、header、body,正确位置是 Filter + HttpServletRequestWrapper。
2.1 拦截器中修改 request attribute
这是拦截器里最常用的方式:
@Component
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// 往 request 里放属性
request.setAttribute("userId", 1001L);
request.setAttribute("tenantId", "abc");
return true;
}
}
Controller 里可以这样取:
@GetMapping("/user")
public String user(@RequestAttribute("userId") Long userId) {
return "userId=" + userId;
}
或者:
@GetMapping("/user")
public String user(HttpServletRequest request) {
Object userId = request.getAttribute("userId");
return "userId=" + userId;
}
这种方式并没有修改原始请求参数,只是给 request 增加属性。
2.2 为什么不能在拦截器里直接改参数/请求体?
HttpServletRequest 接口本身是只读的,没有 setParameter、setHeader 方法。
而且拦截器的 preHandle 方法参数是引用传递,你就算这样写:
@Override
public boolean preHandle(HttpServletRequest request, ...) {
request = new ParameterRequestWrapper(request); // 无效
return true;
}
也没有用,因为 Controller 最终拿到的还是 DispatcherServlet 持有的原始 request,不是你这个新对象。
执行顺序大致是:
Filter -> DispatcherServlet -> Interceptor.preHandle -> Controller
所以想替换 request,必须在进入 DispatcherServlet 之前,也就是在 Filter 里做。
2.3 正确做法:Filter + HttpServletRequestWrapper
修改请求参数
自定义 Wrapper:
public class ParameterRequestWrapper extends HttpServletRequestWrapper {
private final Map<String, String[]> params = new HashMap<>();
public ParameterRequestWrapper(HttpServletRequest request) {
super(request);
params.putAll(request.getParameterMap());
}
public void setParameter(String name, String value) {
params.put(name, new String[]{value});
}
@Override
public String getParameter(String name) {
String[] values = params.get(name);
return values != null && values.length > 0 ? values[0] : null;
}
@Override
public String[] getParameterValues(String name) {
return params.get(name);
}
@Override
public Map<String, String[]> getParameterMap() {
return Collections.unmodifiableMap(params);
}
@Override
public Enumeration<String> getParameterNames() {
return Collections.enumeration(params.keySet());
}
}
在 Filter 中包装并替换:
@Component
public class ModifyRequestFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
ParameterRequestWrapper wrapper = new ParameterRequestWrapper(request);
// 修改参数
wrapper.setParameter("name", "newName");
// 关键:把包装后的 request 传给后面
filterChain.doFilter(wrapper, response);
}
}
这样 Controller 里 @RequestParam("name") 拿到的就是 newName。
修改请求头
重写 getHeader 相关方法:
public class HeaderRequestWrapper extends HttpServletRequestWrapper {
private final Map<String, String> headers = new HashMap<>();
public HeaderRequestWrapper(HttpServletRequest request) {
super(request);
Enumeration<String> names = request.getHeaderNames();
while (names.hasMoreElements()) {
String name = names.nextElement();
headers.put(name, request.getHeader(name));
}
}
public void setHeader(String name, String value) {
headers.put(name, value);
}
@Override
public String getHeader(String name) {
return headers.get(name);
}
@Override
public Enumeration<String> getHeaderNames() {
return Collections.enumeration(headers.keySet());
}
@Override
public Enumeration<String> getHeaders(String name) {
String value = headers.get(name);
return value == null
? Collections.emptyEnumeration()
: Collections.enumeration(Collections.singletonList(value));
}
}
Filter 中同样:
HeaderRequestWrapper wrapper = new HeaderRequestWrapper(request);
wrapper.setHeader("X-Token", "newToken");
filterChain.doFilter(wrapper, response);
修改请求体(如 JSON body)
如果 Controller 用的是 @RequestBody,就需要重写 getInputStream 或 getReader。
public class BodyRequestWrapper extends HttpServletRequestWrapper {
private byte[] body;
public BodyRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
this.body = StreamUtils.copyToByteArray(request.getInputStream());
}
public void setBody(byte[] body) {
this.body = body;
}
@Override
public ServletInputStream getInputStream() {
ByteArrayInputStream bais = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public boolean isFinished() {
return bais.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
}
@Override
public int read() {
return bais.read();
}
};
}
@Override
public BufferedReader getReader() throws IOException {
String encoding = getCharacterEncoding();
if (encoding == null) {
encoding = "UTF-8";
}
return new BufferedReader(new InputStreamReader(getInputStream(), encoding));
}
}
Filter 中:
@Component
public class ModifyBodyFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
BodyRequestWrapper wrapper = new BodyRequestWrapper(request);
String newBody = "{\"name\":\"newName\"}";
wrapper.setBody(newBody.getBytes(StandardCharsets.UTF_8));
filterChain.doFilter(wrapper, response);
}
}
这样 @RequestBody 解析到的就是修改后的 body。
2.4 如果非要在拦截器层面“影响参数”
常见有两种间接方案:
- 放到 request attribute,Controller 用
@RequestAttribute取。 - 用
ThreadLocal+ 自定义HandlerMethodArgumentResolver或 AOP。
这种方式复杂,容易出问题,一般不推荐。
真正要改原始参数、header、body,还是应该用 Filter。
2.5 注意点
HttpServletRequestWrapper只是包装,必须通过filterChain.doFilter(wrapper, response)传下去,否则无效。- 修改 body 后要注意
Content-Length、字符编码、流只能读一次等问题。 - Filter 顺序很重要,修改请求的 Filter 要尽量靠前。
ContentCachingRequestWrapper只能缓存请求体供重复读取,不能修改。- 拦截器适合做登录校验、权限判断、日志、往 request 放属性;不适合改原始请求数据。
总结:
- 拦截器里改 request:用
request.setAttribute。 - 改参数、header、body:用
Filter + HttpServletRequestWrapper,在filterChain.doFilter(wrapper, response)时替换 request。 - 拦截器里直接
new一个 Wrapper 赋值给 request 参数,是无效的。
三、借助拦截器实现登录校验、权限控制、日志、性能监控、统一处理
可以用 多个拦截器 + 自定义注解 + ThreadLocal + 全局异常处理 来实现。核心思路:
- 登录校验:
preHandle中解析 token,失败返回 401。 - 权限控制:自定义
@RequirePermission,preHandle中读取注解并校验权限。 - 日志:
preHandle记录请求开始,afterCompletion记录结束、状态、异常。 - 性能监控:
preHandle记录开始时间,afterCompletion计算耗时,慢请求告警。 - 统一处理:统一响应格式、统一异常、统一用户上下文、统一清理 ThreadLocal。
3.1 基础准备
用户上下文
用 ThreadLocal 保存当前登录用户,方便 Controller、Service 随时获取。
public class UserContext {
private static final ThreadLocal<LoginUser> HOLDER = new ThreadLocal<>();
public static void set(LoginUser user) {
HOLDER.set(user);
}
public static LoginUser get() {
return HOLDER.get();
}
public static void clear() {
HOLDER.remove();
}
}
LoginUser 简单定义:
public class LoginUser {
private Long id;
private String username;
private Set<String> permissions;
public boolean hasPermission(String permission) {
return permissions != null && permissions.contains(permission);
}
// getter/setter 省略
}
自定义注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginRequired {
}
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequirePermission {
String[] value();
}
统一响应
public class Result<T> {
private int code;
private String msg;
private T data;
public static <T> Result<T> success(T data) {
Result<T> r = new Result<>();
r.code = 0;
r.msg = "success";
r.data = data;
return r;
}
public static <T> Result<T> fail(String msg) {
Result<T> r = new Result<>();
r.code = -1;
r.msg = msg;
return r;
}
// getter/setter 省略
}
写 JSON 的工具方法:
public class ResponseUtils {
private static final ObjectMapper MAPPER = new ObjectMapper();
public static void writeJson(HttpServletResponse response, int status, Result<?> result) throws IOException {
response.setStatus(status);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(MAPPER.writeValueAsString(result));
}
}
3.2 登录校验拦截器
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Resource
private TokenService tokenService;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// 只拦截 Controller 方法
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod hm = (HandlerMethod) handler;
// 方法或类上有 @LoginRequired 才校验
LoginRequired anno = hm.getMethodAnnotation(LoginRequired.class);
if (anno == null) {
anno = hm.getBeanType().getAnnotation(LoginRequired.class);
}
if (anno == null) {
return true;
}
String token = request.getHeader("Authorization");
LoginUser user = tokenService.parseToken(token);
if (user == null) {
ResponseUtils.writeJson(response, 401, Result.fail("未登录"));
return false;
}
// 存入 ThreadLocal 和 request
UserContext.set(user);
request.setAttribute("loginUser", user);
return true;
}
}
如果希望全局都要登录,可以去掉 @LoginRequired 判断,直接校验 token。
3.3 权限控制拦截器
@Component
public class PermissionInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod hm = (HandlerMethod) handler;
RequirePermission anno = hm.getMethodAnnotation(RequirePermission.class);
if (anno == null) {
anno = hm.getBeanType().getAnnotation(RequirePermission.class);
}
if (anno == null) {
return true;
}
LoginUser user = UserContext.get();
if (user == null) {
ResponseUtils.writeJson(response, 401, Result.fail("未登录"));
return false;
}
for (String perm : anno.value()) {
if (!user.hasPermission(perm)) {
ResponseUtils.writeJson(response, 403, Result.fail("无权限"));
return false;
}
}
return true;
}
}
使用示例:
@RestController
@RequestMapping("/user")
public class UserController {
@LoginRequired
@GetMapping("/info")
public Result<UserVO> info() {
LoginUser user = UserContext.get();
return Result.success(userService.getInfo(user.getId()));
}
@LoginRequired
@RequirePermission("user:delete")
@DeleteMapping("/{id}")
public Result<Void> delete(@PathVariable Long id) {
userService.delete(id);
return Result.success(null);
}
}
3.4 日志与性能监控拦截器
日志和性能监控可以放在同一个拦截器里。
@Component
public class LogInterceptor implements HandlerInterceptor {
private static final Logger log = LoggerFactory.getLogger(LogInterceptor.class);
private static final String START_TIME = "startTime";
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
request.setAttribute(START_TIME, System.nanoTime());
String traceId = UUID.randomUUID().toString().replace("-", "");
MDC.put("traceId", traceId);
log.info("请求开始: method={} uri={} ip={}",
request.getMethod(),
request.getRequestURI(),
request.getRemoteAddr());
return true;
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) throws Exception {
Long start = (Long) request.getAttribute(START_TIME);
long costMs = start == null ? 0 : (System.nanoTime() - start) / 1_000_000;
log.info("请求结束: method={} uri={} status={} cost={}ms ex={}",
request.getMethod(),
request.getRequestURI(),
response.getStatus(),
costMs,
ex == null ? "无" : ex.getMessage());
// 慢请求告警
if (costMs > 1000) {
log.warn("慢请求: method={} uri={} cost={}ms",
request.getMethod(),
request.getRequestURI(),
costMs);
}
// 统一清理,防止 ThreadLocal 和 MDC 泄漏
MDC.clear();
UserContext.clear();
}
}
注意:不要在日志里打印密码、token、身份证等敏感信息。请求体在拦截器里也不方便重复读取,建议只记录 URI、参数名、耗时、状态码。
3.5 统一处理
统一异常处理
拦截器不能替代全局异常处理。统一异常建议用 @RestControllerAdvice:
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(BusinessException.class)
public Result<?> handleBusiness(BusinessException e) {
return Result.fail(e.getMessage());
}
@ExceptionHandler(Exception.class)
public Result<?> handleException(Exception e) {
log.error("系统异常", e);
return Result.fail("系统异常");
}
}
统一响应格式
可以在 Controller 中统一返回 Result<T>,也可以用 ResponseBodyAdvice 自动包装。简单场景直接返回 Result 即可。
统一用户上下文
- 登录拦截器:
UserContext.set(user) - 权限拦截器:
UserContext.get() - 日志拦截器:
afterCompletion中UserContext.clear()
这样能保证请求结束后 ThreadLocal 一定被清理,避免线程池复用导致的内存泄漏。
3.6 注册拦截器
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Resource
private LogInterceptor logInterceptor;
@Resource
private LoginInterceptor loginInterceptor;
@Resource
private PermissionInterceptor permissionInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 日志最先注册,preHandle 最先执行,afterCompletion 最后执行
registry.addInterceptor(logInterceptor)
.addPathPatterns("/**");
// 登录校验
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(
"/login",
"/register",
"/error",
"/static/**",
"/swagger-ui/**",
"/v3/api-docs/**"
);
// 权限控制
registry.addInterceptor(permissionInterceptor)
.addPathPatterns("/**");
}
}
顺序很重要:
preHandle按注册顺序执行:日志 → 登录 → 权限。afterCompletion按注册逆序执行:权限 → 登录 → 日志。- 日志拦截器最先注册,所以它的
afterCompletion最后执行,可以记录完整耗时,并统一清理 ThreadLocal。
3.7 注意事项
-
拦截器不能修改请求参数、请求头、请求体
如果要改这些,必须用Filter + HttpServletRequestWrapper。 -
ThreadLocal 必须清理
放在最外层拦截器的afterCompletion里,例如日志拦截器。 -
异步请求
如果 Controller 返回Callable、DeferredResult,afterCompletion可能不执行,需要实现AsyncHandlerInterceptor的afterConcurrentHandlingStarted。 -
拦截器 vs 全局异常
拦截器适合做前置校验、日志、性能监控、上下文设置。统一异常处理交给@RestControllerAdvice。 -
权限控制复杂时用 Spring Security
如果权限模型复杂、需要和 OAuth2、JWT、方法级安全集成,优先考虑 Spring Security。简单场景自定义拦截器足够。 -
排除路径要配全
登录、注册、静态资源、错误页、Swagger 等都要排除,否则可能被登录校验拦住。
四、总结
借助拦截器实现这些功能的典型组合是:
| 功能 | 实现位置 | 关键点 |
|---|---|---|
| 登录校验 | LoginInterceptor.preHandle | 解析 token,失败返回 401 |
| 权限控制 | PermissionInterceptor.preHandle | 读取 @RequirePermission,校验权限 |
| 日志 | LogInterceptor.preHandle/afterCompletion | 记录请求开始、结束、状态、异常 |
| 性能监控 | LogInterceptor.afterCompletion | 计算耗时,慢请求告警 |
| 统一处理 | ThreadLocal + @RestControllerAdvice | 统一用户上下文、统一异常、统一清理 |
推荐注册顺序:日志 → 登录 → 权限。这样 preHandle 从前到后校验,afterCompletion 从后到前清理,日志能覆盖整个请求生命周期。