Spring Boot项目中,拦截器经常用于登录校验、Token认证、请求追踪、权限判断以及统一日志记录。实际开发时,一个非常常见的需求是:客户端把用户身份信息、Token、设备标识或自定义参数放在HTTP请求头中,然后由Spring Boot拦截器统一读取。
例如前端发送如下请求:
httpGET /api/user/info HTTP/1.1 Host: localhost:8080 Authorization: Bearer abc123 X-User-Id: 10001 X-Request-Id: req-20261001
拦截器需要从请求对象中获取这些请求头,再根据业务规则进行处理。
一、Spring Boot拦截器接收请求头的基本原理
Spring MVC提供了HandlerInterceptor接口,可以在Controller执行前后介入HTTP请求处理流程。
常用的方法包括:
JavapreHandle() postHandle() afterCompletion()
其中,preHandle()最适合读取请求头并进行校验,因为它发生在Controller方法执行之前。
核心代码非常简单:
Java@Override public boolean preHandle( HttpServletRequest request, HttpServletResponse response, Object handler) { String authorization = request.getHeader("Authorization"); String userId = request.getHeader("X-User-Id"); System.out.println("Authorization: " + authorization); System.out.println("X-User-Id: " + userId); return true; }
这里的HttpServletRequest就是当前HTTP请求对象,通过getHeader()即可获取指定请求头。
需要注意的是,请求头名称通常不区分大小写,但代码中建议按照客户端实际约定保持统一,例如统一使用Authorization、X-User-Id等名称。
二、创建Spring Boot拦截器
首先创建一个实现HandlerInterceptor的类。
Javapackage com.example.demo.interceptor; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.web.servlet.HandlerInterceptor; public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle( HttpServletRequest request, HttpServletResponse response, Object handler) { String token = request.getHeader("Authorization"); if (token == null || token.isBlank()) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return false; } System.out.println("收到Token:" + token); return true; } }
如果使用的是较老版本Spring Boot,例如Spring Boot 2.x,Servlet包通常使用:
Javaimport javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse;
Spring Boot 3.x则切换到了Jakarta EE,因此应该使用:
Javaimport jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse;
这也是升级Spring Boot版本后经常出现的编译错误来源之一。
三、注册拦截器
仅仅创建HandlerInterceptor并不会自动生效,还需要将它注册到Spring MVC。
可以创建一个配置类:
Javapackage com.example.demo.config; import com.example.demo.interceptor.AuthInterceptor; 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 { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new AuthInterceptor()) .addPathPatterns("/api/**"); } }
此时访问:
/api/user/info
就会进入AuthInterceptor。
而访问:
/public/info
则不会经过该拦截器。
四、使用依赖注入方式管理拦截器
实际项目中,拦截器通常还需要调用Service、Redis或其他组件。如果直接使用:
Javanew AuthInterceptor()
就无法方便地使用Spring依赖注入。
更推荐将拦截器声明为Spring Bean:
Java@Component public class AuthInterceptor implements HandlerInterceptor { private final UserService userService; public AuthInterceptor(UserService userService) { this.userService = userService; } @Override public boolean preHandle( HttpServletRequest request, HttpServletResponse response, Object handler) { String userId = request.getHeader("X-User-Id"); if (userId == null || userId.isBlank()) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return false; } return userService.exists(userId); } }
配置类通过构造方法注入:
Java@Configuration public class WebMvcConfig implements WebMvcConfigurer { private final AuthInterceptor authInterceptor; public WebMvcConfig(AuthInterceptor authInterceptor) { this.authInterceptor = authInterceptor; } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor) .addPathPatterns("/api/**"); } }
这种方式更符合Spring Boot的依赖注入设计,也便于后续进行单元测试和业务扩展。
五、一次接收多个请求头
实际接口往往不只有一个请求头。
例如:
httpAuthorization: Bearer abc123 X-User-Id: 10001 X-Device-Id: android-001 X-Request-Id: req-100001
拦截器可以分别获取:
JavaString authorization = request.getHeader("Authorization"); String userId = request.getHeader("X-User-Id"); String deviceId = request.getHeader("X-Device-Id"); String requestId = request.getHeader("X-Request-Id");
也可以封装成统一对象:
Javapublic class RequestHeaderInfo { private String authorization; private String userId; private String deviceId; private String requestId; // getter、setter }
然后在拦截器中进行组装:
JavaRequestHeaderInfo info = new RequestHeaderInfo(); info.setAuthorization(request.getHeader("Authorization")); info.setUserId(request.getHeader("X-User-Id")); info.setDeviceId(request.getHeader("X-Device-Id")); info.setRequestId(request.getHeader("X-Request-Id"));
当请求头数量较多时,这种方式可以减少业务代码中的重复读取。
六、遍历所有请求头进行调试
排查“前端明明传了请求头,但后端获取不到”的问题时,可以先把所有请求头打印出来。
JavaEnumeration<String> headerNames = request.getHeaderNames(); while (headerNames.hasMoreElements()) { String headerName = headerNames.nextElement(); String headerValue = request.getHeader(headerName); System.out.println(headerName + " = " + headerValue); }
完整示例:
Java@Override public boolean preHandle( HttpServletRequest request, HttpServletResponse response, Object handler) { Enumeration<String> headerNames = request.getHeaderNames(); while (headerNames.hasMoreElements()) { String name = headerNames.nextElement(); System.out.println(name + " = " + request.getHeader(name)); } return true; }
这种方式非常适合开发环境排查问题。
不过生产环境不建议直接打印Authorization、Cookie、Session标识等敏感信息,否则可能造成凭据泄露。
七、为什么request.getHeader()获取不到数据
这是开发过程中最常见的问题之一。
例如前端代码发送:
JavaScriptfetch("/api/user/info", { headers: { "X-User-Id": "10001" } });
后端却得到:
JavaString userId = request.getHeader("X-User-ID");
一般来说,请求头名称大小写并不是这里的主要问题。更应该检查请求是否真的经过了目标服务器,以及请求头是否实际出现在HTTP请求中。
可以通过浏览器开发者工具查看:
Network └── 请求 └── Headers └── Request Headers
确认是否存在:
X-User-Id: 10001
如果浏览器网络面板中都没有该字段,那么问题通常发生在前端请求构造、代理配置或网关层,而不是Spring Boot拦截器。
八、跨域请求导致请求头异常
当前端和后端处于不同域名或端口时,例如:
前端:http://localhost:5173 后端:http://localhost:8080
浏览器会受到CORS策略影响。
尤其是自定义请求头:
X-User-Id X-Request-Id Authorization
可能触发预检请求。
Spring Boot需要正确配置跨域策略,例如:
Java@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("http://localhost:5173") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*"); } }
如果项目使用Spring Security,还需要结合Security的CORS配置,否则单独配置MVC跨域可能仍然无法解决问题。
九、OPTIONS预检请求不要误判为业务请求
跨域场景下,浏览器可能首先发送:
httpOPTIONS /api/user/info
而不是直接发送:
httpGET /api/user/info
如果拦截器对所有请求都强制校验Token,就可能导致OPTIONS请求直接返回401。
可以根据实际情况放行预检请求:
Java@Override public boolean preHandle( HttpServletRequest request, HttpServletResponse response, Object handler) { if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { return true; } String token = request.getHeader("Authorization"); if (token == null || token.isBlank()) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return false; } return true; }
不过是否放行OPTIONS需要结合实际安全策略处理,不能简单地把所有请求方法都无条件放开。
十、拦截器路径配置不正确
如果请求头读取代码完全没有执行,需要优先确认拦截器是否真正匹配到了请求。
例如:
Javaregistry.addInterceptor(authInterceptor) .addPathPatterns("/api/**");
那么:
/api/user/info
可以匹配。
但:
/user/info
就不会匹配。
还可以使用:
Java.addPathPatterns("/**") .excludePathPatterns( "/login", "/register", "/error" );
用于全局拦截并排除不需要认证的接口。
例如:
Java@Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor) .addPathPatterns("/**") .excludePathPatterns( "/login", "/register", "/error" ); }
排查时可以先暂时配置/**确认拦截器是否执行,再逐步缩小匹配范围。
十一、请求头经过网关后丢失
微服务项目中,请求通常不是直接进入Spring Boot,而是经过Nginx、API Gateway或其他代理。
请求链路可能是:
浏览器 ↓ Nginx ↓ API Gateway ↓ Spring Boot ↓ Interceptor ↓ Controller
如果浏览器能够看到:
httpX-User-Id: 10001
但Spring Boot中:
Javarequest.getHeader("X-User-Id")
却得到null,就需要检查中间代理。
尤其需要确认网关是否修改、删除或重新构造了请求头。
对于Nginx等反向代理,还需要检查相关请求头转发配置。例如自定义Header涉及代理转发时,应结合实际Nginx配置确认请求是否完整传递。
十二、Authorization请求头的特殊处理
Token认证中经常使用:
httpAuthorization: Bearer eyJ...
后端可以直接获取:
JavaString authorization = request.getHeader("Authorization");
然后进一步解析:
Javaif (authorization == null || !authorization.startsWith("Bearer ")) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return false; } String token = authorization.substring(7);
实际项目中不要只判断Token是否存在,还应该验证Token签名、有效期、发行方以及其他必要的安全信息。
如果项目已经使用Spring Security,通常应优先采用Security提供的认证机制,而不是在普通MVC拦截器中自行实现完整认证体系。
十三、不要把请求头和请求参数混淆
下面三种数据的位置完全不同。
请求参数:
httpGET /user?id=10001
获取方式:
Javarequest.getParameter("id");
请求头:
httpX-User-Id: 10001
获取方式:
Javarequest.getHeader("X-User-Id");
JSON请求体:
JSON{ "userId": 10001 }
通常由Controller通过:
Java@PostMapping("/user") public void save(@RequestBody UserRequest request) { }
获取。
因此,如果前端把参数放在Header中,后端却使用getParameter()获取,自然无法得到结果。
十四、请求头读取后如何传给Controller
有些项目会在拦截器中读取用户信息,然后Controller还需要继续使用。
一种简单方式是使用请求属性:
JavaString userId = request.getHeader("X-User-Id"); request.setAttribute("userId", userId);
Controller中读取:
Java@GetMapping("/user/info") public String userInfo(HttpServletRequest request) { String userId = (String) request.getAttribute("userId"); return userId; }
这种方式适合少量数据传递。
如果需要更规范的用户上下文管理,可以结合ThreadLocal,但必须注意在线程复用环境下及时清理:
Javatry { UserContext.set(userInfo); return true; } finally { UserContext.clear(); }
尤其是在afterCompletion()中进行清理,可以避免线程池复用导致的数据污染。
十五、拦截器中返回false会发生什么
preHandle()返回值决定Controller是否继续执行。
Javareturn true;
表示允许继续执行。
Javareturn false;
表示请求链停止,Controller不会继续执行。
例如:
Javaif (token == null) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return false; }
此时Controller不会被调用。
因此,排查“接口为什么没有进入Controller”时,除了检查路由和Controller映射,也应该检查拦截器是否返回了false。
十六、统一返回JSON错误信息
生产项目通常不建议只设置HTTP状态码,可以返回统一JSON。
例如:
Javaresponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(""" { "code": 401, "message": "未提供有效认证信息" } """); return false;
如果项目已经使用统一异常处理机制,可以进一步结合异常体系处理认证失败,让返回结构保持一致。
十七、请求头中包含中文时的处理
HTTP请求头原则上更适合传输Token、ID、编码后的字符串等信息。
如果业务确实需要传递中文,不建议直接把复杂中文对象塞进Header,而应该采用URL编码、Base64或JSON序列化等明确方案,并确保客户端与服务器使用一致的编码规则。
例如:
X-User-Name: %E5%BC%A0%E4%B8%89
服务端获取后再根据约定进行解码。
不过对于复杂业务数据,更推荐放在请求体中,而不是过度依赖请求头。
十八、拦截器接收请求头的完整示例
一个较完整的认证拦截器可以写成:
Java@Component public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle( HttpServletRequest request, HttpServletResponse response, Object handler) { if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { return true; } String token = request.getHeader("Authorization"); String userId = request.getHeader("X-User-Id"); String requestId = request.getHeader("X-Request-Id"); if (token == null || token.isBlank()) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return false; } System.out.println("userId = " + userId); System.out.println("requestId = " + requestId); return true; } }
配置:
Java@Configuration public class WebMvcConfig implements WebMvcConfigurer { private final AuthInterceptor authInterceptor; public WebMvcConfig(AuthInterceptor authInterceptor) { this.authInterceptor = authInterceptor; } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor) .addPathPatterns("/api/**") .excludePathPatterns( "/api/login", "/api/register" ); } }
这种结构已经能够满足大多数简单的请求头认证需求。
十九、Spring Boot拦截器请求头问题排查清单
遇到request.getHeader()获取不到值时,可以按照请求链路逐层排查。
首先确认客户端实际发送了请求头:
浏览器开发者工具 → Network → Request Headers
其次确认请求是否进入了拦截器,可以临时增加日志:
JavaSystem.out.println("AuthInterceptor executed");
然后确认拦截器路径:
Java.addPathPatterns("/api/**")
是否能够匹配当前URL。
接下来检查读取名称:
Javarequest.getHeader("X-User-Id");
是否与客户端约定一致。
如果浏览器已经发送,但后端仍然没有获取到,则继续检查:
浏览器 → Nginx → 网关 → Spring Boot
中间是否存在Header过滤或重写。
如果只有跨域请求失败,则重点检查:
CORS OPTIONS预检请求 allowedHeaders Spring Security CORS配置
如果拦截器执行了,但Controller没有执行,则重点检查preHandle()是否返回了false。
二十、拦截器与Filter应该如何选择
Spring Boot中处理请求头并不只有拦截器一种方式。
Filter位于Servlet请求处理链更前面:
HTTP请求 ↓ Filter ↓ DispatcherServlet ↓ Interceptor ↓ Controller
如果需求属于整个Web应用级别的请求处理,例如统一日志、字符编码、基础安全处理,可以考虑Filter。
如果需求与Spring MVC的Controller映射、用户认证、接口权限等关系更加紧密,则Interceptor通常更直观。
对于已经采用Spring Security的项目,认证和授权应优先交给Security过滤器链处理,而不是重复在MVC拦截器中实现。
二十一、常见错误总结
request.getHeader()本身通常没有复杂问题,真正容易出错的是请求链路。
比较典型的问题包括:
-
前端实际上没有发送请求头。
-
请求头名称与前后端约定不一致。
-
拦截器没有注册。
-
addPathPatterns()没有匹配当前请求。 -
preHandle()提前返回了false。 -
跨域请求被浏览器拦截。
-
OPTIONS预检请求被认证逻辑拦截。
-
Nginx或网关修改、过滤了请求头。
-
Spring Boot 2.x与3.x的Servlet包发生变化。
-
使用Spring Security后仍然重复实现认证逻辑。
-
把Header、Query Parameter和Request Body混为一谈。
-
在日志中直接打印Token等敏感数据。
因此,排查问题时不要只盯着request.getHeader()这一行代码,而应该沿着“客户端发送—代理转发—Spring拦截器—Controller”的完整链路定位。
对于简单项目,HandlerInterceptor + request.getHeader()即可完成请求头读取和基础认证;对于微服务或安全要求较高的项目,则需要进一步考虑网关转发、CORS、Spring Security、Token验证以及敏感信息保护等问题。