接口日志与加密(Gateway)
之前有一篇讲到在 Springboot 项目中做接口加解密,详见《接口日志与加密(SpringBoot)》一文,今天这篇将介绍在 Gateway 中做接口加解密。Gateway 属于 SpringCloud 家族,使用上 Gateway 时,一般都是微服务项目了,本文将会涉及一下几个要点:
- 服务注册到 Nacos;
- 网关路由分发;
- 网关请求加解密;
- 配置网关动态路由;
- 路由分发负载均衡;
一、安装启动 Nacos
进入 nacos 下载 页面,找到最新的 Assets 中的 zip 文件,例如:nacos-server-2.0.3.zip,下载完成之后解压;

然后可参考 Nacos 文档 中的方式启动。注意,启动脚本中默认是集群模式,你可以在启动命令中显示指定模式,但为了使用方便建议修改默认为单机模式。这里以 Windows 脚本为例,打开刚刚解压好的文件夹,进入到 nacos/bin 目录下,找到 startup.cmd 文件;

接着点右键编辑,打开搜索
set MODE,将set MODE="cluster"修改为set MODE="standalone",然后保存关闭;
双击 startup.cmd ,此时会打开一个 Dos 窗口,等到窗口显示
"Nacos started successfully in stand alone mode. use embedded storage,即表示启动成功;
接着打开浏览器输入:http://127.0.0.1:8848/nacos 即可看到登录页,用户名和密码均为 nacos,点击 提交 后进入主页;

此时已完成 Nacos 安装和启动,接下来进行网关工程搭建。
二、工程创建及路由分发
这里可参考官方文档,如果你搭建 Spring 项目见 Nacos Spring ,如果你搭建 Spring Boot 项目见 Nacos Spring Boot ,如果你搭建 Spring Cloud 项目见 Nacos Spring Cloud 。注意,Gateway 使用的是 WebFlux,是一个非阻塞异步的框架,和 SpringMvc 框架不同,因此不可以在网关工程的 pom.xml 文件中引入
spring-webmvc的依赖。Nacos 有默认保留的命名空间
public,你可以使用这个命名空间。我们这里新建一个命名空间dev_java;
连接 nacos 配置需要写在
bootstrap.yml中,启动项目时bootstrap.yml优先被加载,之后application.yml被加载;
注意,
pom.xml文件中需要引入spring-cloud-starter-bootstrap依赖才可以支持加载bootstrap.yml文件。这里我们给网关服务命名为:example-gateway;
接着在 Nacos 上新增配置文件;

注意 Data ID 命名中有 .yaml 后缀,配置格式选择 YAML ,因为内容不可以为空,这里我们随意写一个符合 yaml 语法的配置,稍后再进行修改,然后点击
发布;
在网关工程的
bootstrap.yml文件中配置静态路由,表示匹配以/demoA开头的请求将被分发到http://127.0.0.1:9501这个服务中,以/demoB开头的请求将被分发到http://127.0.0.1:9502这个服务中;
上述步骤完成后,网关工程搭建完成。接着再搭建两个普通的 SpringBoot 工程:
example-service-a、example-service-b;
然后启动这三个服务;

用 Postman 请求一下,可以看到正常响应;

三、网关请求加解密
先来回顾一下在 SpringBoot 中实现加解密的思路,其本质是实现
RequestWrapper和ResponseWrapper接口,创建两个实例,重写其中获取请求/响应参数的方法以支持加解密,然后在 Filter 中将原ServletRequest和ServletResponse实例替换为支持加解密的实例。
在 Gateway 中的实现加解密的思路与上述类似,创建两个类,一个继承ServerHttpRequestDecorator,一个继承ServerHttpResponseDecorator,重写其中获取请求/响应参数的方法以支持加解密,然后加在GlobalFilter过滤链中。
- 创建支持加密的全局过滤器
这里判断请求头,携带了crypto=true才走加解密逻辑。注意getOrder()方法的返回值,值越小优先级越高,但必须小于 -1 ,修改请求/响应参数才有效。
1 |
|
- 读取并修改 MediaType 为 application/json 的请求体
注意必须创建新的 HttpHeaders,否则可能会出现请求被截断的情况。比如浏览器报错,ST 请求不支持,或 T 请求不支持,这其实是 POST、GET 的前两位被截掉了,导致出错;
注意headers.remove(HttpHeaders.CONTENT_LENGTH);方法,若解密请求体导致请求体内容长度变化,则需要移除请求头中的 CONTENT_LENGTH,否则解密后的请求体参数可能会被截断,导致下游服务无法识别参数。
1 | private Mono<Void> readBodyData(ServerWebExchange exchange, GatewayFilterChain chain, CryptoFilterContext context) { |
- 读取并修改 MediaType 为 application/x-www-form-urlencoded 的请求体
注意必须创建新的 HttpHeaders,否则可能会出现请求被截断的情况。比如浏览器报错,ST 请求不支持,或 T 请求不支持,这其实是 POST、GET 的前两位被截掉了,导致出错;
注意headers.remove(HttpHeaders.CONTENT_LENGTH);方法,若解密请求体导致请求体内容长度变化,则需要移除请求头中的 CONTENT_LENGTH,否则解密后的请求体参数可能会被截断,导致下游服务无法识别参数。
1 | private Mono<Void> readFormData(ServerWebExchange exchange, GatewayFilterChain chain, CryptoFilterContext context) { |
- 支持解密请求体的 RequestDecorator
注意httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");,上文中若请求体解密了,会移除 contentLength ,这里给新的请求头设置 TRANSFER_ENCODING 为 chunked ,表示请求体的长度未知,不能按 contentLength 的长度来读取请求体,需要按数据块来读取,直到读取不到为止。
1 | public class RequestDecorator extends ServerHttpRequestDecorator { |
- 支持加密请求体的 RequestDecorator
注意若响应体太长,会出现响应体被截成多段,然后分多次响应,fluxBody.buffer().map()方法可以解决分段响应的问题;
若下游服务开启了响应数据压缩,则这里必须对获取到的响应字节解压,然后才能转换为字符串加密。
1 | public class ResponseDecorator extends ServerHttpResponseDecorator { |
验证加密效果。上文中
data.setMessage("response from crypto filter");给响应的 message 字段赋值,与上图对比,可以看到响应体已经被修改了。注意请求头中需要有crypto=true才会走加解密。
四、网关动态路由
上文,我们已经实现了网关路由分发,以及对请求体数据解密,对响应体数据加密。但缺点在于路由不够灵活。如果我们需要改变路由,或者增删路由规则,则要修改
bootstrap.yml文件中的 routes 配置 ,为使配置生效,需要重新打包并启动服务。
若将bootstrap.yml文件中的 routes 配置移至 Nacos 中,则原配置文件中的路由可以删除掉,也可以保留,但没有意义。因为服务启动时,连接并读取到 Nacos 上的配置,会覆盖掉项目中的相同配置,此时项目中配置的路由就无意义了。
但这种方式依然不是动态路由方式,无论是从项目中还是 Nacos 上读取到的路由,都在服务启动时加载好了,后续增删改变路由配置,不会生效,除非重启服务。

在 Nacos 上创建
example-gateway-router.json文件,用来存放路由配置;
将网关工程中的 resources/router/rouerList.json 内容拷贝至
example-gateway-router.json文件 中;
在 Nacos 上的
example-gateway.yaml文件中配置如下,以启用动态路由,并指定路由配置文件;
要想实现动态路由效果,需要实现
RouteDefinitionRepository和ApplicationEventPublisherAware这两个接口;
1 |
|
监听 Nacos 上 example-gateway-router.json 文件的变动,一旦监听到内容变化,马上刷新路由。
Tip:其实这种监听 Nacos 上配置文件的更改,是基于 HTTP 长轮询。客户端(java服务) 默认每隔 30 秒发送一个 HTTP 请求到 Nacos 服务端,超时时间为 30 秒。若这 30 秒内有配置内容更改,则请求会立刻响应给客户端,返回更改后的内容;若这 30 秒内无配置内容修改,则请求超时,没有任何内容返回,然后立刻发起下一次请求,周而复始。
这种做法的好处在于,它可以减轻 Nacos 服务端的压力,不长时间占用网络资源。如果换成推送的方式,需要使用 WebSocket 保持长连接,客户端与服务端需要始终保持连接,并且还需要做心跳检测、断线重连,这样会使连接变得不稳定。且配置文件一般很少改动,持续占用连接资源,这又会造成资源浪费。当大量服务使用同一个 Nacos 做为配置中心时,保持 Socket 连接还会增加 Nacos 服务端压力。因此,HTTP 长轮询是较好的方式。
1 |
|
- 重新启动网关服务,在 Postman 中请求 http://127.0.0.1:9501/demoA./get ,可以看到请求正常响应,这里就不放图了,和第三节中最后响应一模一样;
五、路由分发负载均衡
上面我们在配置路由中的 uri 时给的是一个
http://协议地址,它对应了我们下游的一个服务,但生产环境下的服务是集群式的,这样给固定地址就显得不太合适了,因此需要做负载均衡,路由到下游集群服务中随机的一个,或者可以设置权重。
- 在网关
pom.xml文件中增加spring-cloud-starter-loadbalancer依赖,使其支持lb://的负载均衡协议。
此时在 Postman 中请求 http://127.0.0.1:9501/demoB./get ,注意在请求头中加上load-balance=true,这不是必须的,只是因为我在路由配置的 predicates 中增加对此请求头的判断,可以看到请求正常响应;
1 | { |
- 完整的路由配置如下,这里做个简要说明。
predicates 中"name": "Header"配置, 表示请求头中要携带 load-balance 且值为 true 才会匹配到该路由;
filters 中PrefixPath配置,表示给请求统一加前缀,如:前端发起请求的 uri 为/prefixA/get,则分发到下游服务时就变成了/prefix/prefixA/get,/prefix为自定义配置;
filters 中StripPrefix配置,表示截取掉请求的前缀,如:前端发起请求的 uri 为/strip/stripB/get, 则分发到下游服务时就变成了/stripB/get,截取部分以 / 划分,截取长度由 parts 参数指定。
至于为什么 args 下面的参数名是 prefix 和 parts,可以看PrefixPathGatewayFilterFactory、StripPrefixGatewayFilterFactory这两个类中,有一个静态内部类 Config,其中的私有属性即为配置的参数名。
添加/移除请求头,添加/移除请求参数,可以看AddRequestHeaderGatewayFilterFactory,RemoveRequestHeaderGatewayFilterFactory,AddRequestParameterGatewayFilterFactory,RemoveRequestParameterGatewayFilterFactory
1 | { |
上述的 predicates、 filters 写法也有 yaml 文件中的写法,但一般不会在 yaml 文件中配置路由,这里就不介绍那样的写法了。
Tip:本文完整示例代码已上传至 Gitee
