前言:

最近开发了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,文章结尾会提供此组件的实现)
 
最全面的改造Zuul网关为Spring Cloud Gateway(包含Zuul核心实现和Spring Cloud Gateway核心实现)-LMLPHP
 
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     #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转发。

重点在:

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,项目我会一直更新。

 

如果转载请写上出处!感谢阅读!

08-06 13:05