在 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 的实现稍微有点复杂,使用了观察者模式。
-
首先是
org.apache.catalina.connector.RequestFacade
实现。 -
RequestFacade
中定义了一个org.apache.catalina.connector.Request request
的字段。protected org.apache.catalina.connector.Request request
-
org.apache.catalina.connector.Request
中又定义了一个org.apache.coyote.Request coyoteRequest
字段。protected org.apache.coyote.Request coyoteRequest;
-
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
同样没问题。