Spring Boot中拦截器接收请求头数据的配置与问题排查

8 次阅读

Spring Boot项目中,拦截器经常用于登录校验、Token认证、请求追踪、权限判断以及统一日志记录。实际开发时,一个非常常见的需求是:客户端把用户身份信息、Token、设备标识或自定义参数放在HTTP请求头中,然后由Spring Boot拦截器统一读取。

例如前端发送如下请求:

http
GET /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请求处理流程。

常用的方法包括:

Java
preHandle()
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的类。

Java
package 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包通常使用:

Java
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

Spring Boot 3.x则切换到了Jakarta EE,因此应该使用:

Java
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

这也是升级Spring Boot版本后经常出现的编译错误来源之一。

三、注册拦截器

仅仅创建HandlerInterceptor并不会自动生效,还需要将它注册到Spring MVC。

可以创建一个配置类:

Java
package 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或其他组件。如果直接使用:

Java
new 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的依赖注入设计,也便于后续进行单元测试和业务扩展。

五、一次接收多个请求头

实际接口往往不只有一个请求头。

例如:

http
Authorization: Bearer abc123
X-User-Id: 10001
X-Device-Id: android-001
X-Request-Id: req-100001

拦截器可以分别获取:

Java
String 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");

也可以封装成统一对象:

Java
public class RequestHeaderInfo {

    private String authorization;
    private String userId;
    private String deviceId;
    private String requestId;

    // getter、setter
}

然后在拦截器中进行组装:

Java
RequestHeaderInfo 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"));

当请求头数量较多时,这种方式可以减少业务代码中的重复读取。

六、遍历所有请求头进行调试

排查“前端明明传了请求头,但后端获取不到”的问题时,可以先把所有请求头打印出来。

Java
Enumeration<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()获取不到数据

这是开发过程中最常见的问题之一。

例如前端代码发送:

JavaScript
fetch("/api/user/info", {
    headers: {
        "X-User-Id": "10001"
    }
});

后端却得到:

Java
String 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预检请求不要误判为业务请求

跨域场景下,浏览器可能首先发送:

http
OPTIONS /api/user/info

而不是直接发送:

http
GET /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需要结合实际安全策略处理,不能简单地把所有请求方法都无条件放开。

十、拦截器路径配置不正确

如果请求头读取代码完全没有执行,需要优先确认拦截器是否真正匹配到了请求。

例如:

Java
registry.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

如果浏览器能够看到:

http
X-User-Id: 10001

但Spring Boot中:

Java
request.getHeader("X-User-Id")

却得到null,就需要检查中间代理。

尤其需要确认网关是否修改、删除或重新构造了请求头。

对于Nginx等反向代理,还需要检查相关请求头转发配置。例如自定义Header涉及代理转发时,应结合实际Nginx配置确认请求是否完整传递。

十二、Authorization请求头的特殊处理

Token认证中经常使用:

http
Authorization: Bearer eyJ...

后端可以直接获取:

Java
String authorization = request.getHeader("Authorization");

然后进一步解析:

Java
if (authorization == null ||
        !authorization.startsWith("Bearer ")) {

    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
    return false;
}

String token = authorization.substring(7);

实际项目中不要只判断Token是否存在,还应该验证Token签名、有效期、发行方以及其他必要的安全信息。

如果项目已经使用Spring Security,通常应优先采用Security提供的认证机制,而不是在普通MVC拦截器中自行实现完整认证体系。

十三、不要把请求头和请求参数混淆

下面三种数据的位置完全不同。

请求参数:

http
GET /user?id=10001

获取方式:

Java
request.getParameter("id");

请求头:

http
X-User-Id: 10001

获取方式:

Java
request.getHeader("X-User-Id");

JSON请求体:

JSON
{
    "userId": 10001
}

通常由Controller通过:

Java
@PostMapping("/user")
public void save(@RequestBody UserRequest request) {
}

获取。

因此,如果前端把参数放在Header中,后端却使用getParameter()获取,自然无法得到结果。

十四、请求头读取后如何传给Controller

有些项目会在拦截器中读取用户信息,然后Controller还需要继续使用。

一种简单方式是使用请求属性:

Java
String 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,但必须注意在线程复用环境下及时清理:

Java
try {
    UserContext.set(userInfo);
    return true;
} finally {
    UserContext.clear();
}

尤其是在afterCompletion()中进行清理,可以避免线程池复用导致的数据污染。

十五、拦截器中返回false会发生什么

preHandle()返回值决定Controller是否继续执行。

Java
return true;

表示允许继续执行。

Java
return false;

表示请求链停止,Controller不会继续执行。

例如:

Java
if (token == null) {
    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
    return false;
}

此时Controller不会被调用。

因此,排查“接口为什么没有进入Controller”时,除了检查路由和Controller映射,也应该检查拦截器是否返回了false。

十六、统一返回JSON错误信息

生产项目通常不建议只设置HTTP状态码,可以返回统一JSON。

例如:

Java
response.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

其次确认请求是否进入了拦截器,可以临时增加日志:

Java
System.out.println("AuthInterceptor executed");

然后确认拦截器路径:

Java
.addPathPatterns("/api/**")

是否能够匹配当前URL。

接下来检查读取名称:

Java
request.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()本身通常没有复杂问题,真正容易出错的是请求链路。

比较典型的问题包括:

  1. 前端实际上没有发送请求头。

  2. 请求头名称与前后端约定不一致。

  3. 拦截器没有注册。

  4. addPathPatterns()没有匹配当前请求。

  5. preHandle()提前返回了false。

  6. 跨域请求被浏览器拦截。

  7. OPTIONS预检请求被认证逻辑拦截。

  8. Nginx或网关修改、过滤了请求头。

  9. Spring Boot 2.x与3.x的Servlet包发生变化。

  10. 使用Spring Security后仍然重复实现认证逻辑。

  11. 把Header、Query Parameter和Request Body混为一谈。

  12. 在日志中直接打印Token等敏感数据。

因此,排查问题时不要只盯着request.getHeader()这一行代码,而应该沿着“客户端发送—代理转发—Spring拦截器—Controller”的完整链路定位。

对于简单项目,HandlerInterceptor + request.getHeader()即可完成请求头读取和基础认证;对于微服务或安全要求较高的项目,则需要进一步考虑网关转发、CORS、Spring Security、Token验证以及敏感信息保护等问题。