最全面的改造Zuul網關爲Spring Cloud Gateway(包含Zuul核心實現和Spring Cloud Gateway核心實現)

前言:html

最近開發了Zuul網關的實現和Spring Cloud Gateway實現,對比Spring Cloud Gateway發現後者性能好支持場景也豐富。在高併發或者複雜的分佈式下,後者限流和自定義攔截也很棒。java

 

提示:react

本文主要列出本人開發的Zuul網關核心代碼以及Spring Cloud Gateway核心代碼實現。由於本人技術有限,主要是參照了 Spring Cloud Gateway 若有不足之處還請見諒並留言指出。git

 

1:爲何要作網關github

(1)網關層對外部和內部進行了隔離,保障了後臺服務的安全性。
(2)對外訪問控制由網絡層面轉換成了運維層面,減小變動的流程和錯誤成本。
(3)減小客戶端與服務的耦合,服務能夠獨立運行,並經過網關層來作映射。
(4)經過網關層聚合,減小外部訪問的頻次,提高訪問效率。
(5)節約後端服務開發成本,減小上線風險。
(6)爲服務熔斷,灰度發佈,線上測試提供簡單方案。
(7)便於進行應用層面的擴展。 
 
相信在尋找相關資料的夥伴應該都知道,在微服務環境下,要作到一個比較健壯的流量入口仍是很重要的,須要考慮的問題也比較複雜和衆多。
 
2:網關和鑑權基本實現架構(圖中包含了auth組件,或SSO,文章結尾會提供此組件的實現)
 
 
3:Zuul的實現
 
(1)第一代的zuul使用的是netflix開發的,在pom引用上都是用的原來的。
複製代碼
 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: 白名單鏈接,每一個微服務的請求入口地址,包含即經過。後端

 
(3)上面提到白名單,那須要初始化白名單
複製代碼
 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)第二代的Gateway則是由Spring Cloud開發,並且用了最新的Spring5.0和響應式Reactor以及最新的Webflux等等,好比原來的阻塞式請求如今變成了異步非阻塞式。
   那麼在pom上就變了,變得和原來的starer-web也不兼容了。
複製代碼
 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
複製代碼

 

網關限流用的 spring-boot-starter-data-redis-reactive 作令牌桶IP限流。

具體實如今這個類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。

原文地址:https://www.cnblogs.com/KuJo/p/11306361.html
相關文章
相關標籤/搜索