Spring Cloud Gateway-ServerWebExchange核心方法与请求或者响应内容的修改(上)
我们在使用Spring Cloud Gateway的时候 注意到过滤器 包括GatewayFilter、GlobalFilter和过滤器链GatewayFilterChain 都依赖到ServerWebExchange
public interface GlobalFilter { Mono Void filter(ServerWebExchange exchange, GatewayFilterChain chain); public interface GatewayFilter extends ShortcutConfigurable { Mono Void filter(ServerWebExchange exchange, GatewayFilterChain chain); public interface GatewayFilterChain { Mono Void filter(ServerWebExchange exchange); 复制代码
这里的设计和Servlet中的Filter是相似的 当前过滤器可以决定是否执行下一个过滤器的逻辑 由GatewayFilterChain#filter()是否被调用来决定。而ServerWebExchange就相当于当前请求和响应的上下文。ServerWebExchange实例不单存储了Request和Response对象 还提供了一些扩展方法 如果想实现改造请求参数或者响应参数 就必须深入了解ServerWebExchange。
先看ServerWebExchange的注释
Contract for an HTTP request-response interaction. Provides access to the HTTP request and response and also exposes additional server-side processing related
properties and features such as request attributes.
翻译一下大概是
ServerWebExchange是一个HTTP请求-响应交互的契约。提供对HTTP请求和响应的访问 并公开额外的服务器端处理相关属性和特性 如请求属性。
其实 ServerWebExchange命名为服务网络交换器 存放着重要的请求-响应属性、请求实例和响应实例等等 有点像Context的角色。
ServerWebExchange接口的所有方法
public interface ServerWebExchange { // 日志前缀属性的KEY 值为org.springframework.web.server.ServerWebExchange.LOG_ID // 可以理解为 attributes.set( org.springframework.web.server.ServerWebExchange.LOG_ID , 日志前缀的具体值 // 作用是打印日志的时候会拼接这个KEY对饮的前缀值 默认值为 String LOG_ID_ATTRIBUTE ServerWebExchange.class.getName() .LOG_ID String getLogPrefix(); // 获取ServerHttpRequest对象 ServerHttpRequest getRequest(); // 获取ServerHttpResponse对象 ServerHttpResponse getResponse(); // 返回当前exchange的请求属性 返回结果是一个可变的Map Map String, Object getAttributes(); // 根据KEY获取请求属性 Nullable default T T getAttribute(String name) { return (T) getAttributes().get(name); // 根据KEY获取请求属性 做了非空判断 SuppressWarnings( unchecked ) default T T getRequiredAttribute(String name) { T value getAttribute(name); Assert.notNull(value, () - Required attribute name is missing return value; // 根据KEY获取请求属性 需要提供默认值 SuppressWarnings( unchecked ) default T T getAttributeOrDefault(String name, T defaultValue) { return (T) getAttributes().getOrDefault(name, defaultValue); // 返回当前请求的网络会话 Mono WebSession getSession(); // 返回当前请求的认证用户 如果存在的话 T extends Principal Mono T getPrincipal(); // 返回请求的表单数据或者一个空的Map 只有Content-Type为application/x-www-form-urlencoded的时候这个方法才会返回一个非空的Map -- 这个一般是表单数据提交用到 Mono MultiValueMap String, String getFormData(); // 返回multipart请求的part数据或者一个空的Map 只有Content-Type为multipart/form-data的时候这个方法才会返回一个非空的Map -- 这个一般是文件上传用到 Mono MultiValueMap String, Part getMultipartData(); // 返回Spring的上下文 Nullable ApplicationContext getApplicationContext(); // 这几个方法和lastModified属性相关 boolean isNotModified(); boolean checkNotModified(Instant lastModified); boolean checkNotModified(String etag); boolean checkNotModified( Nullable String etag, Instant lastModified); // URL转换 String transformUrl(String url); // URL转换映射 void addUrlTransformer(Function String, String transformer); // 注意这个方法 方法名是 改变 这个是修改ServerWebExchange属性的方法 返回的是一个Builder实例 Builder是ServerWebExchange的内部类 default Builder mutate() { return new DefaultServerWebExchangeBuilder(this); interface Builder { // 覆盖ServerHttpRequest Builder request(Consumer ServerHttpRequest.Builder requestBuilderConsumer); Builder request(ServerHttpRequest request); // 覆盖ServerHttpResponse Builder response(ServerHttpResponse response); // 覆盖当前请求的认证用户 Builder principal(Mono Principal principalMono); // 构建新的ServerWebExchange实例 ServerWebExchange build(); 复制代码
注意到ServerWebExchange#mutate()方法 ServerWebExchange实例可以理解为不可变实例 如果我们想要修改它 需要通过mutate()方法生成一个新的实例 例如这样
public class CustomGlobalFilter implements GlobalFilter { Override public Mono Void filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request exchange.getRequest(); // 这里可以修改ServerHttpRequest实例 ServerHttpRequest newRequest ... ServerHttpResponse response exchange.getResponse(); // 这里可以修改ServerHttpResponse实例 ServerHttpResponse newResponse ... // 构建新的ServerWebExchange实例 ServerWebExchange newExchange exchange.mutate().request(newRequest).response(newResponse).build(); return chain.filter(newExchange); 复制代码
ServerHttpRequest实例是用于承载请求相关的属性和请求体 Spring Cloud Gateway中底层使用Netty处理网络请求 通过追溯源码 可以从ReactorHttpHandlerAdapter中得知ServerWebExchange实例中持有的ServerHttpRequest实例的具体实现是ReactorServerHttpRequest。之所以列出这些实例之间的关系 是因为这样比较容易理清一些隐含的问题 例如
ReactorServerHttpRequest的父类AbstractServerHttpRequest中初始化内部属性headers的时候把请求的HTTP头部封装为只读的实例public AbstractServerHttpRequest(URI uri, Nullable String contextPath, HttpHeaders headers) { this.uri uri; this.path RequestPath.parse(uri, contextPath); this.headers HttpHeaders.readOnlyHttpHeaders(headers); // HttpHeaders类中的readOnlyHttpHeaders方法 其中ReadOnlyHttpHeaders屏蔽了所有修改请求头的方法 直接抛出UnsupportedOperationException public static HttpHeaders readOnlyHttpHeaders(HttpHeaders headers) { Assert.notNull(headers, HttpHeaders must not be null if (headers instanceof ReadOnlyHttpHeaders) { return headers; else { return new ReadOnlyHttpHeaders(headers); 复制代码
所以不能直接从ServerHttpRequest实例中直接获取请求头HttpHeaders实例并且进行修改。
ServerHttpRequest接口如下
public interface HttpMessage { // 获取请求头 目前的实现中返回的是ReadOnlyHttpHeaders实例 只读 HttpHeaders getHeaders(); public interface ReactiveHttpInputMessage extends HttpMessage { // 返回请求体的Flux封装 Flux DataBuffer getBody(); public interface HttpRequest extends HttpMessage { // 返回HTTP请求方法 解析为HttpMethod实例 Nullable default HttpMethod getMethod() { return HttpMethod.resolve(getMethodValue()); // 返回HTTP请求方法 字符串 String getMethodValue(); // 请求的URI URI getURI(); public interface ServerHttpRequest extends HttpRequest, ReactiveHttpInputMessage { // 连接的唯一标识或者用于日志处理标识 String getId(); // 获取请求路径 封装为RequestPath对象 RequestPath getPath(); // 返回查询参数 是只读的MultiValueMap实例 MultiValueMap String, String getQueryParams(); // 返回Cookie集合 是只读的MultiValueMap实例 MultiValueMap String, HttpCookie getCookies(); // 远程服务器地址信息 Nullable default InetSocketAddress getRemoteAddress() { return null; // SSL会话实现的相关信息 Nullable default SslInfo getSslInfo() { return null; // 修改请求的方法 返回一个建造器实例Builder Builder是内部类 default ServerHttpRequest.Builder mutate() { return new DefaultServerHttpRequestBuilder(this); interface Builder { // 覆盖请求方法 Builder method(HttpMethod httpMethod); // 覆盖请求的URI、请求路径或者上下文 这三者相互有制约关系 具体可以参考API注释 Builder uri(URI uri); Builder path(String path); Builder contextPath(String contextPath); // 覆盖请求头 Builder header(String key, String value); Builder headers(Consumer HttpHeaders headersConsumer); // 覆盖SslInfo Builder sslInfo(SslInfo sslInfo); // 构建一个新的ServerHttpRequest实例 ServerHttpRequest build(); 复制代码
如果要修改ServerHttpRequest实例 那么需要这样做
ServerHttpRequest request exchange.getRequest(); ServerHttpRequest newRequest request.mutate().headers( key , value ).path( /myPath ).build(); 复制代码
这里最值得注意的是 ServerHttpRequest或者说HttpMessage接口提供的获取请求头方法HttpHeaders getHeaders();返回结果是一个只读的实例 具体是ReadOnlyHttpHeaders类型 这里提多一次 笔者写这篇博文时候使用的Spring Cloud Gateway版本为Greenwich.SR1。
ServerHttpResponse实例是用于承载响应相关的属性和响应体 Spring Cloud Gateway中底层使用Netty处理网络请求 通过追溯源码 可以从ReactorHttpHandlerAdapter中得知ServerWebExchange实例中持有的ServerHttpResponse实例的具体实现是ReactorServerHttpResponse。之所以列出这些实例之间的关系 是因为这样比较容易理清一些隐含的问题 例如
// ReactorServerHttpResponse的父类 public AbstractServerHttpResponse(DataBufferFactory dataBufferFactory, HttpHeaders headers) { Assert.notNull(dataBufferFactory, DataBufferFactory must not be null Assert.notNull(headers, HttpHeaders must not be null this.dataBufferFactory dataBufferFactory; this.headers headers; this.cookies new LinkedMultiValueMap (); public ReactorServerHttpResponse(HttpServerResponse response, DataBufferFactory bufferFactory) { super(bufferFactory, new HttpHeaders(new NettyHeadersAdapter(response.responseHeaders()))); Assert.notNull(response, HttpServerResponse must not be null this.response response; 复制代码
可知ReactorServerHttpResponse构造函数初始化实例的时候 存放响应Header的是HttpHeaders实例 也就是响应Header是可以直接修改的。
ServerHttpResponse接口如下
public interface HttpMessage { // 获取响应Header 目前的实现中返回的是HttpHeaders实例 可以直接修改 HttpHeaders getHeaders(); public interface ReactiveHttpOutputMessage extends HttpMessage { // 获取DataBufferFactory实例 用于包装或者生成数据缓冲区DataBuffer实例(创建响应体) DataBufferFactory bufferFactory(); // 注册一个动作 在HttpOutputMessage提交之前此动作会进行回调 void beforeCommit(Supplier ? extends Mono Void action); // 判断HttpOutputMessage是否已经提交 boolean isCommitted(); // 写入消息体到HTTP协议层 Mono Void writeWith(Publisher ? extends DataBuffer body); // 写入消息体到HTTP协议层并且刷新缓冲区 Mono Void writeAndFlushWith(Publisher ? extends Publisher ? extends DataBuffer body); // 指明消息处理已经结束 一般在消息处理结束自动调用此方法 多次调用不会产生副作用 Mono Void setComplete(); public interface ServerHttpResponse extends ReactiveHttpOutputMessage { // 设置响应状态码 boolean setStatusCode( Nullable HttpStatus status); // 获取响应状态码 Nullable HttpStatus getStatusCode(); // 获取响应Cookie 封装为MultiValueMap实例 可以修改 MultiValueMap String, ResponseCookie getCookies(); // 添加响应Cookie void addCookie(ResponseCookie cookie); 复制代码
这里可以看到除了响应体比较难修改之外 其他的属性都是可变的。
ServerWebExchangeUtils里面存放了很多静态公有的字符串KEY值(这些字符串KEY的实际值是org.springframework.cloud.gateway.support.ServerWebExchangeUtils. 下面任意的静态公有KEY) 这些字符串KEY值一般是用于ServerWebExchange的属性(Attribute 见上文的ServerWebExchange#getAttributes()方法)的KEY 这些属性值都是有特殊的含义 在使用过滤器的时候如果时机适当可以直接取出来使用 下面逐个分析。
ServerWebExchangeUtils提供的上下文属性用于Spring Cloud Gateway的ServerWebExchange组件处理请求和响应的时候 内部一些重要实例或者标识属性的安全传输和使用 使用它们可能存在一定的风险 因为没有人可以确定在版本升级之后 原有的属性KEY或者VALUE是否会发生改变 如果评估过风险或者规避了风险之后 可以安心使用。例如我们在做请求和响应日志(类似Nginx的Access Log)的时候 可以依赖到GATEWAY_ROUTE_ATTR 因为我们要打印路由的目标信息。举个简单例子
Slf4j Component public class AccessLogFilter implements GlobalFilter { Override public Mono Void filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request exchange.getRequest(); String path request.getPath().pathWithinApplication().value(); HttpMethod method request.getMethod(); // 获取路由的目标URI URI targetUri exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR); InetSocketAddress remoteAddress request.getRemoteAddress(); return chain.filter(exchange.mutate().build()).then(Mono.fromRunnable(() - { ServerHttpResponse response exchange.getResponse(); HttpStatus statusCode response.getStatusCode(); log.info( 请求路径:{},客户端远程IP地址:{},请求方法:{},目标URI:{},响应码:{} , path, remoteAddress, method, targetUri, statusCode); })); 复制代码
修改请求体是一个比较常见的需求。例如我们使用Spring Cloud Gateway实现网关的时候 要实现一个功能 把存放在请求头中的JWT解析后 提取里面的用户ID 然后写入到请求体中。我们简化这个场景 假设我们把userId明文存放在请求头中的accessToken中 请求体是一个JSON结构
{ serialNumber : 请求流水号 , payload : { // ... 这里是有效载荷 存放具体的数据 复制代码
我们需要提取accessToken 也就是userId插入到请求体JSON中如下
{ userId : 用户ID , serialNumber : 请求流水号 , payload : { // ... 这里是有效载荷 存放具体的数据 复制代码
这里为了简化设计 用全局过滤器GlobalFilter实现 实际需要结合具体场景考虑
Slf4j Component public class ModifyRequestBodyGlobalFilter implements GlobalFilter { private final DataBufferFactory dataBufferFactory new NettyDataBufferFactory(ByteBufAllocator.DEFAULT); Autowired private ObjectMapper objectMapper; Override public Mono Void filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request exchange.getRequest(); String accessToken request.getHeaders().getFirst( accessToken if (!StringUtils.hasLength(accessToken)) { throw new IllegalArgumentException( accessToken // 新建一个ServerHttpRequest装饰器,覆盖需要装饰的方法 ServerHttpRequestDecorator decorator new ServerHttpRequestDecorator(request) { Override public Flux DataBuffer getBody() { Flux DataBuffer body super.getBody(); InputStreamHolder holder new InputStreamHolder(); body.subscribe(buffer - holder.inputStream buffer.asInputStream()); if (null ! holder.inputStream) { try { // 解析JSON的节点 JsonNode jsonNode objectMapper.readTree(holder.inputStream); Assert.isTrue(jsonNode instanceof ObjectNode, JSON格式异常 ObjectNode objectNode (ObjectNode) jsonNode; // JSON节点最外层写入新的属性 objectNode.put( userId , accessToken); DataBuffer dataBuffer dataBufferFactory.allocateBuffer(); String json objectNode.toString(); log.info( 最终的JSON数据为:{} , json); dataBuffer.write(json.getBytes(StandardCharsets.UTF_8)); return Flux.just(dataBuffer); } catch (Exception e) { throw new IllegalStateException(e); } else { return super.getBody(); // 使用修改后的ServerHttpRequestDecorator重新生成一个新的ServerWebExchange return chain.filter(exchange.mutate().request(decorator).build()); private class InputStreamHolder { InputStream inputStream; 复制代码
测试一下
// HTTP POST /order/json HTTP/1.1 Host: localhost:9090 Content-Type: application/json accessToken: 10086 Accept: */* Cache-Control: no-cache Host: localhost:9090 accept-encoding: gzip, deflate content-length: 94 Connection: keep-alive cache-control: no-cache serialNumber : 请求流水号 , payload : { name : doge // 日志输出 最终的JSON数据为:{ serialNumber : 请求流水号 , payload :{ name : doge }, userId : 10086 } 复制代码
最重要的是用到了ServerHttpRequest装饰器ServerHttpRequestDecorator 主要覆盖对应获取请求体数据缓冲区的方法即可 至于怎么处理其他逻辑需要自行考虑 这里只是做一个简单的示范。一般的代码逻辑如下
ServerHttpRequest request exchange.getRequest(); ServerHttpRequestDecorator requestDecorator new ServerHttpRequestDecorator(request) { Override public Flux DataBuffer getBody() { // 拿到承载原始请求体的Flux Flux DataBuffer body super.getBody(); // 这里通过自定义方式生成新的承载请求体的Flux Flux DataBuffer newBody ... return newBody; return chain.filter(exchange.mutate().request(requestDecorator).build()); 复制代码
SpringCloud GateWay通过过滤器GatewayFilter修改请求或响应内容 Spring Cloud Gateway在有些场景中需要获取request body内容进行参数校验或参数修改,我们通过在GatewayFilter中获取请求内容来获取和修改请求体,下面我们就基于ServerWebExchange来实现。
spring-boot-route(一)Controller接收参数的几种方式 Controller接收参数的常用方式总体可以分为三类。第一类是Get请求通过拼接url进行传递,第二类是Post请求通过请求体进行传递,第三类是通过请求头部进行参数传递。
SpringBoot统一处理响应信息 在目前流行的前后端分离的软件项目中,JSON格式的数据交互是业界标准,后端开发一般要和前端约定好返回的数据格式,提高接口联调及整个软件项目的开发效率。
Java Spring Boot开发实战系列课程【第15讲】:Spring Boot 2.0 API与Spring REST Docs实战 立即下载
相关文章
- Spring Boot 注解大全,真是太全了!
- Spring Web Flow 入门demo(二)与业务结合 附源代码
- Spring Boot的exit code
- Spring Boot Cache Redis缓存
- spring常用的注入方式有哪些?
- spring支持几种bean的作用域?
- Spring Cloud Gateway 2.1.0 中文官网文档
- 微服务轮子项目(51) -Spring Cloud性能调优
- 硅谷Spring项目组专家教你利用Spring Cloud构建微服务
- Spring AOP
- Spring mvc
- 拜托!面试请不要再问我Spring Cloud底层原理
- Spring IOC源码分析之-刷新前的准备工作
- 漏洞复现----42、Spring Cloud Gateway Actuator API SpEL表达式注入命令执行(CVE-2022-22947)
- Spring RCE(CVE-2022-22965)漏洞复现POC
- Spring Boot 多数据配置更新
- springboot(五):spring data jpa的使用
- Spring MVC框架处理Web请求的基本流程
- OAuth2密码模式已死,最先进的Spring Cloud认证授权方案在这里
- Spring Security 实战干货:如何实现不同的接口不同的安全策略
- Spring Cloud Gateway 请求报文获取 高性能实现方法
- Spring Cloud Edgware Release Notes
- spring cloud学习地址
- Spring_Cloud_Finchley.SR1文档对SpringCloud配置文件优先级的描述
- Spring Cloud :Gateway 路由定义定位器 RouteDefinitionLocator (三)
- Spring Cloud Alibaba 微服务组件 Nacos 注册中心(三)
- Spring中最常用的11个扩展点
- Spring Cloud Netflix Eureka: 多网卡环境下Eureka服务注册IP选择问题
- 菜鸟学习Spring——60s利用JoinPoint获取參数的值和方法名称
- spring Cloud 全局异常捕获
- zookeeper服务发现实战及原理--spring-cloud-zookeeper源码分析
- eclipse中配置maven和创建第一个 Spring Boot Application
- Spring_Boot下Spring_Batch入门实例