Spring MVC拦截器的快速应用

基础

拦截器的生效位置

为什么 preHandle 是主流?(核心原因)

以后面的案例的「token 校验」为例,就能明白为什么大家优先用 preHandle

1. 「提前拦截」= 减少无效消耗(最核心)

如果 token 无效 / 不存在,preHandle 返回 false,请求会直接被拦截,不会进入控制器、不会调用 Service/DAO、不会操作数据库------ 相当于 "把坏人挡在门外",而不是 "让坏人进屋里再赶出去"。

  • 比如:用户拿失效的 token 访问 /api/user/infopreHandle 直接返回 401,控制器的 getUserInfo() 方法根本不会执行,节省了服务器资源;
  • 如果用 postHandle:控制器方法已经执行完(比如查了数据库、查了用户信息),再发现 token 无效,此时只能修改响应,但数据库查询、业务逻辑执行的资源已经消耗了,完全没必要。
2. preHandle 是唯一能「阻断请求」的方法

postHandleafterCompletion 都是 void 返回值,哪怕发现 token 无效,也无法阻止请求流程(因为控制器已经执行了),只能做一些 "补救"(比如修改响应体),但核心业务逻辑已经执行,失去了拦截的意义。而 preHandleboolean 返回值是 "生死开关",这是实现「权限控制、token 校验、请求限流」等核心拦截逻辑的关键。

3. 符合 "拦截器" 的设计初衷

拦截器(Interceptor)的核心目的是「在请求处理前做校验 / 过滤,避免无效请求消耗资源」,而不是 "请求处理后做善后"------preHandle 完美契合这个初衷,而另外两个方法只是补充。

postHandleafterCompletion 什么时候用?

不是说这两个方法没用,而是它们的场景很特定,只适合「后置处理」,比如:

1. postHandle 适用场景
  • 统一修改响应数据(比如给所有接口返回值加统一的包装格式:{code:200, data:..., msg:"success"});
  • 修改视图渲染参数(比如给所有页面添加全局的用户信息);
  • 记录控制器执行耗时(结合 preHandle 记录的开始时间,计算耗时)。
2. afterCompletion 适用场景
  • 资源清理(比如关闭请求中打开的流、释放临时文件);
  • 全局异常记录(通过方法参数 Exception ex 捕获整个请求流程的异常,记录日志);
  • 最终的请求日志汇总(比如记录请求的最终状态、响应码)。

1. 拦截器的使用步骤

1.1 创建拦截器

  • 第1步:工程目录下创建拦截器 base.interceptors.MyInterCeptor(命名随意)

    public class MyInterceptor implements HandlerInterceptor {
    /**
    * 1.执行handler之前调用的拦截方法;
    * 2.可以进行编码格式设置、登录保护、权限处理等。
    * @param request 请求对象
    * @param response 响应对象
    * @param handler handler是我们要调用的方法对象
    * @return true放行,false拦截
    * @throws Exception
    */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    System.out.println("1.preHandle执行");
    return true;
    }

    复制代码
      /**
       * 1.当handler方法执行完成后,触发的方法,没有拦截机制了。
       * 2.此方法只有 preHandle中 return true时才会触发
       * 3.对结果进行处理,比如敏感词检查。
       * @param request 请求
       * @param response 响应
       * @param handler handler方法
       * @param modelAndView 返回的视图和模型数据
       * @throws Exception
       */
      @Override
      public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
          System.out.println("2.postHandle执行");
      }
    
      /**
       * 整体处理完毕,调用此方法
       * @param request 请求
       * @param response 响应
       * @param handler handler方法
       * @param ex 异常对象
       * @throws Exception
       */
      @Override
      public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
          System.out.println("3.afterCompletion执行");
      }

    }

  • 第2步:注册拦截器,工程目录下创建 config.MvcConfig(命名随意)

    • 第1:实现WebMvcConfigurer,它是SpringMVC的标准化配置类,提供拦截器的注入

    • 第2:重写注入方法:addInterceptors

    • 第3:注册拦截器:registry.addInterceptor()

      @Configuration
      public class InterceptorConfig implements WebMvcConfigurer {
      @Override
      public void addInterceptors(InterceptorRegistry registry) {
      //1.拦截所有请求
      registry.addInterceptor(new MyInterceptor())
      .addPathPatterns("/**");
      }
      }

1.2 注册拦截器

SpringMVC 的配置类中,通过实现 WebMvcConfigurer 接口 并重写 addInterceptors 方法进行拦截器的注册。

  • 第1步:创建配置类,并实现 WebMvcConfigurer

  • 第2步:注册拦截器,重写 addInterceptors

    @Configuration
    public class InterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    //1.拦截所有请求
    registry.addInterceptor(new MyInterceptor()).addPathPatterns("/v1/**");
    }
    }

1.2.1 拦截器配置方案
1.拦截全部请求
复制代码
registry.addInterceptor(new MyInterceptor()) .addPathPatterns("/**");
2.拦截指定请求

示例: 拦截所有 /v1/user/ 开头的请求,使用 /v1/user/**

复制代码
registry.addInterceptor(new MyInterceptor()) .addPathPatterns("/v1/user/**");
3.排除指定请求

示例: 拦截所有的 /v1/user/ 开头的请求, 但是不包含登录请求 /v1/user/login

复制代码
registry
.addInterceptor(new MyInterceptor()) 
.addPathPatterns("/v1/user/**")
.excludePathPatterns("/v1/user/login");

实战

2. 限定访问时间拦截器

要求: 项目工程中,限定所有请求只能在每天的: 06:00 - 22:59 之间进行访问;

  • 第1步:创建拦截器的类并实现拦截器功能;

    public class TimeAccessInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request,
    HttpServletResponse response, Object handler) throws Exception {
    System.out.println("进入访问时间限定拦截器");
    LocalTime now = LocalTime.now();//JDK中的时间对象
    int hour = now.getHour();//获取当前时间对应小时
    if (hour<6 || hour>22){
    throw new RuntimeException("请在06:00~22:00之间进行访问");
    }
    return true;
    }
    }

  • 第2步:注册拦截器并添加拦截策略;

    @Configuration
    public class InterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new TimeAccessInterceptor())
    .addPathPatterns("/v1/user/**");
    }
    }

  • 第3步:重启工程测

3. 权限校验拦截器

由后端进⾏⽤⼾⾝份验证,若不存在token或失效,则失败返回

采⽤拦截器来完成校验token的合法性

第1步:创建拦截器的类并实现拦截器功能;

复制代码
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response, Object handler) throws Exception {
        //从 header 中获取 token
        String jwtToken = request.getHeader("user_token");
        log.info("获取路径:{}", request.getRequestURI());
        log.info("从header中获取token:{}", jwtToken);
        //验证用户 token
        Claims claims = JWTUtil.parseJWT(jwtToken);
        if (null == claims){
            response.setStatus(401);
            return false;
        }
        log.info("令牌验证通过, 放行");
        return true;
    }
}

第2步:注册拦截器并添加拦截策略;

复制代码
@Configuration
public class AppConfig implements WebMvcConfigurer {

    @Autowired
    private LoginInterceptor loginInterceptor;

    private final List<String> excludes = Arrays.asList(
            "/**/*.html",
            "/css/**",
            "/js/**",
            "/pic/**",
            "/*.jpg",
            "/favicon.ico",
            "/**/login",
            "/register",
            "/verification-code/send",
            "/winning-records/show"
    );

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns(excludes);
    }
}

4. 限流功能拦截器[扩展]

因为系统处理能力有限,所以现在要对接口访问进行限流,实现方案是编写一个拦截器,对请求进行拦截和计算,要求: 单个IP限制在每分钟访问网站不能超过3次,超过3次则认为该客户端存在攻击风险,具体代码如下:

  • 前提: 获取客户端的IP地址 request.getRemoteAddr();

  • 计数的HashMap:

    {"192.168.1.11": 2, "192.168.1.12": 1, "192.168.1.13": 3}

  • 记录时间的HashMap:

    {"192.168.1.11": "2025-08-11 10:00:00", "192.168.1.12": "2025-08-11 11:00:00", "192.168.1.13": "2025-08-11 09:00:00"}

  • 情况1: 该IP第一次访问网站;

  • 情况2: 该IP之前访问过,但是距离本次访问的时间已经超过了60秒;

  • 情况3: 该IP在1分钟之内访问过;

  • 获取到客户端的IP地址: request.getRemoteAddr();

  • 存储计数的HashMap: {"192.168.1.11": 2, "192.168.1.12": 3, "192.168.1.13": 1}

  • 存储最后一次访问时间的HashMap: {"192.168.1.11": "2025-07-11 00:00:00", "192.168.1.12":"2025-07-10:00:00:00", "192.168.1.13": "2020-01-01 00:00:00"}

  • 第1步: 编写拦截器

    复制代码
    package cn.tedu._03vehicle.base.interceptors;
    
    import org.springframework.web.servlet.HandlerInterceptor;
    
    import jakarta.servlet.http.HttpServletRequest;
    import jakarta.servlet.http.HttpServletResponse;
    import java.time.LocalDateTime;
    import java.util.concurrent.ConcurrentHashMap;
    
    public class RateLimitInterceptor implements HandlerInterceptor {
    
        private static final int MAX_REQUESTS_PER_MINUTE = 3;
        private static final long WINDOW_SIZE_SECONDS = 60;
    
        // {"192.168.1.11": 1, "192.168.1.12": 1, "192.168.1.13": 2}
        private final ConcurrentHashMap<String, Integer> clientCounts = new ConcurrentHashMap<>();
        // {"192.168.1.11": "2024-01-01 14:00:00", "192.168.1.12": "2024-01-01 14:30:30", ...}
        private final ConcurrentHashMap<String, LocalDateTime> lastResetTimes = new ConcurrentHashMap<>();
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            //获取客户端的IP地址
            String clientId = request.getRemoteAddr();
    
            // 尝试获取上次重置时间
            LocalDateTime lastResetTime = lastResetTimes.get(clientId);
            // 如果上次重置时间不存在或已过期,则重置计数器和时间
            if (lastResetTime == null || LocalDateTime.now().isAfter(lastResetTime.plusSeconds(WINDOW_SIZE_SECONDS))) {
                clientCounts.put(clientId, 0);
                lastResetTimes.put(clientId, LocalDateTime.now());
            }
    
            // 增加请求次数
            Integer count = clientCounts.get(clientId);
            count = count + 1;
            clientCounts.put(clientId, count);
    
            // 检查是否超过限制
            if (count > MAX_REQUESTS_PER_MINUTE) {
                throw new RuntimeException("访问太频繁了");
            }
    
            return true;
        }
    }
  • 第2步: 配置拦截器

    @Configuration
    public class InterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new RateLimitInterceptor())
    .addPathPatterns("/v1/user/**");
    }
    }

  • 第3步: 重启工程测试

相关推荐
wsfk12341 小时前
总结:Spring Boot 之spring.factories
java·spring boot·spring
兮动人2 小时前
Druid连接池心跳与空闲连接回收配置指南
java·druid
callJJ2 小时前
Java 源码阅读方法论:从入门到实战
java·开发语言·人工智能·spring·ioc·源码阅读
BD_Marathon2 小时前
原型模式——克隆羊
java·开发语言·原型模式
独自破碎E2 小时前
【滑动窗口】BISHI47 交换到最大
java·开发语言·javascript
java1234_小锋2 小时前
Java高频面试题:Zookeeper对节点的watch监听通知是永久的吗?
java·zookeeper·java-zookeeper
zjttsh2 小时前
怎么下载安装yarn
java
invicinble2 小时前
centos7
java
冰暮流星2 小时前
sql语言之having语句使用
java·数据库·sql