在 HttpServletRequest 中添加自定义请求头

HTTP 请求头一般都是由客户端设置,服务端获取,用以处理业务。但是偶尔也有一些需求,需要服务端对一些请求,添加自定义的请求头。例如:统一为所有请求添加一个名为 X-Requested-Id 的请求头,用于跟踪不同请求。

Servlet 中的请求对象 jakarta.servlet.http.HttpServletRequest 并未提供设置 Header 的方法。

但是,但是通过一些方法,可以实现。本教程将会教你如何在 Spring Boot 应用中添加自定义请求头到 HttpServletRequest

思路

既然可以从 HttpServletRequest 获取到 Header,那它一定是把 Header 存储在了某个地方。我们只要找到 HttpServletRequest 存储 Header 的容器,就可以对其进行添加、编辑、删除操作。

不同 HttpServletRequest 的实现中,对 Header 的存储方式不一定一样。目前,在 Spring Boot 中最流行的 Servlet 实现就是 Tomcat 和 Undertow,所以下文将会针对这两个服务器进行讲解。

示例应用

创建一个 Spring Boot 应用。我们要在 Filter 中为所有请求添加一个 X-Requested-Id 请求头,并在 Controller 中获取并返回。

Controller 如下:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import jakarta.servlet.http.HttpServletRequest;

@RestController
@RequestMapping("/demo")
public class DemoController {
    @GetMapping
    public String getHeaders (HttpServletRequest request) {
        
        // 获取 X-Requested-Id Header 值
        String requestedId = request.getHeader("X-Requested-Id");
        
        return requestedId;
    }
}

在 Spring Boot 应用中,我们可以通过在 Handler 方法中声明 HttpServletRequest 对象,调用其 getHeader() 方法来获取客户端的请求头。

String requestedId  = request.getHeader("X-Requested-Id",);

也可以在 Handler 方法中使用 @RequestHeader 注解参数来声明要获取的请求头:

@RequestHeader(name = "X-Requested-Id", required = false) String requestedId 

Tomcat

Tomcat 对 Servlet 的实现稍微有点复杂,使用了观察者模式。

  1. 首先是 org.apache.catalina.connector.RequestFacade 实现。

  2. RequestFacade 中定义了一个 org.apache.catalina.connector.Request request 的字段。

    protected org.apache.catalina.connector.Request request
    
  3. org.apache.catalina.connector.Request 中又定义了一个 org.apache.coyote.Request coyoteRequest 字段。

    protected org.apache.coyote.Request coyoteRequest;
    
  4. org.apache.coyote.Request 对象有一个 org.apache.tomcat.util.http.MimeHeaders 字段,它便是封装了客户端 Header 的容器。并且提供了获取它的 public 方法。

        // ...
        private final MimeHeaders headers = new MimeHeaders();
        // ...
        public MimeHeaders getMimeHeaders() {
            return headers;
        }
    

那么,我们可以先通过反射从 RequestFacade 中获取到 request 对象,再使用反射从 request 对象中获取到 coyoteRequest 对象。最后调用它的 getMimeHeaders() 方法来获取到封装了请求头的 MimeHeaders 对象。

Filter 实现如下:

import java.io.IOException;
import java.lang.reflect.Field;
import java.util.UUID;

import org.apache.tomcat.util.http.MimeHeaders;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@WebFilter(urlPatterns = "/*") // 拦截所有请求
@Component
public class TomcatRequesteIdHeaderFilter extends HttpFilter{

    /**
    * 
    */
    private static final long serialVersionUID = -287194674100570345L;

    @Override
    protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
            throws IOException, ServletException {

        // 使用反射获取 RequestFacade 中的 org.apache.catalina.connector.Request 对象
        Field connectorRequestField = ReflectionUtils.findField(req.getClass(), "request", org.apache.catalina.connector.Request.class);
        connectorRequestField.setAccessible(true); // 允许反射访问
        org.apache.catalina.connector.Request connectorRequest = (org.apache.catalina.connector.Request) ReflectionUtils.getField(connectorRequestField, req);

        // 使用反射获取 org.apache.catalina.connector.Request 中的 org.apache.coyote.Request 对象
        Field coyoteRequestField = ReflectionUtils.findField(org.apache.catalina.connector.Request.class, "coyoteRequest", org.apache.coyote.Request.class);
        coyoteRequestField.setAccessible(true); // 允许反射访问
        org.apache.coyote.Request coyoteRequest = (org.apache.coyote.Request) ReflectionUtils.getField(coyoteRequestField, connectorRequest);

        // 从 coyoteRequest 对象获取到 MimeHeaders
        MimeHeaders headers = coyoteRequest.getMimeHeaders();

        // 添加新的 Header
        headers.addValue("X-Requested-Id").setString(UUID.randomUUID().toString());

        // 继续执行调用链
        super.doFilter(connectorRequest, res, chain);
    }
}

我们在此 Filter 中为所有请求,都添加了一个 X-Requested-Id 头,它是值是一个 UUID。

测试,启动服务后使用 curl 发起请求:

$ curl localhost:8080/demo
ee3382ba-6567-4261-bb0a-91ddfbdab119

如你所见,Controller 返回了 UUID 值,表示我们成功在 HttpServletRequest 中添加了 X-Requested-Id 头。

Undertow

Spring Boot 默认使用 Tomcat 作为嵌入式服务器,如果你想替换为 Undertow 可以参考 这篇文章

Undertow 的实现就简单太多了,首先是 io.undertow.servlet.spec.HttpServletRequestImpl 实现。它提供了一个 public 方法可以获取到 io.undertow.server.HttpServerExchange 对象。

io.undertow.server.HttpServerExchange 又有一个 public 方法可以获取到 io.undertow.util.HeaderMap 对象,这就是封装了请求头的 Map 对象。

简而言之, Undertow 不需要反射,支持链式调用,甚至只要一行代码就可以获取到 HeaderMap 对象,如下:


import java.io.IOException;
import java.util.UUID;

import org.springframework.stereotype.Component;

import io.undertow.servlet.spec.HttpServletRequestImpl;
import io.undertow.util.HeaderMap;
import io.undertow.util.HttpString;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@WebFilter(urlPatterns = "/*")
@Component
public class UndertowRequesteIdHeaderFilter extends HttpFilter{

    /**
    * 
    */
    private static final long serialVersionUID = -374523395760767552L;

    @Override
    protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        
        // 强制转换为 HttpServletRequestImpl
        HttpServletRequestImpl request = (HttpServletRequestImpl) req;
        // 获取到 HeaderMap
        HeaderMap headerMap = request.getExchange().getRequestHeaders();

        // 添加 Header
        headerMap.add(HttpString.tryFromString("X-Requested-Id"), UUID.randomUUID().toString());
        
        // 继续执行调用链
        super.doFilter(req, res, chain);
    }
}

Tomcat 示例一样,也是添加了一个值是 UUID 的 X-Requested-Id 头到请求中。

启动服务,使用 CURL 进行测试:

$ curl localhost:8080/demo
02ceed83-f33a-4387-8e41-e89e9916e63d

同样没问题。