最全面的改造Zuul閘道器為Spring Cloud Gateway(包含Zuul核心實現和Spring Cloud Gateway核心實現)

yugenhai發表於2019-08-05

前言:

最近開發了Zuul閘道器的實現和Spring Cloud Gateway實現,對比Spring Cloud Gateway發現後者效能好支援場景也豐富。在高併發或者複雜的分散式下,後者限流和自定義攔截也很棒。

 

提示:

本文主要列出本人開發的Zuul閘道器核心程式碼以及Spring Cloud Gateway核心程式碼實現。因為本人技術有限,主要是參照了 Spring Cloud Gateway 如有不足之處還請見諒並留言指出。

 

1:為什麼要做閘道器

(1)閘道器層對外部和內部進行了隔離,保障了後臺服務的安全性。
(2)對外訪問控制由網路層面轉換成了運維層面,減少變更的流程和錯誤成本。
(3)減少客戶端與服務的耦合,服務可以獨立執行,並通過閘道器層來做對映。
(4)通過閘道器層聚合,減少外部訪問的頻次,提升訪問效率。
(5)節約後端服務開發成本,減少上線風險。
(6)為服務熔斷,灰度釋出,線上測試提供簡單方案。
(7)便於進行應用層面的擴充套件。 
 
相信在尋找相關資料的夥伴應該都知道,在微服務環境下,要做到一個比較健壯的流量入口還是很重要的,需要考慮的問題也比較複雜和眾多。
 
2:閘道器和鑑權基本實現架構(圖中包含了auth元件,或SSO,文章結尾會提供此元件的實現)
 
 
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 的內容

給個提示,在原來的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: 白名單連線,每個微服務的請求入口地址,包含即通過。

 
(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) { 這個方法裡可以自定義做驗證。

 

結束語:

我實現了一遍兩種閘道器,發現還是官網的文件最靠譜,也是能落地到專案中的。如果你需要原始碼的請到 餘根海的部落格 去clone,如果幫助到了你,還請點個 star,專案我會一直更新。

 

如果轉載請寫上出處!感謝閱讀!

相關文章