最全面的改造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: 白名单连接,每个微服务的请求入口地址,包含即通过。

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

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

原文地址:https://www.cnblogs.com/KuJo/p/11306361.html

时间: 2024-08-02 15:53:45

最全面的改造Zuul网关为Spring Cloud Gateway(包含Zuul核心实现和Spring Cloud Gateway核心实现)的相关文章

spring cloud深入学习(十二)-----Spring Cloud Zuul网关 Filter、熔断、重试、高可用的使用方式

Zuul的核心 Filter是Zuul的核心,用来实现对外服务的控制.Filter的生命周期有4个,分别是“PRE”.“ROUTING”.“POST”.“ERROR”,整个生命周期可以用下图来表示. Zuul大部分功能都是通过过滤器来实现的,这些过滤器类型对应于请求的典型生命周期. PRE: 这种过滤器在请求被路由之前调用.我们可利用这种过滤器实现身份验证.在集群中选择请求的微服务.记录调试信息等. ROUTING:这种过滤器将请求路由到微服务.这种过滤器用于构建发送给微服务的请求,并使用Apa

spring cloud 学习(6) - zuul 微服务网关

微服务架构体系中,通常一个业务系统会有很多的微服务,比如:OrderService.ProductService.UserService...,为了让调用更简单,一般会在这些服务前端再封装一层,类似下面这样: 前面这一层俗称为“网关层”,其存在意义在于,将"1对N"问题 转换成了"1对1”问题,同时在请求到达真正的微服务之前,可以做一些预处理,比如:来源合法性检测,权限校验,反爬虫之类... 传统方式下,最土的办法,网关层可以人肉封装,类似以下示例代码: LoginResul

Spring Cloud(Dalston.SR5)--Zuul 网关-Hystrix 回退

当我们对网关进行配置让其调用集群的服务时,将会执行 Ribbon 路由过滤器,该过滤器在进行转发时会封装为一个 Hystrix 命令予以执行,Hystrix 命令具有容错的功能,如果"源服务"出现问题(例如超时),那边所执行的 Hystrix 命令将会触发回退,我们需要实现 org.springframework.cloud.netflix.zuul.filters.route.ZuulFallbackProvider 接口,该接口主要需要实现 getRoute 方法 .fallbac

Spring Cloud(Dalston.SR5)--Zuul 网关-微服务集群

通过 url 映射的方式来实现 zuul 的转发有局限性,比如每增加一个服务就需要配置一条内容,另外后端的服务如果是动态来提供,就不能采用这种方案来配置了.实际上在实现微服务架构时,服务名与服务实例地址的关系在 eureka server 中已经存在了,所以只需要将Zuul注册到 eureka server上去发现其他服务,就可以实现对 serviceId 的映射,并且启用了 eureka server 同时也会启用 ribbon 对服务进行负载均衡调用,加入 Zuul 到微服务集群架构图如下:

API网关性能比较:NGINX vs. ZUUL vs. Spring Cloud Gateway vs. Linkerd(转)

前几天拜读了 OpsGenie 公司(一家致力于 Dev & Ops 的公司)的资深工程师 Turgay ?elik 博士写的一篇文章(链接在文末),文中介绍了他们最初也是采用 Nginx 作为单体应用的网关,后来接触到微服务架构后开始逐渐采用了其他组件. 我对于所做的工作或者感兴趣的技术,喜欢刨根问底,所以当读一篇文章时发现没有看到我想要看到的设计思想,我就会四处搜集资料,此外这篇文章涉及了我正在捣鼓的 Spring Cloud,所以我就决定写一篇文章,争取能从设计思路上解释为什么会有这样的性

Spring Cloud之搭建动态Zuul网关路由转发

传统方式将路由规则配置在配置文件中,如果路由规则发生了改变,需要重启服务器.这时候我们结合上节课内容整合SpringCloud Config分布式配置中心,实现动态路由规则. 将yml的内容粘贴到码云上: ###注册 中心 eureka: client: serviceUrl: defaultZone: http://localhost:8100/eureka/ server: ##api网关端口号 port: 80 ###网关名称 spring: ##网关服务名称 application: n

Spring Cloud微服务Ribbon负载均衡/Zuul网关使用

客户端负载均衡,当服务节点出现问题时进行调节或是在正常情况下进行 服务调度.所谓的负载均衡,就是当服务提供的数量和调用方对服务进行 取舍的调节问题,在spring cloud中是通过Ribbon来解决的.还有另外一 种途径是通过服务端的负载均衡Nginx来解决.Ribbon是客户端的负载均 衡,通过Eureka来获取所有的服务的数量,客户端来调用服务时,Ribbon 通过一系列的算法来进行调节,选择哪个服务来进行调用.默认无需对 Ribbon进行配置,它会采用默认的算法进行负载均衡.可以对负载均

创建swagger的springboot-stater,并在spring cloud zuul网关中引入

Swagger 是一款RESTFUL接口的.基于YAML.JSON语言的文档在线自动生成.代码自动生成的工具. 通过在controller中添加注解,即可轻易实现代码文档化. Swagger提供ui界面,方便查看接口说明和测试接口功能. swagger-github 本文主要讲解如何创建一个swagger 的springboot starter项目,只要在其他服务中引入该starter.并添加相关注解,即可完成接口文档化. 并讲解了如何在spring cloud zuul网关中引入swagger

Spring Cloud系列-Zuul网关集成JWT身份验证

前言 这两三年项目中一直在使用比较流行的spring cloud框架,也算有一定积累,打算有时间就整理一些干货与大家分享. 本次分享zuul网关集成jwt身份验证 业务背景 项目开发少不了身份认证,jwt作为当下比较流行的身份认证方式之一主要的特点是无状态,把信息放在客户端,服务器端不需要保存session,适合分布式系统使用. 把jwt集成在网关的好处是业务工程不需要关心身份验证,专注业务逻辑(网关可验证token后,把解析出来的身份信息如userId,放在请求头传递给业务工程). 顺便分享下