深入剖析Spring Boot拦截器:从原理到实战

本文深入剖析Spring Boot 3.x拦截器的核心原理与调用链机制,通过一个基于自定义注解@AuthRequired的权限拦截器完整案例,演示从定义、注册到请求处理的全流程,并总结常见踩坑与性能优化建议,适合后端开发与架构设计人员阅读。

概述

Spring Boot 3.2.0(基于Spring Framework 6.x)中的拦截器(Interceptor)是Spring MVC提供的一种轻量级面向切面编程实现,用于在请求进入Controller前后执行通用逻辑,典型场景包括登录认证、权限校验、接口幂等判断、访问日志采集等。本文以权限认证为业务背景,分三个层次展开:先剖析拦截器链的底层执行机制,再基于自定义注解实现一套通用鉴权方案,最后通过curl命令演示完整的请求验证过程,并汇总常见问题与性能优化建议。

核心原理

1. 拦截器所处的位置

在Servlet体系中,一次HTTP请求会依次经过Filter(过滤器)Interceptor(拦截器)。Filter由Servlet容器管理,作用于Servlet层面,无法感知Spring MVC中的处理器方法;而Interceptor由Spring容器管理,位于DispatcherServlet分发阶段,可获取运行时的HandlerMethod对象,从而拿到精确的方法参数和注解元数据,实现业务级别的横切控制。

其核心数据结构是HandlerExecutionChain——DispatcherServlet通过HandlerMapping获取它,该对象内部持有两个关键成员:

  • handler:目标Controller的方法封装(即HandlerMethod)。
  • interceptorList:当前请求需要经过的拦截器列表,按注册顺序排列。

DispatcherServlet中的doDispatch方法会顺序驱动拦截器链中每个拦截器的三个回调方法,形成完整的请求处理闭环。

2. HandlerInterceptor的三阶段回调

Spring提供了HandlerInterceptor接口,全部方法带默认实现,开发者只需覆写关心的环节:

public interface HandlerInterceptor {

    // 请求到达Controller之前执行;返回false则中断请求链
    default boolean preHandle(HttpServletRequest request,
                              HttpServletResponse response,
                              Object handler) throws Exception {
        return true;
    }

    // 处理器方法执行后、视图渲染前执行,可修改ModelAndView
    default void postHandle(HttpServletRequest request,
                            HttpServletResponse response,
                            Object handler,
                            @Nullable ModelAndView modelAndView) throws Exception {
    }

    // 整个请求结束(含视图渲染完毕)后的回调,常用于资源清理
    default void afterCompletion(HttpServletRequest request,
                                 HttpServletResponse response,
                                 Object handler,
                                 @Nullable Exception ex) throws Exception {
    }
}

三个回调的调用时机与顺序规则如下:

  • preHandle:按注册顺序正序执行。任一拦截器返回false,后续拦截器和Controller方法均不再执行。
  • postHandle:按注册顺序逆序执行。仅在Controller方法成功返回后触发。
  • afterCompletion:按注册顺序逆序执行。无论Controller方法是否抛出异常,只要对应的preHandle已返回true,该方法必然被回调。

这个正序进、逆序出的行为,与栈结构完全一致,能够在控制器执行完毕后,按依赖的反方向完成资源释放。

3. 基于注解的拦截器设计原理

传统拦截器常用request.getRequestURI()结合配置表判断接口权限,但这种方式难以应对Spring Cloud微服务或RESTful API的动态路由。更优雅的方案是通过自定义注解声明式地标记权限需求,拦截器借助HandlerMethod反射获取目标方法和目标类的注解元数据,在preHandle阶段完成鉴权。

其核心原理是:请求进入拦截器时,handler参数实际上是HandlerMethod实例,通过handlerMethod.getMethodAnnotation()能够直接拿到方法上标注的注解对象,配合@Target@Retention等元注解即构成一套可配置的鉴权体系。

下面给出完整实现。首先定义权限注解:

package com.example.interceptor.annotation;

import java.lang.annotation.*;

/**
 * 自定义权限认证注解
 * 标注在Controller方法或类上,表示访问该资源需要登录及指定角色
 */
@Documented
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthRequired {

    /**
     * 允许访问的角色列表;为空时仅要求登录,不校验角色
     */
    String[] roles() default {};
}

然后实现核心拦截器:

package com.example.interceptor.config;

import com.example.interceptor.annotation.AuthRequired;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import java.util.Arrays;
import java.util.Map;

/**
 * 权限认证拦截器
 * 核心职责:在请求进入Controller之前完成登录态校验与角色鉴权
 */
@Component
public class AuthInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {

        // 1. 仅拦截控制器方法(静态资源等无HandlerMethod元数据,直接放行)
        if (!(handler instanceof HandlerMethod handlerMethod)) {
            return true;
        }

        // 2. 优先取方法级注解,其次取类级注解
        AuthRequired authRequired = handlerMethod.getMethodAnnotation(AuthRequired.class);
        if (authRequired == null) {
            authRequired = handlerMethod.getBeanType().getAnnotation(AuthRequired.class);
        }

        // 3. 未标注@AuthRequired的接口视为公开接口
        if (authRequired == null) {
            return true;
        }

        // 4. 校验登录态:从请求头中解析Bearer Token
        String token = request.getHeader("Authorization");
        if (token == null || !token.startsWith("Bearer ")) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write("{\"code\":401,\"message\":\"未登录或登录已过期\"}");
            return false;  // 返回false中断请求链
        }

        // 5. 从Token中解析会话数据(生产环境应查Redis或SSO服务)
        Map<String, String> session = resolveSession(token);
        request.setAttribute("userId", session.get("userId"));
        request.setAttribute("userRole", session.get("role"));

        // 6. 角色细粒度鉴权
        String[] roles = authRequired.roles();
        if (roles.length > 0 && !Arrays.asList(roles).contains(session.get("role"))) {
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write("{\"code\":403,\"message\":\"权限不足\"}");
            return false;
        }

        return true;
    }

    /**
     * 模拟从Token中解析会话信息
     * 演示逻辑:token包含"admin"关键字对应管理员角色,否则为普通用户
     */
    private Map<String, String> resolveSession(String token) {
        if (token.contains("admin")) {
            return Map.of("userId", "10001", "role", "ADMIN");
        }
        return Map.of("userId", "10002", "role", "USER");
    }
}

最后通过WebMvcConfigurer注册拦截器,并配置拦截路径:

package com.example.interceptor.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 拦截器注册配置
 * 以声明式方式注册拦截器并定义拦截范围
 */
posted @ 2026-08-15 11:35  艾搜艾学  阅读(4)  评论(0)    收藏  举报