最全面的改造Zuul閘道器為Spring Cloud Gateway(包含Zuul核心實現和Spring Cloud Gateway核心實現)
前言:
最近開發了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: 白名單連線,每個微服務的請求入口地址,包含即通過。
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,專案我會一直更新。
如果轉載請寫上出處!感謝閱讀!