SpringMVC拦截器深度解析:从核心原理到动态权限控制实战

📅 2026/8/12 20:36:32
SpringMVC拦截器深度解析:从核心原理到动态权限控制实战
1. 从“请求”到“响应”为什么我们需要拦截器在任何一个稍微有点规模的Web应用里你肯定遇到过这样的场景用户登录后才能访问个人中心管理员操作前需要校验权限每次请求来了得记录下日志看看谁在访问、花了多长时间。这些事儿如果每个Controller方法里都写一遍校验、记录、权限判断的代码那代码很快就会变得臃肿不堪维护起来简直是噩梦。SpringMVC的拦截器Interceptor就是为了解决这个“横切关注点”问题而生的。你可以把它想象成高速公路上的收费站和检查站。用户的HTTP请求就是一辆车从入口DispatcherServlet接收请求到目的地具体的Controller方法之间会经过好几个这样的“站点”。拦截器就是这些站点它可以在车辆到达Controller之前预处理、Controller处理完毕之后后处理、以及整个请求完成之后完成回调这三个关键节点对请求和响应进行拦截和处理。和过滤器Filter不同过滤器是Servlet规范的一部分工作得更底层它能过滤一切请求包括静态资源。而拦截器是SpringMVC框架层面的它的能力更强因为它能获取到Spring的上下文能知道当前请求具体是要交给哪个HandlerController方法处理也能方便地操作Handler相关的模型数据。简单说过滤器像是小区的门卫管所有进出的人拦截器像是公司前台只处理来公司办事的访客并且清楚访客要找哪个部门。最近看到“qwq拦截器”这类网络热词在开发者社区流传虽然带着点戏谑的意味但也侧面反映了拦截器在实际开发中的高频使用和重要性。它绝不是一个可有可无的装饰品而是构建清晰、健壮、可维护的Web层架构的核心组件之一。接下来我们就深入它的内部看看怎么把它用好、用对。2. 拦截器的核心三阶段预处理、后处理与完成回调理解拦截器的执行时机是正确使用它的前提。一个拦截器的生命周期紧密环绕一次请求-响应过程主要分为三个阶段对应着HandlerInterceptor接口的三个方法。2.1preHandle请求的“安检门”这是拦截器最先执行的方法在HandlerAdapter调用具体的Controller方法之前被触发。public interface HandlerInterceptor { default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { return true; } // ... 其他方法 }它的核心职责是进行请求的预处理和决定是否放行。参数解读request,response标准的Http对象。handler将要执行的处理器对象通常是HandlerMethod封装了目标Controller方法的信息。你可以通过它获取方法上的注解、参数等实现更精细的控制。返回值意义true安检通过请求会继续向下传递执行下一个拦截器的preHandle或最终的Controller方法。false安检不通过请求就此打住。后续的拦截器、Controller方法都不会执行。此时你需要负责通过response对象向客户端返回一个合理的响应如重定向到登录页、返回错误JSON否则浏览器会一直等待。典型应用场景身份认证Authentication检查Session或Token中是否存在登录信息。Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(false); if (session null || session.getAttribute(user) null) { response.sendRedirect(/login); return false; // 未登录中断请求重定向到登录页 } return true; // 已登录放行 }权限校验Authorization在已认证的基础上判断用户是否有权限访问当前资源。这通常需要结合handler参数解析方法上的注解如PreAuthorize但更常见的做法是在拦截器里自定义逻辑。请求日志记录记录请求的URL、IP、时间等用于监控和调试。防重复提交检查请求携带的Token防止表单重复提交。注意preHandle方法按拦截器配置的顺序执行。如果某个拦截器返回false它以及它之前的拦截器的afterCompletion方法依然会被调用如果它们的preHandle都返回了true但它之后的拦截器的preHandle和所有拦截器的postHandle都不会执行。2.2postHandle渲染前的“化妆师”这个方法在Controller方法执行之后但在视图渲染之前被调用。此时Controller方法已经返回了ModelAndView对象或对应的模型数据。default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, Nullable ModelAndView modelAndView) throws Exception { }它的核心职责是对模型数据Model或视图View进行后处理。参数解读前三个参数同preHandle。modelAndViewController处理后的结果。可能是null例如ResponseBody注解的方法直接写回响应体也可能包含了模型数据和视图信息。返回值无。典型应用场景向所有视图添加公共模型数据比如在每个页面上都需要显示当前登录用户信息、网站公告等。你可以在这里向modelAndView中添加这些通用的属性。Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { if (modelAndView ! null) { // 假设从请求属性中获取了当前用户 User currentUser (User) request.getAttribute(currentUser); modelAndView.addObject(currentUser, currentUser); // 添加站点名称 modelAndView.addObject(siteName, 我的技术博客); } }统一修改视图名称根据某些条件动态改变要渲染的视图路径。对模型数据进行二次加工或校验。重要提示postHandle方法按拦截器配置的逆序执行。也就是说最后配置的拦截器的postHandle会最先执行。另外如果preHandle返回false则postHandle不会被调用。2.3afterCompletion收尾的“清洁工”这是拦截器链中最后执行的方法在整个请求完成之后即视图渲染完毕或ResponseBody内容写入完成之后调用。无论请求处理过程中是否发生异常这个方法都会被调用前提是该拦截器的preHandle返回了true。default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Nullable Exception ex) throws Exception { }它的核心职责是进行资源清理和最终处理。参数解读ex请求处理过程中抛出的异常。如果整个过程顺利此参数为null。返回值无。典型应用场景性能监控与统计记录请求的总耗时。通常会在preHandle中记录开始时间存入ThreadLocal或请求属性中在afterCompletion中计算耗时并记录日志或发送到监控系统。Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { long startTime System.currentTimeMillis(); request.setAttribute(startTime, startTime); return true; } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { long startTime (Long) request.getAttribute(startTime); long endTime System.currentTimeMillis(); log.info(请求 [{}] 耗时{} ms, request.getRequestURI(), endTime - startTime); }资源清理清理ThreadLocal变量防止内存泄漏。这是使用ThreadLocal时至关重要的一步。异常日志的补充记录虽然Spring有全局异常处理器ControllerAdvice但在这里可以记录一些更上下文相关的异常信息。注意afterCompletion方法也按拦截器配置的逆序执行。它的执行类似于try-finally块中的finally是进行收尾工作的安全地带。3. 实战如何定义并配置一个拦截器理解了原理我们来动手实现一个完整的拦截器。我们将实现一个日志拦截器记录请求的入参、出参和耗时。3.1 第一步实现HandlerInterceptor接口创建一个类实现HandlerInterceptor接口并重写你需要的方法。通常我们会使用Component注解将其交给Spring管理。package com.example.demo.interceptor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.Map; import java.util.stream.Collectors; Component Slf4j public class ApiLogInterceptor implements HandlerInterceptor { private static final String START_TIME_ATTR startTime; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1. 记录开始时间 request.setAttribute(START_TIME_ATTR, System.currentTimeMillis()); // 2. 记录请求信息注意GET参数在URL中POST参数需要从流中读取这里简单处理 String requestURI request.getRequestURI(); String method request.getMethod(); String queryString request.getQueryString(); String clientIP getClientIp(request); // 获取HandlerMethod信息记录是哪个Controller方法 String handlerInfo Unknown Handler; if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod (HandlerMethod) handler; handlerInfo handlerMethod.getBeanType().getSimpleName() # handlerMethod.getMethod().getName(); } log.info([请求开始] IP: {}, URI: {}, Method: {}, Handler: {}, Query: {}, clientIP, requestURI, method, handlerInfo, queryString); // 记录POST请求的Body注意Body流只能读一次需要额外处理如使用ContentCachingRequestWrapper // 此处为简化示例实际项目需谨慎处理。 return true; // 始终放行 } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 计算耗时 Long startTime (Long) request.getAttribute(START_TIME_ATTR); if (startTime ! null) { long duration System.currentTimeMillis() - startTime; int status response.getStatus(); log.info([请求结束] URI: {}, Status: {}, Duration: {} ms, Exception: {}, request.getRequestURI(), status, duration, ex ! null ? ex.getMessage() : None); } // 清理属性 request.removeAttribute(START_TIME_ATTR); } /** * 获取客户端真实IP考虑了代理情况 */ private String getClientIp(HttpServletRequest request) { String ip request.getHeader(X-Forwarded-For); if (ip null || ip.length() 0 || unknown.equalsIgnoreCase(ip)) { ip request.getHeader(Proxy-Client-IP); } if (ip null || ip.length() 0 || unknown.equalsIgnoreCase(ip)) { ip request.getHeader(WL-Proxy-Client-IP); } if (ip null || ip.length() 0 || unknown.equalsIgnoreCase(ip)) { ip request.getRemoteAddr(); } // 对于通过多个代理的情况第一个IP为客户端真实IP if (ip ! null ip.contains(,)) { ip ip.split(,)[0].trim(); } return ip; } }代码要点解析ThreadLocalvsRequest Attribute这里选择将开始时间存储在HttpServletRequest的属性中而不是ThreadLocal。因为在标准的Servlet容器和SpringMVC中一次请求的生命周期内处理线程可能发生变化例如遇到异步处理ThreadLocal不一定可靠。而Request Attribute是跟随请求对象走的更为安全。HandlerMethod类型判断不是所有handler都是HandlerMethod例如对于静态资源请求可能是ResourceHttpRequestHandler。进行类型判断可以避免ClassCastException。获取客户端IP直接使用request.getRemoteAddr()可能拿到的是代理服务器的IP。通过读取常见的代理转发头如X-Forwarded-For能更准确地获取真实IP。POST Body读取问题在preHandle中直接读取request.getInputStream()会导致Controller无法再次读取参数。如果需要记录POST Body需要使用ContentCachingRequestWrapper对原Request进行包装这是一个常见的“坑”。3.2 第二步配置拦截器并指定拦截路径实现了拦截器还需要告诉SpringMVC在哪些路径下使用它。这需要通过实现WebMvcConfigurer接口或继承WebMvcConfigurationSupport但更推荐前者来配置。package com.example.demo.config; import com.example.demo.interceptor.ApiLogInterceptor; 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 ApiLogInterceptor apiLogInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(apiLogInterceptor) .addPathPatterns(/api/**) // 拦截所有以 /api 开头的请求 .excludePathPatterns(/api/public/**, /api/auth/login); // 排除登录接口和公共接口 // 可以继续添加更多拦截器 // registry.addInterceptor(new AuthInterceptor()).addPathPatterns(/**); } }配置参数详解.addInterceptor(interceptor)注册拦截器实例。.addPathPatterns(String... patterns)指定要拦截的路径模式。支持Ant风格?,*,**和正则表达式。?匹配一个字符。*匹配零个或多个字符不包含路径分隔符/。**匹配零个或多个目录。例如/user/*匹配/user/123但不匹配/user/123/profile/user/**则两者都匹配。.excludePathPatterns(String... patterns)指定要排除的路径模式。常用于登录、验证码、静态资源等不需要拦截的接口。拦截器顺序在addInterceptors方法中注册的顺序就是拦截器preHandle的执行顺序也是postHandle和afterCompletion的逆序。通常把最通用的拦截器如日志放在前面把最具体的拦截器如权限校验放在后面。4. 进阶拦截器中的那些“坑”与最佳实践在实际项目中仅仅实现基础功能是不够的。下面这些我踩过的坑和总结的经验可能比官方文档更有用。4.1 路径匹配的优先级与陷阱路径配置看似简单但理解不透彻容易导致拦截失效或过度拦截。registry.addInterceptor(interceptorA) .addPathPatterns(/admin/**) .excludePathPatterns(/admin/login, /admin/static/**); registry.addInterceptor(interceptorB) .addPathPatterns(/**);问题对于路径/admin/user/list哪个拦截器生效答案是两个都生效。InterceptorB配置了/**会匹配所有路径。执行顺序是A.preHandle-B.preHandle- ... -B.postHandle-A.postHandle。最佳实践精确匹配优先尽量使用精确的路径模式避免滥用/**。/**通常只用于全局性的拦截器如日志、跨域。理清业务边界将不同业务模块的拦截器分开配置并利用excludePathPatterns精细控制。例如API日志拦截器拦截/api/**管理后台权限拦截器拦截/admin/**并排除/admin/login。注意静态资源Spring Boot默认将/static,/public等目录下的资源映射为静态资源。如果你配置了/**的拦截器默认会拦截到对.js,.css,.png等静态资源的请求。通常我们需要排除它们.excludePathPatterns(/static/**, /public/**, /resources/**, /error);/error路径是Spring Boot默认的错误页面路径也建议排除避免拦截器本身出错导致循环重定向。4.2 异步请求Async下的拦截器行为这是拦截器的一个重大变化点。在Spring MVC中当Controller方法返回DeferredResult、Callable或使用Async时请求处理是异步的。preHandle正常执行在异步任务提交前。postHandle不会立即执行它会在异步任务开始后立即被调用此时异步任务可能还没完成模型数据可能不完整。这对于需要依赖异步处理结果的postHandle逻辑来说是致命的。afterCompletion同样会在异步任务开始后、主请求线程结束时立即被调用而不是在异步任务完成后。解决方案使用AsyncHandlerInterceptor它是HandlerInterceptor的子接口增加了两个专门用于异步处理的方法default void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { }当Controller开启异步处理时postHandle和afterCompletion不会被调用取而代之的是afterConcurrentHandlingStarted被调用。你可以在这里进行一些资源清理。如果需要在异步任务真正完成后执行逻辑就不能依赖拦截器了。你需要使用DeferredResult的setResultHandler或Callable的返回后处理。Spring的EventListener监听ServletRequestHandledEvent事件。在异步任务内部如Async注解的方法里手动调用你的后处理逻辑。实战建议对于有异步接口的项目日志记录、性能统计等收尾工作最好转移到异步任务完成后的回调中或者使用更全局的解决方案如Servlet Filter或AOP。4.3 拦截器 vs 过滤器 vs 切片AOP这是面试常考题也是设计时容易混淆的地方。三者的区别和应用场景如下表所示特性过滤器 (Filter)拦截器 (Interceptor)切片 (Aspect)所属规范/框架Servlet 规范 (J2EE)Spring MVC 框架Spring AOP 框架作用范围最广所有请求包括静态资源仅Spring MVC管理的请求Spring容器管理的Bean的方法调用获取Spring上下文不能直接获取需通过SpringBeanAutowiringSupport等能本身就是Spring Bean能基于Spring代理获取Handler信息不能能通过handler参数不能但可以获取方法签名执行时机在DispatcherServlet之前/之后在DispatcherServlet之内Controller方法前后在Bean方法调用前后典型场景字符编码转换、CORS跨域、压缩、安全框架如Shiro的入口过滤MVC特定处理登录校验、权限验证、日志记录、模型数据增强业务层通用处理事务管理、性能监控、缓存、日志非Web层、参数校验如何选择需要处理静态资源、或做最底层的请求/响应包装如读取Body- 用过滤器。需要对Web请求进行预处理、后处理且需要知道是哪个Controller方法- 用拦截器。需要对Service层业务方法进行通用增强如记录执行时间、缓存- 用AOP切片。4.4 拦截器中的异常处理在拦截器方法中抛出异常会怎样在preHandle中抛出异常请求会被中断进入Spring MVC的异常处理流程ControllerAdvice或HandlerExceptionResolver该拦截器的afterCompletion仍然会被调用ex参数不为null但后续拦截器和Controller不会执行。在postHandle或afterCompletion中抛出异常异常会被捕获并记录到日志但可能不会再走全局异常处理流程因为响应可能已经提交了一部分。这可能导致不完整的响应或奇怪的客户端行为。最佳实践拦截器中的代码应尽可能健壮做好异常捕获和处理避免异常向上抛出影响主流程。对于可预见的错误如权限不足应在preHandle中通过response.sendError()或返回错误JSON并return false来优雅处理。Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { try { // ... 校验逻辑 if (!hasPermission) { response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:403,\msg\:\无权限访问\}); return false; } return true; } catch (Exception e) { log.error(拦截器预处理异常, e); // 即使出错也返回一个统一的错误响应而不是抛出异常 response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); return false; } }5. 动态拦截实现更灵活的拦截规则有时静态的路径匹配无法满足复杂需求。比如我们想根据数据库配置动态决定哪些接口需要拦截或者根据用户角色动态排除某些路径。这就需要实现动态拦截。核心思路是自定义一个拦截器在preHandle方法中根据当前请求的URI、用户信息等动态查询规则引擎可以是数据库、配置文件、缓存等来决定是否放行。5.1 基于数据库配置的动态拦截器示例假设我们有一张表interceptor_rule存储了需要拦截的URL模式。定义规则服务Service public class DynamicInterceptorRuleService { Autowired private RuleRepository ruleRepository; // 假设的DAO /** * 判断当前请求是否需要被拦截 * param requestUri 请求URI * param userRole 当前用户角色可从Session获取 * return true 需要拦截 false 放行 */ public boolean shouldIntercept(String requestUri, String userRole) { ListInterceptorRule rules ruleRepository.findActiveRules(); for (InterceptorRule rule : rules) { // 实现简单的Ant路径匹配生产环境可用Spring的AntPathMatcher if (pathMatches(rule.getUrlPattern(), requestUri)) { // 检查规则是否对当前用户角色生效 return !rule.getExcludedRoles().contains(userRole); } } return false; // 没有匹配规则默认放行 } private boolean pathMatches(String pattern, String path) { // 简化实现实际应使用org.springframework.util.AntPathMatcher // 这里仅作示例 AntPathMatcher matcher new AntPathMatcher(); return matcher.match(pattern, path); } }实现动态拦截器Component public class DynamicAuthInterceptor implements HandlerInterceptor { Autowired private DynamicInterceptorRuleService ruleService; Autowired private UserService userService; // 获取当前用户 Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String requestUri request.getRequestURI(); // 获取当前用户角色示例 User currentUser userService.getCurrentUser(request); String role (currentUser ! null) ? currentUser.getRole() : ANONYMOUS; if (ruleService.shouldIntercept(requestUri, role)) { // 执行具体的拦截逻辑如权限校验 if (!checkPermission(currentUser, requestUri)) { response.sendError(403, Forbidden); return false; } } // 不需要拦截或校验通过放行 return true; } private boolean checkPermission(User user, String uri) { // 具体的权限校验逻辑 return true; // 示例 } }配置拦截器这个动态拦截器通常配置为拦截所有路径/**因为它内部自己做了判断。Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(dynamicAuthInterceptor) .addPathPatterns(/**) .excludePathPatterns(/login, /static/**); // 仍然可以排除绝对不需要判断的路径 }这种方式的优点是灵活性极高规则变更无需重启应用。缺点是每次请求都需要查询规则可能带来性能开销需要通过缓存规则来优化。5.2 结合注解实现方法级细粒度控制另一种更常见的动态控制方式是结合自定义注解和拦截器。这在权限控制场景下非常流行。定义权限注解Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RequirePermission { String value(); // 权限标识符如 user:add }在Controller方法上使用RestController RequestMapping(/api/user) public class UserController { PostMapping RequirePermission(user:add) public Result addUser(RequestBody User user) { // ... return Result.success(); } }在拦截器中解析注解Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod (HandlerMethod) handler; // 查找方法上的注解 RequirePermission annotation handlerMethod.getMethodAnnotation(RequirePermission.class); if (annotation ! null) { String requiredPermission annotation.value(); // 获取当前用户权限列表从Session、Token等 SetString userPermissions getCurrentUserPermissions(request); if (!userPermissions.contains(requiredPermission)) { response.sendError(403, 缺少权限: requiredPermission); return false; } } } return true; }这种方式将拦截规则声明在了代码层面清晰直观且能与Spring Security等安全框架的理念结合。它比纯路径匹配更精确直接关联到业务方法。6. 性能考量与拦截器链优化当项目中有十几个甚至更多拦截器时每个请求都要走过这一长串链条性能影响不容忽视。虽然单个拦截器的开销很小但积少成多。优化策略精简拦截器逻辑preHandle中的代码应尽可能轻量避免耗时的IO操作如频繁的数据库查询。对于需要查库的权限校验应使用缓存如Redis存储用户-权限关系。合理安排拦截器顺序将最可能快速失败如格式校验、或最轻量如日志记录的拦截器放在前面。将重量级的、需要复杂计算的拦截器放在后面。这样无效请求能在前期就被快速拒绝减少后续开销。利用excludePathPatterns这是最有效的优化手段之一。确保静态资源、健康检查端点如/actuator/health、内网API等明确不需要拦截的路径被精确排除。避免在拦截器中做重复工作如果多个拦截器都需要当前用户信息可以在第一个拦截器中查询并放入request属性中后续拦截器直接使用避免重复查询。// 在第一个拦截器如AuthInterceptor中 User user userService.getUserByToken(token); request.setAttribute(CURRENT_USER, user); // 在后续拦截器中 User user (User) request.getAttribute(CURRENT_USER);对于异步请求慎用拦截器如前所述异步请求下postHandle和afterCompletion的行为不符合预期。对于异步接口考虑将逻辑移到异步任务内部或使用事件监听避免无用的拦截器执行。拦截器是SpringMVC框架赋予我们的一把利器用好了能让Web层代码整洁如新逻辑清晰。它的核心价值在于对“横切关注点”的封装。但在享受便利的同时必须深刻理解它的生命周期、执行顺序、在异步场景下的特殊表现以及如何避免常见的性能陷阱。从简单的日志记录到复杂的动态权限网关拦截器的设计思想始终贯穿其中。掌握它你就掌握了构建高可维护性Web应用的一项重要技能。