最全面的改造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 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
50000+
5万行代码练就真实本领
17年
创办于2008年老牌培训机构
1000+
合作企业
98%
就业率

联系我们

电话咨询

0532-85025005

扫码添加微信