前言:
最近开发了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 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <!-- zuul网关最基本要用到的 --> <!-- 封装原来的jedis,用处是在网关里来放token到redis或者调redis来验证当前是否有效,或者说直接用redis负载--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- 客户端注册eureka使用的,微服务必备 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <!-- zuul --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-zuul</artifactId> </dependency> <!-- 熔断支持 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency> <!--负载均衡 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId> </dependency> <!-- 调用feign --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <!-- 健康 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
(2)修改application-dev.yml 的内容
给个提示,在原来的starter-web中 yml的 context-path是不需要用的,微服务中只需要用application-name去注册中心找实例名即可,况且webflux后context-path已经不存在了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 spring: application: name: gateway #eureka-gateway-monitor-config 每个端口+1 server: port: 8702 #eureka注册配置 eureka: instance: #使用IP注册 prefer-ip-address: true ##续约更新时间间隔设置5秒,m默认30s lease-renewal-interval-in-seconds: 30 ##续约到期时间10秒,默认是90秒 lease-expiration-duration-in-seconds: 90 client: serviceUrl: defaultZone: http://localhost:8700/eureka/ # route connection zuul: host: #单个服务最大请求 max-per-route-connections: 20 #网关最大连接数 max-total-connections: 200 #routes to serviceId routes: api-product.path: /api/product/** api-product.serviceId: product api-customer.path: /api/customer/** api-customer.serviceId: customer #移除url同时移除服务 auth-props: #accessIp: 127.0.0.1 #accessToken: admin #authLevel: dev #服务 api-urlMap: { product: 1&2, customer: 1&1 } #移除url同时移除服务 exclude-urls: - /pro - /cust #断路时间 hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 300000 #ribbon ribbon: ReadTimeout: 15000 ConnectTimeout: 15000 SocketTimeout: 15000 eager-load: enabled: true clients: product, customer
如果仅仅是转发,那很简单,如果要做好场景,则需要添加白名单和黑名单,在zuul里只需要加白名单即可,存在链接或者实例名才能通过filter转发。
重点在:
api-urlMap: 是实例名,如果链接不存在才会去校验,因为端口+链接可以访问,如果加实例名一起也能访问,防止恶意带实例名攻击或者抓包请求后去猜链接后缀来攻击。 exclude-urls: 白名单连接,每个微服务的请求入口地址,包含即通过。
(3)上面提到白名单,那需要初始化白名单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 package org.yugh.gateway.config; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.InitializingBean; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.regex.Pattern; /** * //路由拦截配置 * * @author: 余根海 * @creation: 2019-07-02 19:43 * @Copyright © 2019 yugenhai. All rights reserved. */ @Data @Slf4j @Component @Configuration @ConfigurationProperties(prefix = "auth-props") public class ZuulPropConfig implements InitializingBean { private static final String normal = "(\\w|\\d|-)+"; private List<Pattern> patterns = new ArrayList<>(); private Map<String, String> apiUrlMap; private List<String> excludeUrls; private String accessToken; private String accessIp; private String authLevel; @Override public void afterPropertiesSet() throws Exception { excludeUrls.stream().map(s -> s.replace("*", normal)).map(Pattern::compile).forEach(patterns::add); log.info("============> 配置的白名单Url:{}", patterns); } }
(4)核心代码zuulFilter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 package org.yugh.gateway.filter; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import org.yugh.gateway.common.constants.Constant; import org.yugh.gateway.common.enums.DeployEnum; import org.yugh.gateway.common.enums.HttpStatusEnum; import org.yugh.gateway.common.enums.ResultEnum; import org.yugh.gateway.config.RedisClient; import org.yugh.gateway.config.ZuulPropConfig; import org.yugh.gateway.util.ResultJson; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.function.Function; import java.util.regex.Matcher; /** * //路由拦截转发请求 * * @author: 余根海 * @creation: 2019-06-26 17:50 * @Copyright © 2019 yugenhai. All rights reserved. */ @Slf4j public class PreAuthFilter extends ZuulFilter { @Value("${spring.profiles.active}") private String activeType; @Autowired private ZuulPropConfig zuulPropConfig; @Autowired private RedisClient redisClient; @Override public String filterType() { return "pre"; } @Override public int filterOrder() { return 0; } /** * 部署级别可调控 * * @return * @author yugenhai * @creation: 2019-06-26 17:50 */ @Override public boolean shouldFilter() { RequestContext context = RequestContext.getCurrentContext(); HttpServletRequest request = context.getRequest(); if (activeType.equals(DeployEnum.DEV.getType())) { log.info("请求地址 : {} 当前环境 : {} ", request.getServletPath(), DeployEnum.DEV.getType()); return true; } else if (activeType.equals(DeployEnum.TEST.getType())) { log.info("请求地址 : {} 当前环境 : {} ", request.getServletPath(), DeployEnum.TEST.getType()); return true; } else if (activeType.equals(DeployEnum.PROD.getType())) { log.info("请求地址 : {} 当前环境 : {} ", request.getServletPath(), DeployEnum.PROD.getType()); return true; } return true; } /** * 路由拦截转发 * * @return * @author yugenhai * @creation: 2019-06-26 17:50 */ @Override public Object run() { RequestContext context = RequestContext.getCurrentContext(); HttpServletRequest request = context.getRequest(); String requestMethod = context.getRequest().getMethod(); //判断请求方式 if (Constant.OPTIONS.equals(requestMethod)) { log.info("请求的跨域的地址 : {} 跨域的方法", request.getServletPath(), requestMethod); assemblyCross(context); context.setResponseStatusCode(HttpStatusEnum.OK.code()); context.setSendZuulResponse(false); return null; } //转发信息共享 其他服务不要依赖MVC拦截器,或重写拦截器 if (isIgnore(request, this::exclude, this::checkLength)) { String token = getCookieBySso(request); if(!StringUtils.isEmpty(token)){ //context.addZuulRequestHeader(JwtUtil.HEADER_AUTH, token); } log.info("请求白名单地址 : {} ", request.getServletPath()); return null; } String serverName = request.getServletPath().substring(1, request.getServletPath().indexOf('/', 1)); String authUserType = zuulPropConfig.getApiUrlMap().get(serverName); log.info("实例服务名: {} 对应用户类型: {}", serverName, authUserType); if (!StringUtils.isEmpty(authUserType)) { //用户是否合法和登录 authToken(context); } else { //下线前删除配置的实例名 log.info("实例服务: {} 不允许访问", serverName); unauthorized(context, HttpStatusEnum.FORBIDDEN.code(), "请求的服务已经作废,不可访问"); } return null; /******************************以下代码可能会复用,勿删,若使用Gateway整个路由项目将不使用 add by - yugenhai 2019-0704********************************************/ /*String readUrl = request.getServletPath().substring(1, request.getServletPath().indexOf('/', 1)); try { if (request.getServletPath().length() <= Constant.PATH_LENGTH || zuulPropConfig.getRoutes().size() == 0) { throw new Exception(); } Iterator<Map.Entry<String,String>> zuulMap = zuulPropConfig.getRoutes().entrySet().iterator(); while(zuulMap.hasNext()){ Map.Entry<String, String> entry = zuulMap.next(); String routeValue = entry.getValue(); if(routeValue.startsWith(Constant.ZUUL_PREFIX)){ routeValue = routeValue.substring(1, routeValue.indexOf('/', 1)); } if(routeValue.contains(readUrl)){ log.info("请求白名单地址 : {} 请求跳过的真实地址 :{} ", routeValue, request.getServletPath()); return null; } } log.info("即将请求登录 : {} 实例名 : {} ", request.getServletPath(), readUrl); authToken(context); return null; } catch (Exception e) { log.info("gateway路由器请求异常 :{} 请求被拒绝 ", e.getMessage()); assemblyCross(context); context.set("isSuccess", false); context.setSendZuulResponse(false); context.setResponseStatusCode(HttpStatusEnum.OK.code()); context.getResponse().setContentType("application/json;charset=UTF-8"); context.setResponseBody(JsonUtils.toJson(JsonResult.buildErrorResult(HttpStatusEnum.UNAUTHORIZED.code(),"Url Error, Please Check It"))); return null; } */ } /** * 检查用户 * * @param context * @return * @author yugenhai * @creation: 2019-06-26 17:50 */ private Object authToken(RequestContext context) { HttpServletRequest request = context.getRequest(); HttpServletResponse response = context.getResponse(); /*boolean isLogin = sessionManager.isLogined(request, response); //用户存在 if (isLogin) { try { User user = sessionManager.getUser(request); log.info("用户存在 : {} ", JsonUtils.toJson(user)); // String token = userAuthUtil.generateToken(user.getNo(), user.getUserName(), user.getRealName()); log.info("根据用户生成的Token :{}", token); //转发信息共享 // context.addZuulRequestHeader(JwtUtil.HEADER_AUTH, token); //缓存 后期所有服务都判断 redisClient.set(user.getNo(), token, 20 * 60L); //冗余一份 userService.syncUser(user); } catch (Exception e) { log.error("调用SSO获取用户信息异常 :{}", e.getMessage()); } } else { //根据该token查询该用户不存在 unLogin(request, context); }*/ return null; } /** * 未登录不路由 * * @param request */ private void unLogin(HttpServletRequest request, RequestContext context) { String requestURL = request.getRequestURL().toString(); String loginUrl = getSsoUrl(request) + "?returnUrl=" + requestURL; //Map map = new HashMap(2); //map.put("redirctUrl", loginUrl); log.info("检查到该token对应的用户登录状态未登录 跳转到Login页面 : {} ", loginUrl); assemblyCross(context); context.getResponse().setContentType("application/json;charset=UTF-8"); context.set("isSuccess", false); context.setSendZuulResponse(false); //context.setResponseBody(ResultJson.failure(map, "This User Not Found, Please Check Token").toString()); context.setResponseStatusCode(HttpStatusEnum.OK.code()); } /** * 判断是否忽略对请求的校验 * @param request * @param functions * @return */ private boolean isIgnore(HttpServletRequest request, Function<HttpServletRequest, Boolean>... functions) { return Arrays.stream(functions).anyMatch(f -> f.apply(request)); } /** * 判断是否存在地址 * @param request * @return */ private boolean exclude(HttpServletRequest request) { String servletPath = request.getServletPath(); if (!CollectionUtils.isEmpty(zuulPropConfig.getExcludeUrls())) { return zuulPropConfig.getPatterns().stream() .map(pattern -> pattern.matcher(servletPath)) .anyMatch(Matcher::find); } return false; } /** * 校验请求连接是否合法 * @param request * @return */ private boolean checkLength(HttpServletRequest request) { return request.getServletPath().length() <= Constant.PATH_LENGTH || CollectionUtils.isEmpty(zuulPropConfig.getApiUrlMap()); } /** * 会话存在则跨域发送 * @param request * @return */ private String getCookieBySso(HttpServletRequest request){ Cookie cookie = this.getCookieByName(request, ""); return cookie != null ? cookie.getValue() : null; } /** * 不路由直接返回 * @param ctx * @param code * @param msg */ private void unauthorized(RequestContext ctx, int code, String msg) { assemblyCross(ctx); ctx.getResponse().setContentType("application/json;charset=UTF-8"); ctx.setSendZuulResponse(false); ctx.setResponseBody(ResultJson.failure(ResultEnum.UNAUTHORIZED, msg).toString()); ctx.set("isSuccess", false); ctx.setResponseStatusCode(HttpStatusEnum.OK.code()); } /** * 获取会话里的token * @param request * @param name * @return */ private Cookie getCookieByName(HttpServletRequest request, String name) { Map<String, Cookie> cookieMap = new HashMap(16); Cookie[] cookies = request.getCookies(); if (!StringUtils.isEmpty(cookies)) { Cookie[] c1 = cookies; int length = cookies.length; for(int i = 0; i < length; ++i) { Cookie cookie = c1[i]; cookieMap.put(cookie.getName(), cookie); } }else { return null; } if (cookieMap.containsKey(name)) { Cookie cookie = cookieMap.get(name); return cookie; } return null; } /** * 重定向前缀拼接 * * @param request * @return */ private String getSsoUrl(HttpServletRequest request) { String serverName = request.getServerName(); if (StringUtils.isEmpty(serverName)) { return "https://github.com/yugenhai108"; } return "https://github.com/yugenhai108"; } /** * 拼装跨域处理 */ private void assemblyCross(RequestContext ctx) { HttpServletResponse response = ctx.getResponse(); response.setHeader("Access-Control-Allow-Origin", "*"); response.setHeader("Access-Control-Allow-Headers", ctx.getRequest().getHeader("Access-Control-Request-Headers")); response.setHeader("Access-Control-Allow-Methods", "*"); } }
在 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 <dependency> <groupId>org.yugh</groupId> <artifactId>global-auth</artifactId> <version>0.0.1-SNAPSHOT</version> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </exclusion> </exclusions> </dependency> <!-- gateway --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <!-- feign --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> </dependency> <!-- redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis-reactive</artifactId> </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>23.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
(2)修改application-dev.yml 的内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 server: port: 8706 #setting spring: application: name: gateway-new #redis redis: host: localhost port: 6379 database: 0 timeout: 5000 #遇到相同名字,允许覆盖 main: allow-bean-definition-overriding: true #gateway cloud: gateway: #注册中心服务发现 discovery: locator: #开启通过服务中心的自动根据 serviceId 创建路由的功能 enabled: true routes: #服务1 - id: CompositeDiscoveryClient_CUSTOMER uri: lb://CUSTOMER order: 1 predicates: # 跳过自定义是直接带实例名 必须是大写 同样限流拦截失效 - Path= /api/customer/** filters: - StripPrefix=2 - AddResponseHeader=X-Response-Default-Foo, Default-Bar - name: RequestRateLimiter args: key-resolver: "#{@gatewayKeyResolver}" #限额配置 redis-rate-limiter.replenishRate: 1 redis-rate-limiter.burstCapacity: 1 #用户微服务 - id: CompositeDiscoveryClient_PRODUCT uri: lb://PRODUCT order: 0 predicates: - Path= /api/product/** filters: - StripPrefix=2 - AddResponseHeader=X-Response-Default-Foo, Default-Bar - name: RequestRateLimiter args: key-resolver: "#{@gatewayKeyResolver}" #限额配置 redis-rate-limiter.replenishRate: 1 redis-rate-limiter.burstCapacity: 1 #请求路径选择自定义会进入限流器 default-filters: - AddResponseHeader=X-Response-Default-Foo, Default-Bar - name: gatewayKeyResolver args: key-resolver: "#{@gatewayKeyResolver}" #断路异常跳转 - name: Hystrix args: #网关异常或超时跳转到处理类 name: fallbackcmd fallbackUri: forward:/fallbackController #safe path auth-skip: instance-servers: - CUSTOMER - PRODUCT api-urls: #PRODUCT - /pro #CUSTOMER - /cust #gray-env #... #log logging: level: org.yugh: INFO org.springframework.cloud.gateway: INFO org.springframework.http.server.reactive: INFO org.springframework.web.reactive: INFO reactor.ipc.netty: INFO #reg eureka: instance: prefer-ip-address: true client: serviceUrl: defaultZone: http://localhost:8700/eureka/ ribbon: eureka: enabled: true ReadTimeout: 120000 ConnectTimeout: 30000 #feign feign: hystrix: enabled: false #hystrix hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 20000 management: endpoints: web: exposure: include: '*' base-path: /actuator endpoint: health: show-details: ALWAYS
网关限流用的 spring-boot-starter-data-redis-reactive 做令牌桶IP限流。
具体实现在这个类gatewayKeyResolver
(3)令牌桶IP限流,限制当前IP的请求配额
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package org.yugh.gatewaynew.config; import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; /** * //令牌桶IP限流 * * @author 余根海 * @creation 2019-07-05 15:52 * @Copyright © 2019 yugenhai. All rights reserved. */ @Component public class GatewayKeyResolver implements KeyResolver { @Override public Mono<String> resolve(ServerWebExchange exchange) { return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()); } }
(4)网关的白名单和黑名单配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 package org.yugh.gatewaynew.properties; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.InitializingBean; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.List; import java.util.regex.Pattern; /** * //白名单和黑名单属性配置 * * @author 余根海 * @creation 2019-07-05 15:52 * @Copyright © 2019 yugenhai. All rights reserved. */ @Data @Slf4j @Component @Configuration @ConfigurationProperties(prefix = "auth-skip") public class AuthSkipUrlsProperties implements InitializingBean { private static final String NORMAL = "(\\w|\\d|-)+"; private List<Pattern> urlPatterns = new ArrayList(10); private List<Pattern> serverPatterns = new ArrayList(10); private List<String> instanceServers; private List<String> apiUrls; @Override public void afterPropertiesSet() { instanceServers.stream().map(d -> d.replace("*", NORMAL)).map(Pattern::compile).forEach(serverPatterns::add); apiUrls.stream().map(s -> s.replace("*", NORMAL)).map(Pattern::compile).forEach(urlPatterns::add); log.info("============> 配置服务器ID : {} , 白名单Url : {}", serverPatterns, urlPatterns); } }
(5)核心网关代码GatewayFilter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 package org.yugh.gatewaynew.filter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.CollectionUtils; import org.springframework.web.server.ServerWebExchange; import org.yugh.gatewaynew.config.GatewayContext; import org.yugh.gatewaynew.properties.AuthSkipUrlsProperties; import org.yugh.globalauth.common.constants.Constant; import org.yugh.globalauth.common.enums.ResultEnum; import org.yugh.globalauth.pojo.dto.User; import org.yugh.globalauth.service.AuthService; import org.yugh.globalauth.util.ResultJson; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.nio.charset.StandardCharsets; import java.util.concurrent.ExecutorService; import java.util.regex.Matcher; /** * // 网关服务 * * @author 余根海 * @creation 2019-07-09 10:52 * @Copyright © 2019 yugenhai. All rights reserved. */ @Slf4j public class GatewayFilter implements GlobalFilter, Ordered { @Autowired private AuthSkipUrlsProperties authSkipUrlsProperties; @Autowired @Qualifier(value = "gatewayQueueThreadPool") private ExecutorService buildGatewayQueueThreadPool; @Autowired private AuthService authService; @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { GatewayContext context = new GatewayContext(); ServerHttpRequest request = exchange.getRequest(); ServerHttpResponse response = exchange.getResponse(); response.getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF8); log.info("当前会话ID : {}", request.getId()); //防止网关监控不到限流请求 if (blackServersCheck(context, exchange)) { response.setStatusCode(HttpStatus.FORBIDDEN); byte[] failureInfo = ResultJson.failure(ResultEnum.BLACK_SERVER_FOUND).toString().getBytes(StandardCharsets.UTF_8); DataBuffer buffer = response.bufferFactory().wrap(failureInfo); return response.writeWith(Flux.just(buffer)); } //白名单 if (whiteListCheck(context, exchange)) { authToken(context, request); if (!context.isDoNext()) { byte[] failureInfo = ResultJson.failure(ResultEnum.LOGIN_ERROR_GATEWAY, context.getRedirectUrl()).toString().getBytes(StandardCharsets.UTF_8); DataBuffer buffer = response.bufferFactory().wrap(failureInfo); response.setStatusCode(HttpStatus.UNAUTHORIZED); return response.writeWith(Flux.just(buffer)); } ServerHttpRequest mutateReq = exchange.getRequest().mutate().header(Constant.TOKEN, context.getSsoToken()).build(); ServerWebExchange mutableExchange = exchange.mutate().request(mutateReq).build(); log.info("当前会话转发成功 : {}", request.getId()); return chain.filter(mutableExchange); } else { //黑名单 response.setStatusCode(HttpStatus.FORBIDDEN); byte[] failureInfo = ResultJson.failure(ResultEnum.WHITE_NOT_FOUND).toString().getBytes(StandardCharsets.UTF_8); DataBuffer buffer = response.bufferFactory().wrap(failureInfo); return response.writeWith(Flux.just(buffer)); } } @Override public int getOrder() { return Integer.MIN_VALUE; } /** * 检查用户 * * @param context * @param request * @return * @author yugenhai */ private void authToken(GatewayContext context, ServerHttpRequest request) { try { // boolean isLogin = authService.isLoginByReactive(request); boolean isLogin = true; if (isLogin) { //User userDo = authService.getUserByReactive(request); try { // String ssoToken = authCookieUtils.getCookieByNameByReactive(request, Constant.TOKEN); String ssoToken = "123"; context.setSsoToken(ssoToken); } catch (Exception e) { log.error("用户调用失败 : {}", e.getMessage()); context.setDoNext(false); return; } } else { unLogin(context, request); } } catch (Exception e) { log.error("获取用户信息异常 :{}", e.getMessage()); context.setDoNext(false); } } /** * 网关同步用户 * * @param userDto */ public void synUser(User userDto) { buildGatewayQueueThreadPool.execute(new Runnable() { @Override public void run() { log.info("用户同步成功 : {}", ""); } }); } /** * 视为不能登录 * * @param context * @param request */ private void unLogin(GatewayContext context, ServerHttpRequest request) { String loginUrl = getSsoUrl(request) + "?returnUrl=" + request.getURI(); context.setRedirectUrl(loginUrl); context.setDoNext(false); log.info("检查到该token对应的用户登录状态未登录 跳转到Login页面 : {} ", loginUrl); } /** * 白名单 * * @param context * @param exchange * @return */ private boolean whiteListCheck(GatewayContext context, ServerWebExchange exchange) { String url = exchange.getRequest().getURI().getPath(); boolean white = authSkipUrlsProperties.getUrlPatterns().stream() .map(pattern -> pattern.matcher(url)) .anyMatch(Matcher::find); if (white) { context.setPath(url); return true; } return false; } /** * 黑名单 * * @param context * @param exchange * @return */ private boolean blackServersCheck(GatewayContext context, ServerWebExchange exchange) { String instanceId = exchange.getRequest().getURI().getPath().substring(1, exchange.getRequest().getURI().getPath().indexOf('/', 1)); if (!CollectionUtils.isEmpty(authSkipUrlsProperties.getInstanceServers())) { boolean black = authSkipUrlsProperties.getServerPatterns().stream() .map(pattern -> pattern.matcher(instanceId)) .anyMatch(Matcher::find); if (black) { context.setBlack(true); return true; } } return false; } /** * @param request * @return */ private String getSsoUrl(ServerHttpRequest request) { return request.getPath().value(); } }
在 private void authToken(GatewayContext context, ServerHttpRequest request) { 这个方法里可以自定义做验证。
结束语:
我实现了一遍两种网关,发现还是官网的文档最靠谱,也是能落地到项目中的。如果你需要源码的请到 我的Github 去clone,如果帮助到了你,还请点个 star。
原文地址:https://www.cnblogs.com/KuJo/p/11306361.html