在我看来,在某些场景下,网关就像是一个公共方法,把项目中的都要用到的一些功能提出来,抽象成一个服务。比如,我们可以在业务网关上做日志收集、Token校验等等,当然这么理解很狭隘,因为网关的能力远不止如此,但是不妨碍我们更好地理解它。下面的例子演示了,如何在网关校验Token,并提取用户信息放到Header中传给下游业务系统。

1. 生成Token

用户登录成功以后,生成token,此后的所有请求都带着token。网关负责校验token,并将用户信息放入请求Header,以便下游系统可以方便地获取用户信息。

java中网关的概念 java 网关_java

java中网关的概念 java 网关_ci_02

为了方便演示,本例中涉及三个工程

公共项目:cjs-commons-jwt

认证服务:cjs-auth-service

网关服务:cjs-gateway-example

1.1. Token生成与校验工具类

因为生成token在认证服务中,token校验在网关服务中,因此,我把这一部分写在了公共项目cjs-commons-jwt中

pom.xml

java中网关的概念 java 网关_java中网关的概念_03

java中网关的概念 java 网关_ci_04

JWTUtil.java

java中网关的概念 java 网关_spring_05

java中网关的概念 java 网关_java实现gateway_06

java中网关的概念 java 网关_java实现gateway_07

ResponseCodeEnum.java

java中网关的概念 java 网关_java_08

java中网关的概念 java 网关_java_09

ResponseResult.java

java中网关的概念 java 网关_java中网关的概念_10

java中网关的概念 java 网关_ci_11

1.2. 生成token

这一部分在cjs-auth-service中

pom.xml

java中网关的概念 java 网关_java实现gateway_12

java中网关的概念 java 网关_java_13

java中网关的概念 java 网关_java实现gateway_14

LoginController.java

java中网关的概念 java 网关_ci_15

java中网关的概念 java 网关_java实现gateway_16

java中网关的概念 java 网关_ci_17

java中网关的概念 java 网关_java中网关的概念_18

HelloController.java

java中网关的概念 java 网关_java中网关的概念_19

application.yml

java中网关的概念 java 网关_java实现gateway_20

2. 校验Token

GatewayFilter和GlobalFilter都可以,这里用GlobalFilter

pom.xml

java中网关的概念 java 网关_ci_21

java中网关的概念 java 网关_java实现gateway_22

java中网关的概念 java 网关_spring_23

AuthorizeFilter.java

java中网关的概念 java 网关_java实现gateway_24

java中网关的概念 java 网关_java_25

application.yml

java中网关的概念 java 网关_java实现gateway_26

这里我还自定义了一个日志收集过滤器

java中网关的概念 java 网关_ci_27

java中网关的概念 java 网关_java实现gateway_28

用Postman访问就能看到效果

java中网关的概念 java 网关_java_29

java中网关的概念 java 网关_java_30

java中网关的概念 java 网关_java中网关的概念_31

3. Spring Cloud Gateway

java中网关的概念 java 网关_spring_32

3.1. GatewayFilter Factories

路由过滤器允许以某种方式修改输入的HTTP请求或输出的HTTP响应。路由过滤器适用于特定路由。Spring Cloud Gateway包括许多内置的GatewayFilter工厂。

3.1.1. AddRequestHeader GatewayFilter Factory

AddRequestHeader GatewayFilter 采用name和value参数。

例如:下面的例子,对于所有匹配的请求,将在下游请求头中添加 X-Request-red:blue

java中网关的概念 java 网关_spring_33

刚才说了,AddRequestHeader采用name和value作为参数。而URI中的变量可以用在value中,例如:

java中网关的概念 java 网关_ci_34

3.1.2. AddRequestParameter GatewayFilter Factory

AddRequestParameter GatewayFilter 也是采用name和value参数

例如:下面的例子,对于所有匹配的请求,将会在下游请求的查询字符串中添加 red=blue

java中网关的概念 java 网关_spring_35

同样,AddRequestParameter也支持在value中引用URI中的变量,例如:

java中网关的概念 java 网关_ci_36

3.1.3. AddResponseHeader GatewayFilter Factory

AddResponseHeader GatewayFilter 依然采用name和value参数。不在赘述,如下:

java中网关的概念 java 网关_ci_37

3.1.4. DedupeResponseHeader GatewayFilter Factory

DedupeResponseHeader GatewayFilter 采用一个name参数和一个可选的strategy参数。name可以包含以空格分隔的header名称列表。例如:下面的例子,如果在网关CORS逻辑和下游逻辑都将它们添加的情况下,这将删除Access-Control-Allow-Credentials和Access-Control-Allow-Origin响应头中的重复值。

java中网关的概念 java 网关_java中网关的概念_38

3.1.5. PrefixPath GatewayFilter Factory

PrefixPath GatewayFilter 只有一个prefix参数。下面的例子,对于所有匹配的请求,将会在请求url上加上前缀/mypath,因此请求/hello在被转发后的url变成/mypath/hello

java中网关的概念 java 网关_ci_39

3.1.6. RequestRateLimiter GatewayFilter Factory

RequestRateLimiter GatewayFilter 用一个RateLimiter实现来决定当前请求是否被允许处理。如果不被允许,默认将返回一个HTTP 429状态,表示太多的请求。

这个过滤器采用一个可选的keyResolver参数。keyResolver是实现了KeyResolver接口的一个bean。在配置中,通过SpEL表达式引用它。例如,#{@myKeyResolver}是一个SpEL表达式,它是对名字叫myKeyResolver的bean的引用。KeyResolver默认的实现是PrincipalNameKeyResolver。

默认情况下,如果KeyResolver没有找到一个key,那么请求将会被拒绝。你可以调整这种行为,通过设置spring.cloud.gateway.filter.request-rate-limiter.deny-empty-key (true or false) 和 spring.cloud.gateway.filter.request-rate-limiter.empty-key-status-code属性。

Redis基于 Token Bucket Algorithm (令牌桶算法)实现了一个RequestRateLimiter

redis-rate-limiter.replenishRate 属性指定一个用户每秒允许多少个请求,而没有任何丢弃的请求。这是令牌桶被填充的速率。

redis-rate-limiter.burstCapacity 属性指定用户在一秒钟内执行的最大请求数。这是令牌桶可以容纳的令牌数。将此值设置为零将阻止所有请求。

redis-rate-limiter.requestedTokens 属性指定一个请求要花费多少个令牌。这是每个请求从存储桶中获取的令牌数,默认为1。

通过将replenishRate和burstCapacity设置成相同的值可以实现稳定的速率。通过将burstCapacity设置为高于replenishRate,可以允许临时突发。 在这种情况下,速率限制器需要在两次突发之间保留一段时间(根据replenishRate),因为两个连续的突发将导致请求丢弃(HTTP 429-太多请求)。

通过将replenishRate设置为所需的请求数,将requestTokens设置为以秒为单位的时间跨度并将burstCapacity设置为replenishRate和requestedToken的乘积。可以达到1个请求的速率限制。 例如:设置replenishRate = 1,requestedTokens = 60和burstCapacity = 60将导致限制为每分钟1个请求。

java中网关的概念 java 网关_spring_40

KeyResolver

java中网关的概念 java 网关_java中网关的概念_41

上面的例子,定义了每个用户每秒运行10个请求,令牌桶的容量是20,那么,下一秒将只剩下10个令牌可用。KeyResolver实现仅仅只是简单取请求参数中的user,当然在生产环境中不推荐这么做。

说白了,KeyResolver就是决定哪些请求属于同一个用户的。比如,header中userId相同的就认为是同一个用户的请求。

当然,你也可以自己实现一个RateLimiter,在配置的时候用SpEL表达式#{@myRateLimiter}去引用它。例如:

java中网关的概念 java 网关_java实现gateway_42

补充:(Token Bucket)令牌桶令牌桶是在分组交换计算机网络和电信网络中使用的算法。它可以用来检查数据包形式的数据传输是否符合定义的带宽和突发性限制(对流量不均匀性或变化的度量)。令牌桶算法就好比是一个的固定容量桶,通常以固定速率向其中添加令牌。一个令牌通常代表一个字节。当要检查数据包是否符合定义的限制时,将检查令牌桶以查看其当时是否包含足够的令牌。如果有足够数量的令牌,并假设令牌以字节为单位,那么,与数据包字节数量等效数量的令牌将被删除,并且该数据包可以通过继续传输。如果令牌桶中的令牌数量不够,则数据包不符合要求,并且令牌桶的令牌数量不会变化。不合格的数据包可以有多种处理方式:它们可能会被丢弃当桶中积累了足够的令牌时,可以将它们加入队列进行后续传输它们可以被传输,但被标记为不符合,如果网络负载过高,可能随后被丢弃(PS:这句话的意思是说,想象有一个桶,以固定速率向桶中添加令牌。假设一个令牌等效于一个字节,当一个数据包到达时,假设这个数据包的大小是n字节,如果桶中有足够多的令牌,即桶中令牌的数量大于n,则该数据可以通过,并且桶中要删除n个令牌。如果桶中令牌数不够,则根据情况该数据包可能直接被丢弃,也可能一直等待直到令牌足够,也可能继续传输,但被标记为不合格。还是不够通俗。这样,如果把令牌桶想象成一个水桶的话,令牌想象成水滴的话,那么这个过程就变成了以恒定速率向水桶中滴水,当有人想打一碗水时,如果这个人的碗很小,只能装30滴水,而水桶中水滴数量超过30,那么这个人就可以打一碗水,然后就走了,相应的,水桶中的水在这个人打完以后自然就少了30滴。过了一会儿,又有一个人来打水,他拿的碗比较大,一次能装100滴水,这时候桶里的水不够,这个时候他可能就走了,或者在这儿等着,等到桶中积累了100滴的时候再打。哈哈哈,就是酱紫,不知道大家见过水车没有……)令牌桶算法可以简单地这样理解:每 1/r 秒有一个令牌被添加到令牌桶令牌桶最多可以容纳 b 个令牌。当一个令牌到达时,令牌桶已经满了,那么它将会被丢弃。当一个 n 字节大小的数据包到达时:如果令牌桶中至少有n个令牌,则从令牌桶中删除n个令牌,并将数据包发送到网络。如果可用的令牌少于n个,则不会从令牌桶中删除任何令牌,并且将数据包视为不合格。

3.1.7. RedirectTo GatewayFilter Factory

RedirectTo GatewayFilter 有两个参数:status 和 url。status应该是300系列的。不解释,看示例:

java中网关的概念 java 网关_ci_43

3.1.8. RemoveRequestHeader GatewayFilter Factory

java中网关的概念 java 网关_java中网关的概念_44

3.1.9. RewritePath GatewayFilter Factory

java中网关的概念 java 网关_java_45

3.1.10. Default Filters

为了添加一个过滤器,并将其应用到所有路由上,你可以使用spring.cloud.gateway.default-filters,这个属性值是一个过滤器列表

java中网关的概念 java 网关_java_46

3.2. Global Filters

GlobalFilter应用于所有路由

3.2.1. GlobalFilter与GatewayFilter组合的顺序

当一个请求请求与匹配某个路由时,过滤Web处理程序会将GlobalFilter的所有实例和GatewayFilter的所有特定于路由的实例添加到过滤器链中。该组合的过滤器链由org.springframework.core.Ordered接口排序,可以通过实现getOrder()方法进行设置。

由于Spring Cloud Gateway区分过滤器逻辑执行的“pre”和“post”阶段,因此,优先级最高的过滤器在“pre”阶段是第一个,在“post”阶段是最后一个。

java中网关的概念 java 网关_spring_47

补充:(Token Bucket)令牌桶令牌桶是在分组交换计算机网络和电信网络中使用的算法。它可以用来检查数据包形式的数据传输是否符合定义的带宽和突发性限制(对流量不均匀性或变化的度量)。

令牌桶算法就好比是一个的固定容量桶,通常以固定速率向其中添加令牌。一个令牌通常代表一个字节。当要检查数据包是否符合定义的限制时,将检查令牌桶以查看其当时是否包含足够的令牌。如果有足够数量的令牌,并假设令牌以字节为单位,那么,与数据包字节数量等效数量的令牌将被删除,并且该数据包可以通过继续传输。如果令牌桶中的令牌数量不够,则数据包不符合要求,并且令牌桶的令牌数量不会变化。不合格的数据包可以有多种处理方式:

它们可能会被丢弃当桶中积累了足够的令牌时,可以将它们加入队列进行后续传输它们可以被传输,但被标记为不符合,如果网络负载过高,可能随后被丢弃(PS:这句话的意思是说,想象有一个桶,以固定速率向桶中添加令牌。假设一个令牌等效于一个字节,当一个数据包到达时,假设这个数据包的大小是n字节,如果桶中有足够多的令牌,即桶中令牌的数量大于n,则该数据可以通过,并且桶中要删除n个令牌。如果桶中令牌数不够,则根据情况该数据包可能直接被丢弃,也可能一直等待直到令牌足够,也可能继续传输,但被标记为不合格。

还是不够通俗,这样,如果把令牌桶想象成一个水桶的话,令牌想象成水滴的话,那么这个过程就变成了以恒定速率向水桶中滴水,当有人想打一碗水时,如果这个人的碗很小,只能装30滴水,而水桶中水滴数量超过30,那么这个人就可以打一碗水,然后就走了,相应的,水桶中的水在这个人打完以后自然就少了30滴。过了一会儿,又有一个人来打水,他拿的碗比较大,一次能装100滴水,这时候桶里的水不够,这个时候他可能就走了,或者在这儿等着,等到桶中积累了100滴的时候再打。哈哈哈,就是酱紫,不知道大家见过水车没有……)

令牌桶算法可以简单地这样理解:

每 1/r 秒有一个令牌被添加到令牌桶令牌桶最多可以容纳 b 个令牌。当一个令牌到达时,令牌桶已经满了,那么它将会被丢弃。当一个 n 字节大小的数据包到达时:如果令牌桶中至少有n个令牌,则从令牌桶中删除n个令牌,并将数据包发送到网络。如果可用的令牌少于n个,则不会从令牌桶中删除任何令牌,并且将数据包视为不合格。