前言:
最近开发了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
2
3
4 org.springframework.boot
5 spring-boot-starter-data-redis
6
7
8
9 org.springframework.cloud
10 spring-cloud-starter-netflix-eureka-client
11
12
13
14 org.springframework.cloud
15 spring-cloud-starter-netflix-zuul
16
17
18
19 org.springframework.cloud
20 spring-cloud-starter-netflix-hystrix
21
22
23
24 org.springframework.cloud
25 spring-cloud-starter-netflix-ribbon
26
27
28
29 org.springframework.cloud
30 spring-cloud-starter-openfeign
31
32
33
34 org.springframework.boot
35 spring-boot-starter-actuator
36
复制代码
(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
patterns = new ArrayList<>();
31 private Map apiUrlMap;
32 private List 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> zuulMap = zuulPropConfig.getRoutes().entrySet().iterator();
131 while(zuulMap.hasNext()){
132 Map.Entry 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.getReques