Error message here!

Hide Error message here!

忘记密码?

Error message here!

请输入正确邮箱

Hide Error message here!

密码丢失?请输入您的电子邮件地址。您将收到一个重设密码链接。

Error message here!

返回登录

Close

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

yugenhai 2019-08-05 23:27:00 阅读数:416 评论数:0 点赞数:0 收藏数:0

前言:

最近开发了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: 白名单连接,每个微服务的请求入口地址,包含即通过。

 
(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) { 这个方法里可以自定义做验证。

 

结束语:

我实现了一遍两种网关,发现还是官网的文档最靠谱,也是能落地到项目中的。如果以上帮助到了你,或者需要源码的请到 余根海的博客 去clone,如果帮助到了你,还请你点个 star,项目我会一直更新。

 

如果转载请写上出处!感谢阅读!

版权声明
本文为[yugenhai]所创,转载请带上原文链接,感谢
https://www.cnblogs.com/KuJo/p/11306361.html