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

时间:2020-12-24 21:15:02

前言:

最近开发了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,文章结尾会提供此组件的实现)
 
最全面的改造Zuul网关为Spring Cloud Gateway(包含Zuul核心实现和Spring Cloud Gateway核心实现)
 
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
#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)上面提到白名单,那需要初始化白名单
 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) { 这个方法里可以自定义做验证。

结束语:

我实现了一遍两种网关,发现还是官网的文档最靠谱,也是能落地到项目中的。如果你需要源码的请到 我的Github 去clone,如果帮助到了你,还请点个 star。