前言:
最近開發了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
30
31 #白名單
32 auth-props:
33 #accessIp: 127.0.0.1
34 #accessToken: admin
35 #authLevel: dev
36 #服務
37 api-urlMap: {
38 product: 1&2,
39 customer: 1&1
40 }
41 #移除url同時移除服務
42 exclude-urls:
43 - /pro
44 - /cust
45
46
47 #斷路時間
48 hystrix:
49 command:
50 default:
51 execution:
52 isolation:
53 thread:
54 timeoutInMilliseconds: 300000
55
56 #ribbon
57 ribbon:
58 ReadTimeout: 15000
59 ConnectTimeout: 15000
60 SocketTimeout: 15000
61 eager-load:
62 enabled: true
63 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) { 這個方法裡可以自定義做驗證。
結束語:
我實現了一遍兩種閘道器,發現還是官網的文件最靠譜,也是能落地到專案中的。如果你需要原始碼的請到 餘根海的部落格 去clone,如果幫助到了你,還請點個 star,專案我會一直更新。
如果轉載請寫上出處!感謝閱讀!