最全面的改造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引用上都是用的原来的。<!-- 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已经不存在了。
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 #白名单 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: 白名单连接,每个微服务的请求入口地址,包含即通过。
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
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也不兼容了。<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 的内容
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的请求配额
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)网关的白名单和黑名单配置
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
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) { 这个方法里可以自定义做验证。
结束语:
我实现了一遍两种网关,发现还是官网的文档最靠谱,也是能落地到项目中的。如果你需要源码的请到 余根海的博客 去clone,如果帮助到了你,还请点个 star,项目我会一直更新。
如果转载请写上出处!感谢阅读!
- SpringCloud系列:利用Zuul实现统一服务网关服务,简单实现IP白名单功能
- Spring Boot + Spring Cloud 实现权限管理系统 后端篇(二十一):服务网关(Zuul)
- spring cloud 使用Zuul 实现API网关服务问题
- Spring Cloud 系列之 Spring cloud gateway 实现网关路由转发和过滤功能
- springcloud项目网关升级:gateway替代zuul以及解决gateway跨域访问问题
- API网关性能比较:NGINX vs. ZUUL vs. Spring Cloud Gateway vs. Linkerd(转)
- SpringCloud-网关gateway-zuul
- Spring cloud 网关zuul配置
- 七、SpringCloud之路由网关Zuul
- 微服务网关实战——Spring Cloud Gateway
- Spring Cloud(Dalston.SR5)--Zuul 网关-过滤器
- Spring Cloud微服务中网关服务是如何实现的?(Zuul篇)
- Spring Cloud Zuul API服务网关之请求路由
- 网关 Spring-Cloud-Gateway 源码解析 —— 网关初始化
- 利用SpringCloud搭建微服务4——服务调用组件Fegin的使用,引入API网关治理组件Zuul
- Spring-Cloud-Zuul(网关服务)
- 干货分享微服务spring-cloud(6.Api网关服务zuul)
- 路由网关(zuul) SpringCloud【05】
- spring cloud网关zuul之filter注册
- spring-cloud-zuul 网关