前言:

最近开发了Zuul网关的实现和Spring Cloud Gateway实现,对比Spring Cloud Gateway发现后者性能好支持场景也丰富。在高并发或者复杂的分布式下,后者限流和自定义拦截也很棒。

提示:

本文主要列出本人开发的Zuul网关核心代码以及Spring Cloud Gateway核心代码实现。因为本人技术有限,主要是参照了 Spring Cloud Gateway 如有不足之处还请见谅并留言指出。

1:为什么要做网关

(1)网关层对外部和内部进行了隔离,保障了后台服务的安全性。

(2)对外访问控制由网络层面转换成了运维层面,减少变更的流程和错误成本。

(3)减少客户端与服务的耦合,服务可以独立运行,并通过网关层来做映射。

(4)通过网关层聚合,减少外部访问的频次,提升访问效率。

(5)节约后端服务开发成本,减少上线风险。

(6)为服务熔断,灰度发布,线上测试提供简单方案。

(7)便于进行应用层面的扩展。

相信在寻找相关资料的伙伴应该都知道,在微服务环境下,要做到一个比较健壮的流量入口还是很重要的,需要考虑的问题也比较复杂和众多。

2:网关和鉴权基本实现架构(图中包含了auth组件,或SSO,文章结尾会提供此组件的实现)

![img](../images/最全面的改造Zuul网关为Spring Cloud Gateway/822716-20190805222236873-514557442.png)

3:Zuul的实现

(1)第一代的zuul使用的是netflix开发的,在pom引用上都是用的原来的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<!-- zuul网关最基本要用到的 -->
<!-- 封装原来的jedis,用处是在网关里来放token到redis或者调redis来验证当前是否有效,或者说直接用redis负载-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 客户端注册eureka使用的,微服务必备 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- zuul -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<!-- 熔断支持 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<!--负载均衡 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<!-- 调用feign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- 健康 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

(2)修改application-dev.yml 的内容

给个提示,在原来的starter-web中 yml的 context-path是不需要用的,微服务中只需要用application-name去注册中心找实例名即可,况且webflux后context-path已经不存在了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
spring:
application:
name: gateway

#eureka-gateway-monitor-config 每个端口+1
server:
port: 8702

#eureka注册配置
eureka:
instance:
#使用IP注册
prefer-ip-address: true
##续约更新时间间隔设置5秒,m默认30s
lease-renewal-interval-in-seconds: 30
##续约到期时间10秒,默认是90秒
lease-expiration-duration-in-seconds: 90
client:
serviceUrl:
defaultZone: http://localhost:8700/eureka/

# route connection
zuul:
host:
#单个服务最大请求
max-per-route-connections: 20
#网关最大连接数
max-total-connections: 200
#routes to serviceId
routes:
api-product.path: /api/product/**
api-product.serviceId: product
api-customer.path: /api/customer/**
api-customer.serviceId: customer



#移除url同时移除服务
auth-props:
#accessIp: 127.0.0.1
#accessToken: admin
#authLevel: dev
#服务
api-urlMap: {
product: 1&2,
customer: 1&1
}
#移除url同时移除服务
exclude-urls:
- /pro
- /cust


#断路时间
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 300000

#ribbon
ribbon:
ReadTimeout: 15000
ConnectTimeout: 15000
SocketTimeout: 15000
eager-load:
enabled: true
clients: product, customer

如果仅仅是转发,那很简单,如果要做好场景,则需要添加白名单和黑名单,在zuul里只需要加白名单即可,存在链接或者实例名才能通过filter转发。

重点在:

api-urlMap: 是实例名,如果链接不存在才会去校验,因为端口+链接可以访问,如果加实例名一起也能访问,防止恶意带实例名攻击或者抓包请求后去猜链接后缀来攻击。
exclude-urls: 白名单连接,每个微服务的请求入口地址,包含即通过。

(3)上面提到白名单,那需要初始化白名单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package org.yugh.gateway.config;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

/**
* //路由拦截配置
*
* @author: 余根海
* @creation: 2019-07-02 19:43
* @Copyright © 2019 yugenhai. All rights reserved.
*/
@Data
@Slf4j
@Component
@Configuration
@ConfigurationProperties(prefix = "auth-props")
public class ZuulPropConfig implements InitializingBean {

private static final String normal = "(\\w|\\d|-)+";
private List<Pattern> patterns = new ArrayList<>();
private Map<String, String> apiUrlMap;
private List<String> excludeUrls;
private String accessToken;
private String accessIp;
private String authLevel;

@Override
public void afterPropertiesSet() throws Exception {
excludeUrls.stream().map(s -> s.replace("*", normal)).map(Pattern::compile).forEach(patterns::add);
log.info("============> 配置的白名单Url:{}", patterns);
}
}

(4)核心代码zuulFilter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
package org.yugh.gateway.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.yugh.gateway.common.constants.Constant;
import org.yugh.gateway.common.enums.DeployEnum;
import org.yugh.gateway.common.enums.HttpStatusEnum;
import org.yugh.gateway.common.enums.ResultEnum;
import org.yugh.gateway.config.RedisClient;
import org.yugh.gateway.config.ZuulPropConfig;
import org.yugh.gateway.util.ResultJson;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import java.util.regex.Matcher;

/**
* //路由拦截转发请求
*
* @author: 余根海
* @creation: 2019-06-26 17:50
* @Copyright © 2019 yugenhai. All rights reserved.
*/
@Slf4j
public class PreAuthFilter extends ZuulFilter {


@Value("${spring.profiles.active}")
private String activeType;
@Autowired
private ZuulPropConfig zuulPropConfig;
@Autowired
private RedisClient redisClient;

@Override
public String filterType() {
return "pre";
}

@Override
public int filterOrder() {
return 0;
}


/**
* 部署级别可调控
*
* @return
* @author yugenhai
* @creation: 2019-06-26 17:50
*/
@Override
public boolean shouldFilter() {
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
if (activeType.equals(DeployEnum.DEV.getType())) {
log.info("请求地址 : {} 当前环境 : {} ", request.getServletPath(), DeployEnum.DEV.getType());
return true;
} else if (activeType.equals(DeployEnum.TEST.getType())) {
log.info("请求地址 : {} 当前环境 : {} ", request.getServletPath(), DeployEnum.TEST.getType());
return true;
} else if (activeType.equals(DeployEnum.PROD.getType())) {
log.info("请求地址 : {} 当前环境 : {} ", request.getServletPath(), DeployEnum.PROD.getType());
return true;
}
return true;
}


/**
* 路由拦截转发
*
* @return
* @author yugenhai
* @creation: 2019-06-26 17:50
*/
@Override
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
String requestMethod = context.getRequest().getMethod();
//判断请求方式
if (Constant.OPTIONS.equals(requestMethod)) {
log.info("请求的跨域的地址 : {} 跨域的方法", request.getServletPath(), requestMethod);
assemblyCross(context);
context.setResponseStatusCode(HttpStatusEnum.OK.code());
context.setSendZuulResponse(false);
return null;
}
//转发信息共享 其他服务不要依赖MVC拦截器,或重写拦截器
if (isIgnore(request, this::exclude, this::checkLength)) {
String token = getCookieBySso(request);
if(!StringUtils.isEmpty(token)){
//context.addZuulRequestHeader(JwtUtil.HEADER_AUTH, token);
}
log.info("请求白名单地址 : {} ", request.getServletPath());
return null;
}
String serverName = request.getServletPath().substring(1, request.getServletPath().indexOf('/', 1));
String authUserType = zuulPropConfig.getApiUrlMap().get(serverName);
log.info("实例服务名: {} 对应用户类型: {}", serverName, authUserType);
if (!StringUtils.isEmpty(authUserType)) {
//用户是否合法和登录
authToken(context);
} else {
//下线前删除配置的实例名
log.info("实例服务: {} 不允许访问", serverName);
unauthorized(context, HttpStatusEnum.FORBIDDEN.code(), "请求的服务已经作废,不可访问");
}
return null;

/******************************以下代码可能会复用,勿删,若使用Gateway整个路由项目将不使用 add by - yugenhai 2019-0704********************************************/

/*String readUrl = request.getServletPath().substring(1, request.getServletPath().indexOf('/', 1));
try {
if (request.getServletPath().length() <= Constant.PATH_LENGTH || zuulPropConfig.getRoutes().size() == 0) {
throw new Exception();
}
Iterator<Map.Entry<String,String>> zuulMap = zuulPropConfig.getRoutes().entrySet().iterator();
while(zuulMap.hasNext()){
Map.Entry<String, String> entry = zuulMap.next();
String routeValue = entry.getValue();
if(routeValue.startsWith(Constant.ZUUL_PREFIX)){
routeValue = routeValue.substring(1, routeValue.indexOf('/', 1));
}
if(routeValue.contains(readUrl)){
log.info("请求白名单地址 : {} 请求跳过的真实地址 :{} ", routeValue, request.getServletPath());
return null;
}
}
log.info("即将请求登录 : {} 实例名 : {} ", request.getServletPath(), readUrl);
authToken(context);
return null;
} catch (Exception e) {
log.info("gateway路由器请求异常 :{} 请求被拒绝 ", e.getMessage());
assemblyCross(context);
context.set("isSuccess", false);
context.setSendZuulResponse(false);
context.setResponseStatusCode(HttpStatusEnum.OK.code());
context.getResponse().setContentType("application/json;charset=UTF-8");
context.setResponseBody(JsonUtils.toJson(JsonResult.buildErrorResult(HttpStatusEnum.UNAUTHORIZED.code(),"Url Error, Please Check It")));
return null;
}
*/
}


/**
* 检查用户
*
* @param context
* @return
* @author yugenhai
* @creation: 2019-06-26 17:50
*/
private Object authToken(RequestContext context) {
HttpServletRequest request = context.getRequest();
HttpServletResponse response = context.getResponse();
/*boolean isLogin = sessionManager.isLogined(request, response);
//用户存在
if (isLogin) {
try {
User user = sessionManager.getUser(request);
log.info("用户存在 : {} ", JsonUtils.toJson(user));
// String token = userAuthUtil.generateToken(user.getNo(), user.getUserName(), user.getRealName());
log.info("根据用户生成的Token :{}", token);
//转发信息共享
// context.addZuulRequestHeader(JwtUtil.HEADER_AUTH, token);
//缓存 后期所有服务都判断
redisClient.set(user.getNo(), token, 20 * 60L);
//冗余一份
userService.syncUser(user);
} catch (Exception e) {
log.error("调用SSO获取用户信息异常 :{}", e.getMessage());
}
} else {
//根据该token查询该用户不存在
unLogin(request, context);
}*/
return null;

}


/**
* 未登录不路由
*
* @param request
*/
private void unLogin(HttpServletRequest request, RequestContext context) {
String requestURL = request.getRequestURL().toString();
String loginUrl = getSsoUrl(request) + "?returnUrl=" + requestURL;
//Map map = new HashMap(2);
//map.put("redirctUrl", loginUrl);
log.info("检查到该token对应的用户登录状态未登录 跳转到Login页面 : {} ", loginUrl);
assemblyCross(context);
context.getResponse().setContentType("application/json;charset=UTF-8");
context.set("isSuccess", false);
context.setSendZuulResponse(false);
//context.setResponseBody(ResultJson.failure(map, "This User Not Found, Please Check Token").toString());
context.setResponseStatusCode(HttpStatusEnum.OK.code());
}


/**
* 判断是否忽略对请求的校验
* @param request
* @param functions
* @return
*/
private boolean isIgnore(HttpServletRequest request, Function<HttpServletRequest, Boolean>... functions) {
return Arrays.stream(functions).anyMatch(f -> f.apply(request));
}


/**
* 判断是否存在地址
* @param request
* @return
*/
private boolean exclude(HttpServletRequest request) {
String servletPath = request.getServletPath();
if (!CollectionUtils.isEmpty(zuulPropConfig.getExcludeUrls())) {
return zuulPropConfig.getPatterns().stream()
.map(pattern -> pattern.matcher(servletPath))
.anyMatch(Matcher::find);
}
return false;
}


/**
* 校验请求连接是否合法
* @param request
* @return
*/
private boolean checkLength(HttpServletRequest request) {
return request.getServletPath().length() <= Constant.PATH_LENGTH || CollectionUtils.isEmpty(zuulPropConfig.getApiUrlMap());
}


/**
* 会话存在则跨域发送
* @param request
* @return
*/
private String getCookieBySso(HttpServletRequest request){
Cookie cookie = this.getCookieByName(request, "");
return cookie != null ? cookie.getValue() : null;
}


/**
* 不路由直接返回
* @param ctx
* @param code
* @param msg
*/
private void unauthorized(RequestContext ctx, int code, String msg) {
assemblyCross(ctx);
ctx.getResponse().setContentType("application/json;charset=UTF-8");
ctx.setSendZuulResponse(false);
ctx.setResponseBody(ResultJson.failure(ResultEnum.UNAUTHORIZED, msg).toString());
ctx.set("isSuccess", false);
ctx.setResponseStatusCode(HttpStatusEnum.OK.code());
}


/**
* 获取会话里的token
* @param request
* @param name
* @return
*/
private Cookie getCookieByName(HttpServletRequest request, String name) {
Map<String, Cookie> cookieMap = new HashMap(16);
Cookie[] cookies = request.getCookies();
if (!StringUtils.isEmpty(cookies)) {
Cookie[] c1 = cookies;
int length = cookies.length;
for(int i = 0; i < length; ++i) {
Cookie cookie = c1[i];
cookieMap.put(cookie.getName(), cookie);
}
}else {
return null;
}
if (cookieMap.containsKey(name)) {
Cookie cookie = cookieMap.get(name);
return cookie;
}
return null;
}


/**
* 重定向前缀拼接
*
* @param request
* @return
*/
private String getSsoUrl(HttpServletRequest request) {
String serverName = request.getServerName();
if (StringUtils.isEmpty(serverName)) {
return "https://github.com/yugenhai108";
}
return "https://github.com/yugenhai108";

}

/**
* 拼装跨域处理
*/
private void assemblyCross(RequestContext ctx) {
HttpServletResponse response = ctx.getResponse();
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Headers", ctx.getRequest().getHeader("Access-Control-Request-Headers"));
response.setHeader("Access-Control-Allow-Methods", "*");
}
}

在 if (isIgnore(request, this::exclude, this::checkLength)) { 里面可以去调鉴权组件,或者用redis去存放token,获取直接用redis负载抗流量,具体可以自己实现。

4:Spring Cloud Gateway的实现

(1)第二代的Gateway则是由Spring Cloud开发,而且用了最新的Spring5.0和响应式Reactor以及最新的Webflux等等,比如原来的阻塞式请求现在变成了异步非阻塞式。

 那么在pom上就变了,变得和原来的starer-web也不兼容了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<dependency>
<groupId>org.yugh</groupId>
<artifactId>global-auth</artifactId>
<version>0.0.1-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- gateway -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- feign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

(2)修改application-dev.yml 的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
server:
port: 8706
#setting
spring:
application:
name: gateway-new
#redis
redis:
host: localhost
port: 6379
database: 0
timeout: 5000
#遇到相同名字,允许覆盖
main:
allow-bean-definition-overriding: true
#gateway
cloud:
gateway:
#注册中心服务发现
discovery:
locator:
#开启通过服务中心的自动根据 serviceId 创建路由的功能
enabled: true
routes:
#服务1
- id: CompositeDiscoveryClient_CUSTOMER
uri: lb://CUSTOMER
order: 1
predicates:
# 跳过自定义是直接带实例名 必须是大写 同样限流拦截失效
- Path= /api/customer/**
filters:
- StripPrefix=2
- AddResponseHeader=X-Response-Default-Foo, Default-Bar
- name: RequestRateLimiter
args:
key-resolver: "#{@gatewayKeyResolver}"
#限额配置
redis-rate-limiter.replenishRate: 1
redis-rate-limiter.burstCapacity: 1
#用户微服务
- id: CompositeDiscoveryClient_PRODUCT
uri: lb://PRODUCT
order: 0
predicates:
- Path= /api/product/**
filters:
- StripPrefix=2
- AddResponseHeader=X-Response-Default-Foo, Default-Bar
- name: RequestRateLimiter
args:
key-resolver: "#{@gatewayKeyResolver}"
#限额配置
redis-rate-limiter.replenishRate: 1
redis-rate-limiter.burstCapacity: 1
#请求路径选择自定义会进入限流器
default-filters:
- AddResponseHeader=X-Response-Default-Foo, Default-Bar
- name: gatewayKeyResolver
args:
key-resolver: "#{@gatewayKeyResolver}"
#断路异常跳转
- name: Hystrix
args:
#网关异常或超时跳转到处理类
name: fallbackcmd
fallbackUri: forward:/fallbackController

#safe path
auth-skip:
instance-servers:
- CUSTOMER
- PRODUCT
api-urls:
#PRODUCT
- /pro
#CUSTOMER
- /cust

#gray-env
#...

#log
logging:
level:
org.yugh: INFO
org.springframework.cloud.gateway: INFO
org.springframework.http.server.reactive: INFO
org.springframework.web.reactive: INFO
reactor.ipc.netty: INFO

#reg
eureka:
instance:
prefer-ip-address: true
client:
serviceUrl:
defaultZone: http://localhost:8700/eureka/


ribbon:
eureka:
enabled: true
ReadTimeout: 120000
ConnectTimeout: 30000


#feign
feign:
hystrix:
enabled: false

#hystrix
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 20000

management:
endpoints:
web:
exposure:
include: '*'
base-path: /actuator
endpoint:
health:
show-details: ALWAYS

网关限流用的 spring-boot-starter-data-redis-reactive 做令牌桶IP限流。

具体实现在这个类gatewayKeyResolver

(3)令牌桶IP限流,限制当前IP的请求配额

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package org.yugh.gatewaynew.config;

import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
* //令牌桶IP限流
*
* @author 余根海
* @creation 2019-07-05 15:52
* @Copyright © 2019 yugenhai. All rights reserved.
*/
@Component
public class GatewayKeyResolver implements KeyResolver {

@Override
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
}
}

(4)网关的白名单和黑名单配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package org.yugh.gatewaynew.properties;


import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;

/**
* //白名单和黑名单属性配置
*
* @author 余根海
* @creation 2019-07-05 15:52
* @Copyright © 2019 yugenhai. All rights reserved.
*/
@Data
@Slf4j
@Component
@Configuration
@ConfigurationProperties(prefix = "auth-skip")
public class AuthSkipUrlsProperties implements InitializingBean {

private static final String NORMAL = "(\\w|\\d|-)+";
private List<Pattern> urlPatterns = new ArrayList(10);
private List<Pattern> serverPatterns = new ArrayList(10);
private List<String> instanceServers;
private List<String> apiUrls;

@Override
public void afterPropertiesSet() {
instanceServers.stream().map(d -> d.replace("*", NORMAL)).map(Pattern::compile).forEach(serverPatterns::add);
apiUrls.stream().map(s -> s.replace("*", NORMAL)).map(Pattern::compile).forEach(urlPatterns::add);
log.info("============> 配置服务器ID : {} , 白名单Url : {}", serverPatterns, urlPatterns);
}
}

(5)核心网关代码GatewayFilter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
package org.yugh.gatewaynew.filter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.CollectionUtils;
import org.springframework.web.server.ServerWebExchange;
import org.yugh.gatewaynew.config.GatewayContext;
import org.yugh.gatewaynew.properties.AuthSkipUrlsProperties;
import org.yugh.globalauth.common.constants.Constant;
import org.yugh.globalauth.common.enums.ResultEnum;
import org.yugh.globalauth.pojo.dto.User;
import org.yugh.globalauth.service.AuthService;
import org.yugh.globalauth.util.ResultJson;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;
import java.util.concurrent.ExecutorService;
import java.util.regex.Matcher;

/**
* // 网关服务
*
* @author 余根海
* @creation 2019-07-09 10:52
* @Copyright © 2019 yugenhai. All rights reserved.
*/
@Slf4j
public class GatewayFilter implements GlobalFilter, Ordered {

@Autowired
private AuthSkipUrlsProperties authSkipUrlsProperties;
@Autowired
@Qualifier(value = "gatewayQueueThreadPool")
private ExecutorService buildGatewayQueueThreadPool;
@Autowired
private AuthService authService;


@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
GatewayContext context = new GatewayContext();
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
response.getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF8);
log.info("当前会话ID : {}", request.getId());
//防止网关监控不到限流请求
if (blackServersCheck(context, exchange)) {
response.setStatusCode(HttpStatus.FORBIDDEN);
byte[] failureInfo = ResultJson.failure(ResultEnum.BLACK_SERVER_FOUND).toString().getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(failureInfo);
return response.writeWith(Flux.just(buffer));
}
//白名单
if (whiteListCheck(context, exchange)) {
authToken(context, request);
if (!context.isDoNext()) {
byte[] failureInfo = ResultJson.failure(ResultEnum.LOGIN_ERROR_GATEWAY, context.getRedirectUrl()).toString().getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(failureInfo);
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.writeWith(Flux.just(buffer));
}
ServerHttpRequest mutateReq = exchange.getRequest().mutate().header(Constant.TOKEN, context.getSsoToken()).build();
ServerWebExchange mutableExchange = exchange.mutate().request(mutateReq).build();
log.info("当前会话转发成功 : {}", request.getId());
return chain.filter(mutableExchange);
} else {
//黑名单
response.setStatusCode(HttpStatus.FORBIDDEN);
byte[] failureInfo = ResultJson.failure(ResultEnum.WHITE_NOT_FOUND).toString().getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(failureInfo);
return response.writeWith(Flux.just(buffer));
}
}


@Override
public int getOrder() {
return Integer.MIN_VALUE;
}

/**
* 检查用户
*
* @param context
* @param request
* @return
* @author yugenhai
*/
private void authToken(GatewayContext context, ServerHttpRequest request) {
try {
// boolean isLogin = authService.isLoginByReactive(request);
boolean isLogin = true;
if (isLogin) {
//User userDo = authService.getUserByReactive(request);
try {
// String ssoToken = authCookieUtils.getCookieByNameByReactive(request, Constant.TOKEN);
String ssoToken = "123";
context.setSsoToken(ssoToken);
} catch (Exception e) {
log.error("用户调用失败 : {}", e.getMessage());
context.setDoNext(false);
return;
}
} else {
unLogin(context, request);
}
} catch (Exception e) {
log.error("获取用户信息异常 :{}", e.getMessage());
context.setDoNext(false);
}
}


/**
* 网关同步用户
*
* @param userDto
*/
public void synUser(User userDto) {
buildGatewayQueueThreadPool.execute(new Runnable() {
@Override
public void run() {
log.info("用户同步成功 : {}", "");
}
});

}


/**
* 视为不能登录
*
* @param context
* @param request
*/
private void unLogin(GatewayContext context, ServerHttpRequest request) {
String loginUrl = getSsoUrl(request) + "?returnUrl=" + request.getURI();
context.setRedirectUrl(loginUrl);
context.setDoNext(false);
log.info("检查到该token对应的用户登录状态未登录 跳转到Login页面 : {} ", loginUrl);
}


/**
* 白名单
*
* @param context
* @param exchange
* @return
*/
private boolean whiteListCheck(GatewayContext context, ServerWebExchange exchange) {
String url = exchange.getRequest().getURI().getPath();
boolean white = authSkipUrlsProperties.getUrlPatterns().stream()
.map(pattern -> pattern.matcher(url))
.anyMatch(Matcher::find);
if (white) {
context.setPath(url);
return true;
}
return false;
}


/**
* 黑名单
*
* @param context
* @param exchange
* @return
*/
private boolean blackServersCheck(GatewayContext context, ServerWebExchange exchange) {
String instanceId = exchange.getRequest().getURI().getPath().substring(1, exchange.getRequest().getURI().getPath().indexOf('/', 1));
if (!CollectionUtils.isEmpty(authSkipUrlsProperties.getInstanceServers())) {
boolean black = authSkipUrlsProperties.getServerPatterns().stream()
.map(pattern -> pattern.matcher(instanceId))
.anyMatch(Matcher::find);
if (black) {
context.setBlack(true);
return true;
}
}
return false;
}


/**
* @param request
* @return
*/
private String getSsoUrl(ServerHttpRequest request) {
return request.getPath().value();
}
}

在 private void authToken(GatewayContext context, ServerHttpRequest request) { 这个方法里可以自定义做验证。

结束语:

我实现了一遍两种网关,发现还是官网的文档最靠谱,也是能落地到项目中的。如果你需要源码的请到 我的Github 去clone,如果帮助到了你,还请点个 star。

原文地址:https://www.cnblogs.com/KuJo/p/11306361.html