Skip to content
Go back

Spring MVC 拦截器实战:从 Filter 区别到登录、权限、日志与监控

Published:  at  10:00 AM

一、拦截器与过滤器的区别

一句话总结:
Filter(过滤器)属于 Servlet 规范,由 Servlet 容器管理,作用于所有请求;Interceptor(拦截器)属于 Spring MVC,由 Spring 容器管理,只作用于 Controller 请求,能拿到更多 Spring MVC 上下文信息。

对比维度Filter 过滤器Interceptor 拦截器
所属规范/框架Servlet 规范Spring MVC
管理容器Servlet 容器,如 TomcatSpring 容器
作用范围所有匹配 URL 的请求,包括静态资源、Servlet、JSP只拦截进入 Spring MVC 的 Controller 请求
触发位置在 DispatcherServlet 之前/之后在 DispatcherServlet 内部,Handler 前后
核心接口/方法Filter 的 doFilterHandlerInterceptor 的 preHandle、postHandle、afterCompletion
依赖注入默认不能直接注入 Spring Bean,需桥接可以直接注入 Spring Bean
可访问对象ServletRequest、ServletResponseHttpServletRequest、HttpServletResponse、Handler、ModelAndView
修改请求/响应可通过包装类修改请求体、响应内容一般不直接修改原始请求,更偏向业务拦截
异常处理在 Spring MVC 之外,难以被 Spring 异常处理器处理在 Spring MVC 内,可结合异常处理器
典型用途编码设置、CORS、XSS 过滤、日志、压缩登录校验、权限控制、日志、性能监控、统一处理

执行顺序大致如下:

客户端请求
  -> Filter 前置处理
  -> DispatcherServlet
  -> Interceptor preHandle
  -> Controller
  -> Interceptor postHandle
  -> 视图渲染
  -> Interceptor afterCompletion
  -> Filter 后置处理
  -> 响应客户端

怎么选?

简单记: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 如果非要在拦截器层面“影响参数”

常见有两种间接方案:

  1. 放到 request attribute,Controller 用 @RequestAttribute 取。
  2. 用 ThreadLocal + 自定义 HandlerMethodArgumentResolver 或 AOP。
    这种方式复杂,容易出问题,一般不推荐。

真正要改原始参数、header、body,还是应该用 Filter。

2.5 注意点

总结:


三、借助拦截器实现登录校验、权限控制、日志、性能监控、统一处理

可以用 多个拦截器 + 自定义注解 + 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 即可。

统一用户上下文

这样能保证请求结束后 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("/**");
    }
}

顺序很重要:

3.7 注意事项

  1. 拦截器不能修改请求参数、请求头、请求体
    如果要改这些,必须用 Filter + HttpServletRequestWrapper。

  2. ThreadLocal 必须清理
    放在最外层拦截器的 afterCompletion 里,例如日志拦截器。

  3. 异步请求
    如果 Controller 返回 Callable、DeferredResult,afterCompletion 可能不执行,需要实现 AsyncHandlerInterceptor 的 afterConcurrentHandlingStarted。

  4. 拦截器 vs 全局异常
    拦截器适合做前置校验、日志、性能监控、上下文设置。统一异常处理交给 @RestControllerAdvice。

  5. 权限控制复杂时用 Spring Security
    如果权限模型复杂、需要和 OAuth2、JWT、方法级安全集成,优先考虑 Spring Security。简单场景自定义拦截器足够。

  6. 排除路径要配全
    登录、注册、静态资源、错误页、Swagger 等都要排除,否则可能被登录校验拦住。


四、总结

借助拦截器实现这些功能的典型组合是:

功能实现位置关键点
登录校验LoginInterceptor.preHandle解析 token,失败返回 401
权限控制PermissionInterceptor.preHandle读取 @RequirePermission,校验权限
日志LogInterceptor.preHandle/afterCompletion记录请求开始、结束、状态、异常
性能监控LogInterceptor.afterCompletion计算耗时,慢请求告警
统一处理ThreadLocal + @RestControllerAdvice统一用户上下文、统一异常、统一清理

推荐注册顺序:日志 → 登录 → 权限。这样 preHandle 从前到后校验,afterCompletion 从后到前清理,日志能覆盖整个请求生命周期。


Suggest Changes

Next Post
Spring MVC 自定义参数解析:按类型注入请求上下文