前言:html
最近開發了Zuul網關的實現和Spring Cloud Gateway實現,對比Spring Cloud Gateway發現後者性能好支持場景也豐富。在高併發或者複雜的分佈式下,後者限流和自定義攔截也很棒。java
提示:react
本文主要列出本人開發的Zuul網關核心代碼以及Spring Cloud Gateway核心代碼實現。由於本人技術有限,主要是參照了 Spring Cloud Gateway 若有不足之處還請見諒並留言指出。git
1:爲何要作網關github
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 的內容web
給個提示,在原來的starter-web中 yml的 context-path是不須要用的,微服務中只須要用application-name去註冊中心找實例名便可,何況webflux後context-path已經不存在了。redis
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轉發。spring
重點在:json
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。