最全面的改造Zuul网关为Spring Cloud Gateway(包含Zuul核心实现和Spring Cloud Gateway核心实现)
前言:
最近开发了Zuul网关的实现和Spring Cloud Gateway实现,对比Spring Cloud Gateway发现后者性能好支持场景也丰富。在高并发或者复杂的分布式下,后者限流和自定义拦截也很棒。
提示:
本文主要列出本人开发的Zuul网关核心代码以及Spring Cloud Gateway核心代码实现。因为本人技术有限,主要是参照了 Spring Cloud Gateway 如有不足之处还请见谅并留言指出。
1:为什么要做网关
1 <!-- zuul网关最基本要用到的 -->
2 <!-- 封装原来的jedis,用处是在网关里来放token到redis或者调redis来验证当前是否有效,或者说直接用redis负载-->
3 <dependency>
4 <groupId>org.springframework.boot</groupId>
5 <artifactId>spring-boot-starter-data-redis</artifactId>
6 </dependency>
7 <!-- 客户端注册eureka使用的,微服务必备 -->
8 <dependency>
9 <groupId>org.springframework.cloud</groupId>
10 <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
11 </dependency>
12 <!-- zuul -->
13 <dependency>
14 <groupId>org.springframework.cloud</groupId>
15 <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
16 </dependency>
17 <!-- 熔断支持 -->
18 <dependency>
19 <groupId>org.springframework.cloud</groupId>
20 <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
21 </dependency>
22 <!--负载均衡 -->
23 <dependency>
24 <groupId>org.springframework.cloud</groupId>
25 <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
26 </dependency>
27 <!-- 调用feign -->
28 <dependency>
29 <groupId>org.springframework.cloud</groupId>
30 <artifactId>spring-cloud-starter-openfeign</artifactId>
31 </dependency>
32 <!-- 健康 -->
33 <dependency>
34 <groupId>org.springframework.boot</groupId>
35 <artifactId>spring-boot-starter-actuator</artifactId>
36 </dependency>
(2)修改application-dev.yml 的内容
给个提示,在原来的starter-web中 yml的 context-path是不需要用的,微服务中只需要用application-name去注册中心找实例名即可,况且webflux后context-path已经不存在了。
1 spring: 2 application: 3 name: gateway 4 5 #eureka-gateway-monitor-config 每个端口+1 6 server: 7 port: 8702 8 9 #eureka注册配置 10 eureka: 11 instance: 12 #使用IP注册 13 prefer-ip-address: true 14 ##续约更新时间间隔设置5秒,m默认30s 15 lease-renewal-interval-in-seconds: 30 16 ##续约到期时间10秒,默认是90秒 17 lease-expiration-duration-in-seconds: 90 18 client: 19 serviceUrl: 20 defaultZone: http://localhost:8700/eureka/ 21 22 # route connection 23 zuul: 24 host: 25 #单个服务最大请求 26 max-per-route-connections: 20 27 #网关最大连接数 28 max-total-connections: 200 29 #routes to serviceId 30 routes: 31 api-product.path: /api/product/** 32 api-product.serviceId: product 33 api-customer.path: /api/customer/** 34 api-customer.serviceId: customer 35 36 37 38 #移除url同时移除服务 39 auth-props: 40 #accessIp: 127.0.0.1 41 #accessToken: admin 42 #authLevel: dev 43 #服务 44 api-urlMap: { 45 product: 1&2, 46 customer: 1&1 47 } 48 #移除url同时移除服务 49 exclude-urls: 50 - /pro 51 - /cust 52 53 54 #断路时间 55 hystrix: 56 command: 57 default: 58 execution: 59 isolation: 60 thread: 61 timeoutInMilliseconds: 300000 62 63 #ribbon 64 ribbon: 65 ReadTimeout: 15000 66 ConnectTimeout: 15000 67 SocketTimeout: 15000 68 eager-load: 69 enabled: true 70 clients: product, customer
如果仅仅是转发,那很简单,如果要做好场景,则需要添加白名单和黑名单,在zuul里只需要加白名单即可,存在链接或者实例名才能通过filter转发。
重点在:
api-urlMap: 是实例名,如果链接不存在才会去校验,因为端口+链接可以访问,如果加实例名一起也能访问,防止恶意带实例名攻击或者抓包请求后去猜链接后缀来攻击。
exclude-urls: 白名单连接,每个微服务的请求入口地址,包含即通过。
1 package org.yugh.gateway.config;
2
3 import lombok.Data;
4 import lombok.extern.slf4j.Slf4j;
5 import org.springframework.beans.factory.InitializingBean;
6 import org.springframework.boot.context.properties.ConfigurationProperties;
7 import org.springframework.context.annotation.Configuration;
8 import org.springframework.stereotype.Component;
9
10 import java.util.ArrayList;
11 import java.util.List;
12 import java.util.Map;
13 import java.util.regex.Pattern;
14
15 /**
16 * //路由拦截配置
17 *
18 * @author: 余根海
19 * @creation: 2019-07-02 19:43
20 * @Copyright © 2019 yugenhai. All rights reserved.
21 */
22 @Data
23 @Slf4j
24 @Component
25 @Configuration
26 @ConfigurationProperties(prefix = "auth-props")
27 public class ZuulPropConfig implements InitializingBean {
28
29 private static final String normal = "(\\w|\\d|-)+";
30 private List<Pattern> patterns = new ArrayList<>();
31 private Map<String, String> apiUrlMap;
32 private List<String> excludeUrls;
33 private String accessToken;
34 private String accessIp;
35 private String authLevel;
36
37 @Override
38 public void afterPropertiesSet() throws Exception {
39 excludeUrls.stream().map(s -> s.replace("*", normal)).map(Pattern::compile).forEach(patterns::add);
40 log.info("============> 配置的白名单Url:{}", patterns);
41 }
42
43
44 }
(4)核心代码zuulFilter
1 package org.yugh.gateway.filter;
2
3 import com.netflix.zuul.ZuulFilter;
4 import com.netflix.zuul.context.RequestContext;
5 import lombok.extern.slf4j.Slf4j;
6 import org.springframework.beans.factory.annotation.Autowired;
7 import org.springframework.beans.factory.annotation.Value;
8 import org.springframework.util.CollectionUtils;
9 import org.springframework.util.StringUtils;
10 import org.yugh.gateway.common.constants.Constant;
11 import org.yugh.gateway.common.enums.DeployEnum;
12 import org.yugh.gateway.common.enums.HttpStatusEnum;
13 import org.yugh.gateway.common.enums.ResultEnum;
14 import org.yugh.gateway.config.RedisClient;
15 import org.yugh.gateway.config.ZuulPropConfig;
16 import org.yugh.gateway.util.ResultJson;
17
18 import javax.servlet.http.Cookie;
19 import javax.servlet.http.HttpServletRequest;
20 import javax.servlet.http.HttpServletResponse;
21 import java.util.Arrays;
22 import java.util.HashMap;
23 import java.util.Map;
24 import java.util.function.Function;
25 import java.util.regex.Matcher;
26
27 /**
28 * //路由拦截转发请求
29 *
30 * @author: 余根海
31 * @creation: 2019-06-26 17:50
32 * @Copyright © 2019 yugenhai. All rights reserved.
33 */
34 @Slf4j
35 public class PreAuthFilter extends ZuulFilter {
36
37
38 @Value("${spring.profiles.active}")
39 private String activeType;
40 @Autowired
41 private ZuulPropConfig zuulPropConfig;
42 @Autowired
43 private RedisClient redisClient;
44
45 @Override
46 public String filterType() {
47 return "pre";
48 }
49
50 @Override
51 public int filterOrder() {
52 return 0;
53 }
54
55
56 /**
57 * 部署级别可调控
58 *
59 * @return
60 * @author yugenhai
61 * @creation: 2019-06-26 17:50
62 */
63 @Override
64 public boolean shouldFilter() {
65 RequestContext context = RequestContext.getCurrentContext();
66 HttpServletRequest request = context.getRequest();
67 if (activeType.equals(DeployEnum.DEV.getType())) {
68 log.info("请求地址 : {} 当前环境 : {} ", request.getServletPath(), DeployEnum.DEV.getType());
69 return true;
70 } else if (activeType.equals(DeployEnum.TEST.getType())) {
71 log.info("请求地址 : {} 当前环境 : {} ", request.getServletPath(), DeployEnum.TEST.getType());
72 return true;
73 } else if (activeType.equals(DeployEnum.PROD.getType())) {
74 log.info("请求地址 : {} 当前环境 : {} ", request.getServletPath(), DeployEnum.PROD.getType());
75 return true;
76 }
77 return true;
78 }
79
80
81 /**
82 * 路由拦截转发
83 *
84 * @return
85 * @author yugenhai
86 * @creation: 2019-06-26 17:50
87 */
88 @Override
89 public Object run() {
90 RequestContext context = RequestContext.getCurrentContext();
91 HttpServletRequest request = context.getRequest();
92 String requestMethod = context.getRequest().getMethod();
93 //判断请求方式
94 if (Constant.OPTIONS.equals(requestMethod)) {
95 log.info("请求的跨域的地址 : {} 跨域的方法", request.getServletPath(), requestMethod);
96 assemblyCross(context);
97 context.setResponseStatusCode(HttpStatusEnum.OK.code());
98 context.setSendZuulResponse(false);
99 return null;
100 }
101 //转发信息共享 其他服务不要依赖MVC拦截器,或重写拦截器
102 if (isIgnore(request, this::exclude, this::checkLength)) {
103 String token = getCookieBySso(request);
104 if(!StringUtils.isEmpty(token)){
105 //context.addZuulRequestHeader(JwtUtil.HEADER_AUTH, token);
106 }
107 log.info("请求白名单地址 : {} ", request.getServletPath());
108 return null;
109 }
110 String serverName = request.getServletPath().substring(1, request.getServletPath().indexOf('/', 1));
111 String authUserType = zuulPropConfig.getApiUrlMap().get(serverName);
112 log.info("实例服务名: {} 对应用户类型: {}", serverName, authUserType);
113 if (!StringUtils.isEmpty(authUserType)) {
114 //用户是否合法和登录
115 authToken(context);
116 } else {
117 //下线前删除配置的实例名
118 log.info("实例服务: {} 不允许访问", serverName);
119 unauthorized(context, HttpStatusEnum.FORBIDDEN.code(), "请求的服务已经作废,不可访问");
120 }
121 return null;
122
123 /******************************以下代码可能会复用,勿删,若使用Gateway整个路由项目将不使用 add by - yugenhai 2019-0704********************************************/
124
125 /*String readUrl = request.getServletPath().substring(1, request.getServletPath().indexOf('/', 1));
126 try {
127 if (request.getServletPath().length() <= Constant.PATH_LENGTH || zuulPropConfig.getRoutes().size() == 0) {
128 throw new Exception();
129 }
130 Iterator<Map.Entry<String,String>> zuulMap = zuulPropConfig.getRoutes().entrySet().iterator();
131 while(zuulMap.hasNext()){
132 Map.Entry<String, String> entry = zuulMap.next();
133 String routeValue = entry.getValue();
134 if(routeValue.startsWith(Constant.ZUUL_PREFIX)){
135 routeValue = routeValue.substring(1, routeValue.indexOf('/', 1));
136 }
137 if(routeValue.contains(readUrl)){
138 log.info("请求白名单地址 : {} 请求跳过的真实地址 :{} ", routeValue, request.getServletPath());
139 return null;
140 }
141 }
142 log.info("即将请求登录 : {} 实例名 : {} ", request.getServletPath(), readUrl);
143 authToken(context);
144 return null;
145 } catch (Exception e) {
146 log.info("gateway路由器请求异常 :{} 请求被拒绝 ", e.getMessage());
147 assemblyCross(context);
148 context.set("isSuccess", false);
149 context.setSendZuulResponse(false);
150 context.setResponseStatusCode(HttpStatusEnum.OK.code());
151 context.getResponse().setContentType("application/json;charset=UTF-8");
152 context.setResponseBody(JsonUtils.toJson(JsonResult.buildErrorResult(HttpStatusEnum.UNAUTHORIZED.code(),"Url Error, Please Check It")));
153 return null;
154 }
155 */
156 }
157
158
159 /**
160 * 检查用户
161 *
162 * @param context
163 * @return
164 * @author yugenhai
165 * @creation: 2019-06-26 17:50
166 */
167 private Object authToken(RequestContext context) {
168 HttpServletRequest request = context.getRequest();
169 HttpServletResponse response = context.getResponse();
170 /*boolean isLogin = sessionManager.isLogined(request, response);
171 //用户存在
172 if (isLogin) {
173 try {
174 User user = sessionManager.getUser(request);
175 log.info("用户存在 : {} ", JsonUtils.toJson(user));
176 // String token = userAuthUtil.generateToken(user.getNo(), user.getUserName(), user.getRealName());
177 log.info("根据用户生成的Token :{}", token);
178 //转发信息共享
179 // context.addZuulRequestHeader(JwtUtil.HEADER_AUTH, token);
180 //缓存 后期所有服务都判断
181 redisClient.set(user.getNo(), token, 20 * 60L);
182 //冗余一份
183 userService.syncUser(user);
184 } catch (Exception e) {
185 log.error("调用SSO获取用户信息异常 :{}", e.getMessage());
186 }
187 } else {
188 //根据该token查询该用户不存在
189 unLogin(request, context);
190 }*/
191 return null;
192
193 }
194
195
196 /**
197 * 未登录不路由
198 *
199 * @param request
200 */
201 private void unLogin(HttpServletRequest request, RequestContext context) {
202 String requestURL = request.getRequestURL().toString();
203 String loginUrl = getSsoUrl(request) + "?returnUrl=" + requestURL;
204 //Map map = new HashMap(2);
205 //map.put("redirctUrl", loginUrl);
206 log.info("检查到该token对应的用户登录状态未登录 跳转到Login页面 : {} ", loginUrl);
207 assemblyCross(context);
208 context.getResponse().setContentType("application/json;charset=UTF-8");
209 context.set("isSuccess", false);
210 context.setSendZuulResponse(false);
211 //context.setResponseBody(ResultJson.failure(map, "This User Not Found, Please Check Token").toString());
212 context.setResponseStatusCode(HttpStatusEnum.OK.code());
213 }
214
215
216 /**
217 * 判断是否忽略对请求的校验
218 * @param request
219 * @param functions
220 * @return
221 */
222 private boolean isIgnore(HttpServletRequest request, Function<HttpServletRequest, Boolean>... functions) {
223 return Arrays.stream(functions).anyMatch(f -> f.apply(request));
224 }
225
226
227 /**
228 * 判断是否存在地址
229 * @param request
230 * @return
231 */
232 private boolean exclude(HttpServletRequest request) {
233 String servletPath = request.getServletPath();
234 if (!CollectionUtils.isEmpty(zuulPropConfig.getExcludeUrls())) {
235 return zuulPropConfig.getPatterns().stream()
236 .map(pattern -> pattern.matcher(servletPath))
237 .anyMatch(Matcher::find);
238 }
239 return false;
240 }
241
242
243 /**
244 * 校验请求连接是否合法
245 * @param request
246 * @return
247 */
248 private boolean checkLength(HttpServletRequest request) {
249 return request.getServletPath().length() <= Constant.PATH_LENGTH || CollectionUtils.isEmpty(zuulPropConfig.getApiUrlMap());
250 }
251
252
253 /**
254 * 会话存在则跨域发送
255 * @param request
256 * @return
257 */
258 private String getCookieBySso(HttpServletRequest request){
259 Cookie cookie = this.getCookieByName(request, "");
260 return cookie != null ? cookie.getValue() : null;
261 }
262
263
264 /**
265 * 不路由直接返回
266 * @param ctx
267 * @param code
268 * @param msg
269 */
270 private void unauthorized(RequestContext ctx, int code, String msg) {
271 assemblyCross(ctx);
272 ctx.getResponse().setContentType("application/json;charset=UTF-8");
273 ctx.setSendZuulResponse(false);
274 ctx.setResponseBody(ResultJson.failure(ResultEnum.UNAUTHORIZED, msg).toString());
275 ctx.set("isSuccess", false);
276 ctx.setResponseStatusCode(HttpStatusEnum.OK.code());
277 }
278
279
280 /**
281 * 获取会话里的token
282 * @param request
283 * @param name
284 * @return
285 */
286 private Cookie getCookieByName(HttpServletRequest request, String name) {
287 Map<String, Cookie> cookieMap = new HashMap(16);
288 Cookie[] cookies = request.getCookies();
289 if (!StringUtils.isEmpty(cookies)) {
290 Cookie[] c1 = cookies;
291 int length = cookies.length;
292 for(int i = 0; i < length; ++i) {
293 Cookie cookie = c1[i];
294 cookieMap.put(cookie.getName(), cookie);
295 }
296 }else {
297 return null;
298 }
299 if (cookieMap.containsKey(name)) {
300 Cookie cookie = cookieMap.get(name);
301 return cookie;
302 }
303 return null;
304 }
305
306
307 /**
308 * 重定向前缀拼接
309 *
310 * @param request
311 * @return
312 */
313 private String getSsoUrl(HttpServletRequest request) {
314 String serverName = request.getServerName();
315 if (StringUtils.isEmpty(serverName)) {
316 return "https://github.com/yugenhai108";
317 }
318 return "https://github.com/yugenhai108";
319
320 }
321
322 /**
323 * 拼装跨域处理
324 */
325 private void assemblyCross(RequestContext ctx) {
326 HttpServletResponse response = ctx.getResponse();
327 response.setHeader("Access-Control-Allow-Origin", "*");
328 response.setHeader("Access-Control-Allow-Headers", ctx.getRequest().getHeader("Access-Control-Request-Headers"));
329 response.setHeader("Access-Control-Allow-Methods", "*");
330 }
331
332
333 }
在 if (isIgnore(request, this::exclude, this::checkLength)) { 里面可以去调鉴权组件,或者用redis去存放token,获取直接用redis负载抗流量,具体可以自己实现。
4:Spring Cloud Gateway的实现
1 <dependency>
2 <groupId>org.yugh</groupId>
3 <artifactId>global-auth</artifactId>
4 <version>0.0.1-SNAPSHOT</version>
5 <exclusions>
6 <exclusion>
7 <groupId>org.springframework.boot</groupId>
8 <artifactId>spring-boot-starter-web</artifactId>
9 </exclusion>
10 </exclusions>
11 </dependency>
12 <!-- gateway -->
13 <dependency>
14 <groupId>org.springframework.cloud</groupId>
15 <artifactId>spring-cloud-starter-gateway</artifactId>
16 </dependency>
17 <dependency>
18 <groupId>org.springframework.cloud</groupId>
19 <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
20 </dependency>
21 <!-- feign -->
22 <dependency>
23 <groupId>org.springframework.cloud</groupId>
24 <artifactId>spring-cloud-starter-openfeign</artifactId>
25 </dependency>
26 <dependency>
27 <groupId>org.springframework.boot</groupId>
28 <artifactId>spring-boot-starter-actuator</artifactId>
29 </dependency>
30 <dependency>
31 <groupId>org.springframework.boot</groupId>
32 <artifactId>spring-boot-configuration-processor</artifactId>
33 </dependency>
34 <!-- redis -->
35 <dependency>
36 <groupId>org.springframework.boot</groupId>
37 <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
38 </dependency>
39 <dependency>
40 <groupId>com.google.guava</groupId>
41 <artifactId>guava</artifactId>
42 <version>23.0</version>
43 </dependency>
44 <dependency>
45 <groupId>org.springframework.boot</groupId>
46 <artifactId>spring-boot-starter-test</artifactId>
47 <scope>test</scope>
48 </dependency>
(2)修改application-dev.yml 的内容
1 server:
2 port: 8706
3 #setting
4 spring:
5 application:
6 name: gateway-new
7 #redis
8 redis:
9 host: localhost
10 port: 6379
11 database: 0
12 timeout: 5000
13 #遇到相同名字,允许覆盖
14 main:
15 allow-bean-definition-overriding: true
16 #gateway
17 cloud:
18 gateway:
19 #注册中心服务发现
20 discovery:
21 locator:
22 #开启通过服务中心的自动根据 serviceId 创建路由的功能
23 enabled: true
24 routes:
25 #服务1
26 - id: CompositeDiscoveryClient_CUSTOMER
27 uri: lb://CUSTOMER
28 order: 1
29 predicates:
30 # 跳过自定义是直接带实例名 必须是大写 同样限流拦截失效
31 - Path= /api/customer/**
32 filters:
33 - StripPrefix=2
34 - AddResponseHeader=X-Response-Default-Foo, Default-Bar
35 - name: RequestRateLimiter
36 args:
37 key-resolver: "#{@gatewayKeyResolver}"
38 #限额配置
39 redis-rate-limiter.replenishRate: 1
40 redis-rate-limiter.burstCapacity: 1
41 #用户微服务
42 - id: CompositeDiscoveryClient_PRODUCT
43 uri: lb://PRODUCT
44 order: 0
45 predicates:
46 - Path= /api/product/**
47 filters:
48 - StripPrefix=2
49 - AddResponseHeader=X-Response-Default-Foo, Default-Bar
50 - name: RequestRateLimiter
51 args:
52 key-resolver: "#{@gatewayKeyResolver}"
53 #限额配置
54 redis-rate-limiter.replenishRate: 1
55 redis-rate-limiter.burstCapacity: 1
56 #请求路径选择自定义会进入限流器
57 default-filters:
58 - AddResponseHeader=X-Response-Default-Foo, Default-Bar
59 - name: gatewayKeyResolver
60 args:
61 key-resolver: "#{@gatewayKeyResolver}"
62 #断路异常跳转
63 - name: Hystrix
64 args:
65 #网关异常或超时跳转到处理类
66 name: fallbackcmd
67 fallbackUri: forward:/fallbackController
68
69 #safe path
70 auth-skip:
71 instance-servers:
72 - CUSTOMER
73 - PRODUCT
74 api-urls:
75 #PRODUCT
76 - /pro
77 #CUSTOMER
78 - /cust
79
80 #gray-env
81 #...
82
83 #log
84 logging:
85 level:
86 org.yugh: INFO
87 org.springframework.cloud.gateway: INFO
88 org.springframework.http.server.reactive: INFO
89 org.springframework.web.reactive: INFO
90 reactor.ipc.netty: INFO
91
92 #reg
93 eureka:
94 instance:
95 prefer-ip-address: true
96 client:
97 serviceUrl:
98 defaultZone: http://localhost:8700/eureka/
99
100
101 ribbon:
102 eureka:
103 enabled: true
104 ReadTimeout: 120000
105 ConnectTimeout: 30000
106
107
108 #feign
109 feign:
110 hystrix:
111 enabled: false
112
113 #hystrix
114 hystrix:
115 command:
116 default:
117 execution:
118 isolation:
119 thread:
120 timeoutInMilliseconds: 20000
121
122 management:
123 endpoints:
124 web:
125 exposure:
126 include: '*'
127 base-path: /actuator
128 endpoint:
129 health:
130 show-details: ALWAYS
具体实现在这个类gatewayKeyResolver
(3)令牌桶IP限流,限制当前IP的请求配额
1 package org.yugh.gatewaynew.config;
2
3 import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
4 import org.springframework.stereotype.Component;
5 import org.springframework.web.server.ServerWebExchange;
6 import reactor.core.publisher.Mono;
7
8 /**
9 * //令牌桶IP限流
10 *
11 * @author 余根海
12 * @creation 2019-07-05 15:52
13 * @Copyright © 2019 yugenhai. All rights reserved.
14 */
15 @Component
16 public class GatewayKeyResolver implements KeyResolver {
17
18 @Override
19 public Mono<String> resolve(ServerWebExchange exchange) {
20 return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
21 }
22
23 }
(4)网关的白名单和黑名单配置
1 package org.yugh.gatewaynew.properties;
2
3
4 import lombok.Data;
5 import lombok.extern.slf4j.Slf4j;
6 import org.springframework.beans.factory.InitializingBean;
7 import org.springframework.boot.context.properties.ConfigurationProperties;
8 import org.springframework.context.annotation.Configuration;
9 import org.springframework.stereotype.Component;
10
11 import java.util.ArrayList;
12 import java.util.List;
13 import java.util.regex.Pattern;
14
15 /**
16 * //白名单和黑名单属性配置
17 *
18 * @author 余根海
19 * @creation 2019-07-05 15:52
20 * @Copyright © 2019 yugenhai. All rights reserved.
21 */
22 @Data
23 @Slf4j
24 @Component
25 @Configuration
26 @ConfigurationProperties(prefix = "auth-skip")
27 public class AuthSkipUrlsProperties implements InitializingBean {
28
29 private static final String NORMAL = "(\\w|\\d|-)+";
30 private List<Pattern> urlPatterns = new ArrayList(10);
31 private List<Pattern> serverPatterns = new ArrayList(10);
32 private List<String> instanceServers;
33 private List<String> apiUrls;
34
35 @Override
36 public void afterPropertiesSet() {
37 instanceServers.stream().map(d -> d.replace("*", NORMAL)).map(Pattern::compile).forEach(serverPatterns::add);
38 apiUrls.stream().map(s -> s.replace("*", NORMAL)).map(Pattern::compile).forEach(urlPatterns::add);
39 log.info("============> 配置服务器ID : {} , 白名单Url : {}", serverPatterns, urlPatterns);
40 }
41
42 }
(5)核心网关代码GatewayFilter
1 package org.yugh.gatewaynew.filter;
2
3 import lombok.extern.slf4j.Slf4j;
4 import org.springframework.beans.factory.annotation.Autowired;
5 import org.springframework.beans.factory.annotation.Qualifier;
6 import org.springframework.cloud.gateway.filter.GatewayFilterChain;
7 import org.springframework.cloud.gateway.filter.GlobalFilter;
8 import org.springframework.core.Ordered;
9 import org.springframework.core.io.buffer.DataBuffer;
10 import org.springframework.http.HttpStatus;
11 import org.springframework.http.MediaType;
12 import org.springframework.http.server.reactive.ServerHttpRequest;
13 import org.springframework.http.server.reactive.ServerHttpResponse;
14 import org.springframework.util.CollectionUtils;
15 import org.springframework.web.server.ServerWebExchange;
16 import org.yugh.gatewaynew.config.GatewayContext;
17 import org.yugh.gatewaynew.properties.AuthSkipUrlsProperties;
18 import org.yugh.globalauth.common.constants.Constant;
19 import org.yugh.globalauth.common.enums.ResultEnum;
20 import org.yugh.globalauth.pojo.dto.User;
21 import org.yugh.globalauth.service.AuthService;
22 import org.yugh.globalauth.util.ResultJson;
23 import reactor.core.publisher.Flux;
24 import reactor.core.publisher.Mono;
25
26 import java.nio.charset.StandardCharsets;
27 import java.util.concurrent.ExecutorService;
28 import java.util.regex.Matcher;
29
30 /**
31 * // 网关服务
32 *
33 * @author 余根海
34 * @creation 2019-07-09 10:52
35 * @Copyright © 2019 yugenhai. All rights reserved.
36 */
37 @Slf4j
38 public class GatewayFilter implements GlobalFilter, Ordered {
39
40 @Autowired
41 private AuthSkipUrlsProperties authSkipUrlsProperties;
42 @Autowired
43 @Qualifier(value = "gatewayQueueThreadPool")
44 private ExecutorService buildGatewayQueueThreadPool;
45 @Autowired
46 private AuthService authService;
47
48
49 @Override
50 public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
51 GatewayContext context = new GatewayContext();
52 ServerHttpRequest request = exchange.getRequest();
53 ServerHttpResponse response = exchange.getResponse();
54 response.getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF8);
55 log.info("当前会话ID : {}", request.getId());
56 //防止网关监控不到限流请求
57 if (blackServersCheck(context, exchange)) {
58 response.setStatusCode(HttpStatus.FORBIDDEN);
59 byte[] failureInfo = ResultJson.failure(ResultEnum.BLACK_SERVER_FOUND).toString().getBytes(StandardCharsets.UTF_8);
60 DataBuffer buffer = response.bufferFactory().wrap(failureInfo);
61 return response.writeWith(Flux.just(buffer));
62 }
63 //白名单
64 if (whiteListCheck(context, exchange)) {
65 authToken(context, request);
66 if (!context.isDoNext()) {
67 byte[] failureInfo = ResultJson.failure(ResultEnum.LOGIN_ERROR_GATEWAY, context.getRedirectUrl()).toString().getBytes(StandardCharsets.UTF_8);
68 DataBuffer buffer = response.bufferFactory().wrap(failureInfo);
69 response.setStatusCode(HttpStatus.UNAUTHORIZED);
70 return response.writeWith(Flux.just(buffer));
71 }
72 ServerHttpRequest mutateReq = exchange.getRequest().mutate().header(Constant.TOKEN, context.getSsoToken()).build();
73 ServerWebExchange mutableExchange = exchange.mutate().request(mutateReq).build();
74 log.info("当前会话转发成功 : {}", request.getId());
75 return chain.filter(mutableExchange);
76 } else {
77 //黑名单
78 response.setStatusCode(HttpStatus.FORBIDDEN);
79 byte[] failureInfo = ResultJson.failure(ResultEnum.WHITE_NOT_FOUND).toString().getBytes(StandardCharsets.UTF_8);
80 DataBuffer buffer = response.bufferFactory().wrap(failureInfo);
81 return response.writeWith(Flux.just(buffer));
82 }
83 }
84
85
86 @Override
87 public int getOrder() {
88 return Integer.MIN_VALUE;
89 }
90
91 /**
92 * 检查用户
93 *
94 * @param context
95 * @param request
96 * @return
97 * @author yugenhai
98 */
99 private void authToken(GatewayContext context, ServerHttpRequest request) {
100 try {
101 // boolean isLogin = authService.isLoginByReactive(request);
102 boolean isLogin = true;
103 if (isLogin) {
104 //User userDo = authService.getUserByReactive(request);
105 try {
106 // String ssoToken = authCookieUtils.getCookieByNameByReactive(request, Constant.TOKEN);
107 String ssoToken = "123";
108 context.setSsoToken(ssoToken);
109 } catch (Exception e) {
110 log.error("用户调用失败 : {}", e.getMessage());
111 context.setDoNext(false);
112 return;
113 }
114 } else {
115 unLogin(context, request);
116 }
117 } catch (Exception e) {
118 log.error("获取用户信息异常 :{}", e.getMessage());
119 context.setDoNext(false);
120 }
121 }
122
123
124 /**
125 * 网关同步用户
126 *
127 * @param userDto
128 */
129 public void synUser(User userDto) {
130 buildGatewayQueueThreadPool.execute(new Runnable() {
131 @Override
132 public void run() {
133 log.info("用户同步成功 : {}", "");
134 }
135 });
136
137 }
138
139
140 /**
141 * 视为不能登录
142 *
143 * @param context
144 * @param request
145 */
146 private void unLogin(GatewayContext context, ServerHttpRequest request) {
147 String loginUrl = getSsoUrl(request) + "?returnUrl=" + request.getURI();
148 context.setRedirectUrl(loginUrl);
149 context.setDoNext(false);
150 log.info("检查到该token对应的用户登录状态未登录 跳转到Login页面 : {} ", loginUrl);
151 }
152
153
154 /**
155 * 白名单
156 *
157 * @param context
158 * @param exchange
159 * @return
160 */
161 private boolean whiteListCheck(GatewayContext context, ServerWebExchange exchange) {
162 String url = exchange.getRequest().getURI().getPath();
163 boolean white = authSkipUrlsProperties.getUrlPatterns().stream()
164 .map(pattern -> pattern.matcher(url))
165 .anyMatch(Matcher::find);
166 if (white) {
167 context.setPath(url);
168 return true;
169 }
170 return false;
171 }
172
173
174 /**
175 * 黑名单
176 *
177 * @param context
178 * @param exchange
179 * @return
180 */
181 private boolean blackServersCheck(GatewayContext context, ServerWebExchange exchange) {
182 String instanceId = exchange.getRequest().getURI().getPath().substring(1, exchange.getRequest().getURI().getPath().indexOf('/', 1));
183 if (!CollectionUtils.isEmpty(authSkipUrlsProperties.getInstanceServers())) {
184 boolean black = authSkipUrlsProperties.getServerPatterns().stream()
185 .map(pattern -> pattern.matcher(instanceId))
186 .anyMatch(Matcher::find);
187 if (black) {
188 context.setBlack(true);
189 return true;
190 }
191 }
192 return false;
193 }
194
195
196 /**
197 * @param request
198 * @return
199 */
200 private String getSsoUrl(ServerHttpRequest request) {
201 return request.getPath().value();
202 }
203
204 }
在 private void authToken(GatewayContext context, ServerHttpRequest request) { 这个方法里可以自定义做验证。
结束语:
我实现了一遍两种网关,发现还是官网的文档最靠谱,也是能落地到项目中的。如果你需要源码的请到 我的Github 去clone,如果帮助到了你,还请点个 star。