51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

在 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}

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 {#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

同样没问题。

赞(0)
未经允许不得转载:工具盒子 » 在 HttpServletRequest 中添加自定义请求头