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 阅读数:149 评论数: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