SpringCloud_Gateway服务网关

文章目录

  • 一、SpringCloudGateway服务网关概论
    • 1、SpringCloudGateway服务网关概论
    • 2、SpringCloudGateway的三大核心概念
  • 二、SpringCloudGateway的路由及断言
    • 1、子模块项目SpringCloudGateway的搭建
    • 2、SpringCloudGateway_Java API构建路由
    • 3、SpringCloudGateway的动态路由功能
    • 4、SpringCloudGateway的路由断言
  • 三、SpringCloudGateway的过滤器及跨域
    • 1、SpringCloudGateway的过滤器
    • 2、网关过滤器GatewayFilter
    • 3、自定义网关过滤器GatewayFilter
    • 4、自定义全局过滤器GlobalFilter
    • 5、内置全局过滤器
    • 6、服务网关Gateway实现跨域
  • 四、SpringCloudGateway实现用户鉴权
    • 1、JsonWebToken概论
    • 2、创建用户的微服务及登录操作
    • 3、服务网关Gateway实现用户鉴权
  • 总结

一、SpringCloudGateway服务网关概论

1、SpringCloudGateway服务网关概论

Spring Cloud Gateway 用"Netty + Webflux"实现,不需要导入Web依赖。

  1. Webflux模式替换了旧的Servlet线程模型。用少量的线程处理request和response io操作,这些线程称为Loop线程,而业务交给响应式编程框架处理,响应式编程是非常灵活的,用户可以将业务中阻塞的操作提交到响应式框架的work线程中执行,而不阻塞的操作依然可以在Loop线程中进行处理,大大提高了Loop线程的利用率。
    即Webflux中的Loop线程不仅可以处理请求和响应请求,还可以对业务中不阻塞的操作进行处理,从而提高它的利用率。阻塞的操作由work线程进行处理。
  2. Webflux虽然可以兼容多个底层的通信框架,但是一般情况下,底层使用的还是Netty,毕竟,Netty是目前业界认可的最高性能的通信框架。
    Netty 是一个基于NIO的客户、服务器端的编程框架。提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。
  3. Spring Cloud Gateway特点
    1)易于编写谓词( Predicates )和过滤器( Filters ) 。其Predicates和Filters
    可作用于特定路由。
    2)支持路径重写。
    3)支持动态路由。
    4)集成了Spring Cloud DiscoveryClient。

2、SpringCloudGateway的三大核心概念

  1. 路由(Route)
    这是网关的基本构建块。它由一个ID,一个目标URI,一组断言和一组过滤器定义。如果断言为真,则路由匹配。
    即根据URL请求去匹配路由。
  2. 断言(predicate)
    输入类型是一个ServerWebExchange。我们可以使用它来匹配来自HTTP请求的任何内容,例如headers或参数。匹配请求内容。
    匹配完路由后,每个路由上面都会有断言,然后根据断言来判断是否可以进行路由。
  3. 过滤(filter)
    在匹配完路由和断言为真后,可以在请求被路由前或者之后对请求进行修改。
    即根据业务对其进行监控,限流,日志输出等等。

二、SpringCloudGateway的路由及断言

1、子模块项目SpringCloudGateway的搭建

  1. 在cloud父项目中新建一个模块Module,创建子模块网关cloud-gateway-gateway9527
    SpringCloud_Gateway服务网关_第1张图片

  2. 在POM文件中添加如下依赖

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <parent>
            <artifactId>cloud</artifactId>
            <groupId>com.zzx</groupId>
            <version>1.0-SNAPSHOT</version>
        </parent>
        <modelVersion>4.0.0</modelVersion>
    
        <artifactId>cloud-gateway-gateway9527</artifactId>
    
        <properties>
            <maven.compiler.source>17</maven.compiler.source>
            <maven.compiler.target>17</maven.compiler.target>
        </properties>
        <dependencies>
            <!--  引入网关Gateway依赖   -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-gateway</artifactId>
            </dependency>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.18.22</version>
            </dependency>
            <!--  引入Eureka client依赖 -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            </dependency>
            <!-- actuator监控信息完善 -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-actuator</artifactId>
            </dependency>
        </dependencies>
    
    
    </project>
    
  3. 在gateway子模块中创建包com.zzx,在包下创建主启动类GatewayMain9527

    package com.zzx;
    
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @SpringBootApplication
    @Slf4j
    public class GatewayMain9527 {
        public static void main(String[] args) {
            SpringApplication.run(GatewayMain9527.class,args);
            log.info("************ GatewayMain9527服务 启动成功 *************");
        }
    }
    
    
  4. 在resources目录下创建application.yml文件,配置如下

    server:
      port: 9527
    spring:
      cloud:
        gateway:
          routes:
            # 路由ID,没有固定规则但要求唯一,建议配合服务名
            - id: cloud-payment-provider
              # 匹配后提供服务的路由地址 (即目标服务地址)
              uri: http://localhost:8001
              # 断言会接收一个输入参数,返回一个布尔值结果
              predicates:
                # 路径相匹配的进行路由
                - Path=/payment/*
    
    
  5. 测试
    1)先开启7001和7002的Eureka服务,payment8001服务提供者和gateway9527服务。
    2)在浏览器使用9527端口,也就是网关进行访问payment8001服务即可。
    在浏览器输入:http://localhost:9527/payment/index

2、SpringCloudGateway_Java API构建路由

  1. 在子模块cloud-gateway-gateway9527中的com.zzx包下,创建包config,并在包下创建GatewayConfig

    package com.zzx.config;
    
    import org.springframework.cloud.gateway.route.RouteLocator;
    import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class GatewayConfig {
        @Bean
        public RouteLocator routeLocator(RouteLocatorBuilder builder){
            //获取路由
            RouteLocatorBuilder.Builder routes = builder.routes();
            /**
             * 设置路由
             * 1.路由id
             * 2.路由匹配规则
             * 3.目标地址
             */
            routes.route("path_route",r->r.path("/payment/*").uri("http://localhost:8001/")).build();
            return routes.build();
        }
    }
    
    
  2. 测试
    1)将yml文件中的gateway配置注释掉,然后重启该服务。
    2)在浏览器上访问:http://localhost:9527/payment/index

3、SpringCloudGateway的动态路由功能

  1. 再添加一个服务提供者,用以实现Gateway网关的动态路由的功能。
    1)复制payment8001服务,然后点击cloud父工程,ctrl+v进行粘贴,修改名字为8002
    2)修改POM文件:

    <artifactId>cloud-provider-payment8002</artifactId>
    

    3)将POM右键,选择添加为Maven项目Add as Maven Project
    SpringCloud_Gateway服务网关_第2张图片
    4)修改com.zzx包下的启动类的名字以及类中的名字

    package com.zzx;
    
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    /**
     * 主启动类
     */
    @SpringBootApplication
    @Slf4j
    public class PaymentMain8002 {
        public static void main(String[] args) {
            SpringApplication.run(PaymentMain8002.class,args);
            log.info("****** PaymentMain8002服务启动成功 *****");
        }
    }
    
    

    5)将yml文件的端口号port和instance-id的名字有8001部分都修改为8002
    然后在启动类中运行该payment8002服务。

  2. 修改gateway9527项目的yml文件

    server:
      port: 9527
    
    eureka:
      instance:
        # 注册名
        instance-id: cloud-gateway-gateway9527
      client:
        service-url:
          # Eureka server的地址
          #集群
          defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka
          #单机
          #defaultZone: http://localhost:7001/eureka/
    spring:
      application:
        #设置应用名
        name: cloud-gateway
      cloud:
        gateway:
          routes:
            # 路由ID,没有固定规则但要求唯一,建议配合服务名
            - id: cloud-payment-provider
              # 匹配后提供服务的路由地址 (即目标服务地址) lb后跟提供服务的微服务的名字
              uri: lb://CLOUD-PAYMENT-PROVIDER
              # 断言会接收一个输入参数,返回一个布尔值结果
              predicates:
                # 路径相匹配的进行路由
                - Path=/payment/*
    
  3. 注释之前的配置文件GatewayConfig中的方法。

  4. 在服务提供者payment8001和payment8002中的com.zzx.controller的PaymentController类中添加如下代码

    @Value("${server.port}")
    private String port;
    @GetMapping("lb")
    public String lb(){
        return port;
    }
    

    即通过该lb的url请求来测试动态路由是否配置生效。

  5. 测试动态路由是否配置生效。
    1)重启payment8001和payment8002以及gateway9527服务
    2)浏览器中访问:http://localhost:9527/payment/lb
    SpringCloud_Gateway服务网关_第3张图片
    SpringCloud_Gateway服务网关_第4张图片
    此时刷新后随即出现8001或8002,估计是轮询的策略。

4、SpringCloudGateway的路由断言

  1. UTC时间格式的时间参数时间生成方法

    package demo;
    
    import java.time.ZonedDateTime;
    
    public class Test1 {
    	public static void main(String[] args) {
    		ZonedDateTime now = ZonedDateTime.now();
    		System.out.println(now);
    	}
    }
    
  2. Postman的下载地址:https://dl.pstmn.io/download/latest/win64
    Postman即用来URL请求测试的软件,可以很方便的添加任何请求参数。
    点击+号即可创建新的请求窗口,用来发送URL请求
    SpringCloud_Gateway服务网关_第5张图片

  3. After路由断言

    predicates:
    	- Path=/payment/*
    	# 在这个时间点之后才能访问
    	- After=2030-04-28T11:50:49.213572400+08:00[Asia/Shanghai]
    

    即使用生成的UTC时间格式的时间,在该时间之后才允许访问。

  4. Before路由断言

    predicates:
    	- Path=/payment/*
    	 # 在这个时间点之前才能访问
        - Before=2030-04-28T11:50:49.213572400+08:00[Asia/Shanghai]
    

    即使用生成的UTC时间格式的时间,在该时间之前才允许访问。

  5. Between路由断言

    predicates:
    	- Path=/payment/*
    	# 在两个时间内才能访问
        - Between=2030-04-28T11:50:49.213572400+08:00[Asia/Shanghai],2030-04-28T11:50:49.213572400+08:00[Asia/Shanghai]
    

    即使用生成的UTC时间格式的时间,在两个时间内才允许访问。

  6. Cookie路由断言
    1)Cookie验证的是Cookie中保存的信息,Cookie断言和上面介绍的两种断言使用方式大同小异,唯一的不同是它必须连同属性值一同验证,不能单独只验证属性是否存在。

    predicates:
    	- Path=/payment/*
    	- Cookie=username,zzx
    

    即Cookie的username的值为zzx才允许访问
    2)使用postman进行测试,在headers添加Cookie即可
    SpringCloud_Gateway服务网关_第6张图片
    此时如果不带Cookie,则报404错误

  7. Header路由断言
    1)这个断言会检查Header中是否包含了响应的属性,通常可以用来验证请求是否携带了访问令牌。

    predicates:
    	- Path=/payment/*
    	- Header=X-Request-Id,\d+
    

    2)使用postman进行测试,在headers添加X-Request-Id即可
    SpringCloud_Gateway服务网关_第7张图片

  8. Host路由断言
    1)Host 路由断言 Factory包括一个参数:host name列表。使用Ant路径匹配规则, .作为分隔符。访问的主机匹配http或者https, baidu.com 默认80端口, 就可以通过路由。 多个参数使用,号隔开。

    predicates:
    	- Path=/payment/*
    	- Host=127.0.0.1,localhost
    

    2)使用postman进行测试,在headers添加Host即可
    SpringCloud_Gateway服务网关_第8张图片

  9. Method路由断言
    1)即Request请求的方式,例如GET或POST请求,不匹配则无法进行请求

    predicates:
    	- Path=/payment/*
    	- Method=GET,POST
    

    2)可以使用postman,也可以使用浏览器直接访问,因为不需要加任何参数

  10. Query路由断言
    1)请求断言也是在业务中经常使用的,它会从ServerHttpRequest中的Parameters列表中查询指定的属性,例如验证参数的类型等

    predicates:
    	- Path=/payment/*
    	- Query=age,\d+
    

    2)在参数Params中添加age属性,值为正整数即可访问
    SpringCloud_Gateway服务网关_第9张图片

三、SpringCloudGateway的过滤器及跨域

1、SpringCloudGateway的过滤器

  1. 过滤器Filter
    在用户访问各个服务前,应在网关层统一做好鉴权、限流等工作。
    1)Filter的生命周期
    根据生命周期可以将Spring Cloud Gateway中的Filter分为"PRE"和"POST"两种。
    PRE:代表在请求被路由之前执行该过滤器,此种过滤器可用来实现参数校验、权限校验、流量监控、日志输出、协议转换等功能。
    POST:代表在请求被路由到微服务之后执行该过滤器。此种过滤器可用来实现响应头的修改(如添加标准的HTTP Header )、收集统计信息和指标、将响应发送给客户端、输出日志、流量监控等功能。
    即PRE是路由之前,POST是路由之后。
    2)Filter分类
    根据作用范围,Filter可以分为以下两种。
    GatewayFilter:网关过滤器,此种过滤器只应用在单个路由或者一个分组的路由上。
    GlobalFilter:全局过滤器,此种过滤器会应用在所有的路由上。

2、网关过滤器GatewayFilter

  1. 官方的配置文档:https://docs.spring.io/spring-cloud-gateway/docs/4.0.4/reference/html/#gatewayfilter-factories
  2. 使用内置过滤器SetStatus
    1)在yml文件中的filters下添加过滤器
    server:
      port: 9527
    
    eureka:
      instance:
        # 注册名
        instance-id: cloud-gateway-gateway9527
      client:
        service-url:
          # Eureka server的地址
          #集群
          defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka
          #单机
          #defaultZone: http://localhost:7001/eureka/
    spring:
      application:
        #设置应用名
        name: cloud-gateway
      cloud:
        gateway:
          routes:
            # 路由ID,没有固定规则但要求唯一,建议配合服务名
            - id: cloud-payment-provider
              # 匹配后提供服务的路由地址 (即目标服务地址) lb后跟提供服务的微服务的名字
              uri: lb://CLOUD-PAYMENT-PROVIDER
              # 断言会接收一个输入参数,返回一个布尔值结果
              predicates:
                # 路径相匹配的进行路由
                - Path=/payment/*
                # 在这个时间点之后才能访问
    #            - After=2030-04-28T11:50:49.213572400+08:00[Asia/Shanghai]
                # 在这个时间点之前才能访问
    #            - Before=2030-04-28T11:50:49.213572400+08:00[Asia/Shanghai]
                # 在两个时间内才能访问
    #            - Between=2030-04-28T11:50:49.213572400+08:00[Asia/Shanghai],2030-04-28T11:50:49.213572400+08:00[Asia/Shanghai]
    #            - Cookie=username,zzx
    #            - Header=X-Request-Id,\d+
    #            - Host=127.0.0.1,localhost
    #            - Method=GET,POST
    #            - Query=age,\d+
              #过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
              filters:
                # 修改原始响应的状态码
                - SetStatus=250
    
    2)在浏览器测试:http://localhost:9527/payment/lb
    SpringCloud_Gateway服务网关_第10张图片
    此时响应码成功修改为250。

3、自定义网关过滤器GatewayFilter

  1. 在gateway9527服务的com.zzx.config包下,创建日志网关过滤器类LogGatewayFilterFactory

    package com.zzx.config;
    
    import lombok.Data;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.cloud.gateway.filter.GatewayFilter;
    import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
    import org.springframework.stereotype.Component;
    
    import java.util.Arrays;
    import java.util.List;
    
    /**
     * 日志网关过滤器
     */
    @Component
    @Slf4j
    public class LogGatewayFilterFactory extends AbstractGatewayFilterFactory<LogGatewayFilterFactory.Config> {
    
        public LogGatewayFilterFactory() {
            super(Config.class);
        }
    
        /**
         * 表示配置填写顺序
         * @return
         */
        @Override
        public List<String> shortcutFieldOrder() {
            return Arrays.asList("consoleLog");
        }
    
        /**
         * 执行过滤的逻辑
         * @param config
         * @return
         */
        @Override
        public GatewayFilter apply(Config config) {
            return ((exchange, chain) -> {
                if(config.consoleLog){
                    log.info("********* consoleLog日志 开启 ********");
                }
                return chain.filter(exchange);
            });
        }
    
        /**
         * 过滤器使用的配置内容
         *
         */
        @Data
        public static class Config{
            private boolean consoleLog;
        }
    
    }
    
    
  2. 在YML文件中,添加如下

    filters:
         # 控制日志是否开启
         - Log=true
    

    即开启日志,该true会被consoleLog获取到。 然后即可打印对应的日志。

  3. 测试
    1)重启Gateway9527服务
    2)在浏览器中访问:http://localhost:9527/payment/lb
    在这里插入图片描述
    步骤:
    1、类名必须叫做XxxGatewayFilterFactory,注入到Spring容器后使用时的名称就叫做Xxx。
    2、创建一个静态内部类Config, 里面的属性为配置文件中配置的参数, - 过滤器名称=参数1,参数2…
    2、类必须继承 AbstractGatewayFilterFactory,让父类帮实现配置参数的处理。
    3、重写shortcutFieldOrder()方法,返回List参数列表为Config中属性集合
    return Arrays.asList(“参数1”,参数2…)
    4、无参构造方法中super(Config.class)
    5、编写过滤逻辑 public GatewayFilter apply(Config config)

4、自定义全局过滤器GlobalFilter

  1. 在gateway9527服务的com.zzx.config包下,创建用户鉴权全局过滤器类AuthGlobalFilter

    package com.zzx.config;
    
    import org.apache.commons.lang.StringUtils;
    import org.springframework.cloud.gateway.filter.GatewayFilterChain;
    import org.springframework.cloud.gateway.filter.GlobalFilter;
    import org.springframework.core.Ordered;
    import org.springframework.http.HttpStatus;
    import org.springframework.stereotype.Component;
    import org.springframework.web.server.ServerWebExchange;
    import reactor.core.publisher.Mono;
    
    /**
     * 用户鉴权全局过滤器
     */
    @Component
    public class AuthGlobalFilter implements GlobalFilter, Ordered {
        /**
         * 自定义全局过滤器逻辑
         * @param exchange
         * @param chain
         * @return
         */
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            //1。请求中获取Token令牌
            String token = exchange.getRequest().getQueryParams().getFirst("token");
            //2.判断token是否为空
            if(StringUtils.isEmpty(token)){
                System.out.println("鉴权失败,令牌为空");
                //将状态码设置为未授权
                exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
                return exchange.getResponse().setComplete();
            }
            //3。判断token是否有效
            if(!token.equals("zzx")){
                System.out.println("token令牌无效");
                exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
                return exchange.getResponse().setComplete();
            }
            return chain.filter(exchange);
        }
    
        /**
         * 全局过滤器执行顺序 数值越小,优先级越高
         * @return
         */
        @Override
        public int getOrder() {
            return 0;
        }
    }
    
    
  2. 使用postman测试,在params中添加一个token进行测试
    SpringCloud_Gateway服务网关_第11张图片

5、内置全局过滤器

  1. 官方的配置文档:https://docs.spring.io/spring-cloud-gateway/docs/4.0.4/reference/html/#global-filters
    SpringCloud Gateway内部也是通过一系列的内置全局过滤器对整个路由转发进行处理的。
  2. 路由过滤器(Forward)
  3. 路由过滤器(LoadBalancerClient)
  4. Netty路由过滤器
  5. Netty写响应过滤器(Netty Write Response F)
  6. RouteToRequestUrl 过滤器
  7. 路由过滤器 (Websocket Routing Filter)
  8. 网关指标过滤器(Gateway Metrics Filter)
  9. 组合式全局过滤器和网关过滤器排序(Combined Global Filter and GatewayFilter Ordering)
  10. 路由(Marking An Exchange As Routed)

6、服务网关Gateway实现跨域

  1. 跨域
    即当一个请求url的协议、域名、端口三者之间任意一个与当前页面url不同即为跨域

  2. 在resources目录下创建index.html文件

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>Title</title>
    </head>
    <body>
    
    
    </body>
    <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
    <script>
    
    
      $.get("http://localhost:9527/payment/lb?token=zzx",function(data,status){
        alert("Data: " + data + "\nStatus: " + status);
       });
    </script>
    </html>
    
    
  3. 配置允许跨域
    1)在未配置允许跨域之前,打开该index.html文件时,如图
    在这里插入图片描述
    2)在yml文件中配置允许跨域

    spring:
      cloud:
       gateway:
        globalcors:
         cors-configurations:
          '[/**]':
           allowCredentials: true
           allowedOriginPatterns: "*"
           allowedMethods: "*"
           allowedHeaders: "*"
         add-to-simple-url-handler-mapping: true
    
    

3)配置后,打开该index.html文件时,如图
SpringCloud_Gateway服务网关_第12张图片

四、SpringCloudGateway实现用户鉴权

1、JsonWebToken概论

  1. JWT是一种用于双方之间传递安全信息的简洁的、URL安全的声明规范。定义了一种简洁的,自包含的方法用于通信双方之间以Json对象的形式安全的传递信息。特别适用于分布式站点的单点登录(SSO)场景。

  2. JWT优点
    1)无状态
    2)适合移动端应用
    3)单点登录友好

  3. 用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候会加上签名,服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。

  4. JWT 的三个部分依次如下:
    1)头部(header)
    JSON对象,描述 JWT 的元数据。其中 alg 属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ 属性表示这个令牌(token)的类型(type),统一写为 JWT。

    {
     "alg": "HS256",
     "typ": "JWT"
    }
    

    2)载荷(payload)
    内容又可以分为3种标准
    1.标准中注册的声明
    iss: jwt签发者
    sub: jwt所面向的用户
    aud: 接收jwt的一方
    exp: jwt的过期时间,这个过期时间必须要大于签发时间
    nbf: 定义在什么时间之前,该jwt都是不可用的.
    iat: jwt的签发时间
    jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
    2.公共的声明
    公共的声明可以添加任何的信息。一般这里我们会存放一下用户的基本信息(非敏感信息)。
    3.私有的声明
    私有声明是提供者和消费者所共同定义的声明。需要注意的是,不要存放敏感信息
    base64编码,任何人获取到jwt之后都可以解码!!

    {
     "sub": "1234567890",
     "name": "John Doe",
     "iat": 1516239022
    }
    

    3)签证(signature)
    这部分就是 JWT 防篡改的精髓,其值是对前两部分base64UrlEncode 后使用指定算法签名生成,以默认 HS256 为例,指定一个密钥(secret),就会按照如下公式生成:

    HMACSHA256(
     base64UrlEncode(header) + "." + base64UrlEncode(payload),
     secret,
    )
    
  5. 客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。此后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息Authorization字段里面。

2、创建用户的微服务及登录操作

  1. 在cloud父工程下,创建子模块项目cloud-auth-user6500
    SpringCloud_Gateway服务网关_第13张图片

  2. 在cloud-auth-user6500项目的pom文件中引入依赖

     <dependencies>
        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- redis -->
        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- eureka client 依赖 -->
        <dependency>
     <groupId>org.springframework.cloud</groupId>
          <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
          <groupId>org.projectlombok</groupId>
          <artifactId>lombok</artifactId>
          <version>1.18.22</version>
        </dependency>
        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
         <!--   引入JWT依赖   -->
        <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.23</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>4.2.1</version>
        </dependency>
      </dependencies>
    
    
  3. 在com.zzx中创建一个包utils,创建工具类JWTUtils

    package com.zzx.utils;
    
    import com.auth0.jwt.JWT;
    import com.auth0.jwt.JWTVerifier;
    import com.auth0.jwt.algorithms.Algorithm;
    import com.auth0.jwt.exceptions.JWTVerificationException;
    import com.auth0.jwt.interfaces.DecodedJWT;
    
    import java.util.Date;
    import java.util.concurrent.TimeUnit;
    
    public class JWTUtils {
        // 签发人
        private static final String ISSUSER = "zzx";
        // 过期时间 1分钟
        private static final long TOKEN_EXPIRE_TIME = 60*1000;
        // 秘钥
        public static final String SECRET_KEY = "zzx-13256";
        /**
         * 生成令牌
         * @return
         */
        public static String token(){
            Date now = new Date();
            Algorithm hmac256 = Algorithm.HMAC256(SECRET_KEY);
            // 1.创建JWT
            String token = JWT.create().
                    // 签发人
                    withIssuer(ISSUSER)
                    // 签发时间
                    .withIssuedAt(now)
                    // 过期时间
                    .withExpiresAt(new Date(now.getTime()+TOKEN_EXPIRE_TIME))
                    // 加密算法
                    .sign(hmac256);
            return token;
        }
    
        /**
         * 验证令牌
         * @return
         */
        public static boolean verify(String token){
            try {
                Algorithm hmac256 = Algorithm.HMAC256(SECRET_KEY);
                JWTVerifier verifier = JWT.require(hmac256)
                        // 签发人
                        .withIssuer(ISSUSER)
                        .build();
                // 如果校验有问题则抛出异常
                DecodedJWT verify = verifier.verify(token);
                return true;
            } catch (IllegalArgumentException e) {
                e.printStackTrace();
            } catch (JWTVerificationException e) {
                e.printStackTrace();
            }
            return false;
        }
    
        public static void main(String[] args) throws InterruptedException {
            String token = token();
            System.out.println(token);
            boolean verify = verify(token);
            System.out.println(verify);
            verify = verify(token+" 11");
            System.out.println(verify);
            TimeUnit.SECONDS.sleep(61);
            verify = verify(token);
            System.out.println(verify);
    
        }
    }
    
    

    在该工具类JWTUtils中创建main方法用来测试该工具类。后面需要删掉。

  4. 在com.zzx中创建一个包common,创建类Result

    package com.zzx.common;
    
    import lombok.AllArgsConstructor;
    import lombok.Builder;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    /**
     * 返回实体类
     */
    @AllArgsConstructor
    @NoArgsConstructor
    @Data
    @Builder
    public class Result {
        // 状态码
        private int code;
        // 描述信息
        private String msg;
        // token令牌
        private String token;
    }
    
    

    即用该类来封装返回值信息。

  5. 在com.zzx中创建一个包controller,创建控制层类UserController

    package com.zzx.controller;
    
    import com.zzx.common.Result;
    import com.zzx.utils.JWTUtils;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    /**
     * 用户控制层
     */
    @RestController
    @RequestMapping("user")
    public class UserController {
        /**
         * 登录
         * @param username
         * @param password
         */
        @PostMapping("login")
        public Result login(String username, String password){
            // 1.验证用户名和密码
            // TODO 模拟数据库操作
            if("zzx".equals(username)&&"123456".equals(password)){
                // 2.生成令牌
                String token = JWTUtils.token();
                return Result.builder().code(200).msg("success").token(token).build();
            }else{
                return Result.builder().code(500).msg("用户名或密码不正确").build();
            }
        }
    }
    
    
  6. 在resources目录下创建一个application.yml配置文件

    server:
      port: 6500
    
    eureka:
      instance:
        # 注册名
        instance-id: cloud-auth-user6500
      client:
        service-url:
          # Eureka server的地址
          #集群
          defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka
          #单机
          #defaultZone: http://localhost:7001/eureka/
    spring:
      application:
        #设置应用名
        name: cloud-auth-user
    
  7. 在com.zzx中,修改主启动类Main,修改为UserMain6500

    package com.zzx;
    
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    /**
     * 主启动类
     */
    @Slf4j
    @SpringBootApplication
    public class UserMain6500 {
        public static void main(String[] args) {
            SpringApplication.run(UserMain6500.class,args);
            log.info("************ UserMain6500服务 启动成功 ************");
        }
    }
    
  8. 测试User控制层的login方法
    1)启动eureka服务eureka7001和eureka7002以及user6500
    SpringCloud_Gateway服务网关_第14张图片

2)在postman中,使用POST请求传入用户名和密码,对该url进行测试
SpringCloud_Gateway服务网关_第15张图片

3、服务网关Gateway实现用户鉴权

即在网关过滤器中加入JWT来鉴权

  1. 在gateway9527项目的POM文件中添加JWT依赖

     <!--   引入JWT依赖     -->
            <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>2.0.23</version>
            </dependency>
            <!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
            <dependency>
                <groupId>com.auth0</groupId>
                <artifactId>java-jwt</artifactId>
                <version>4.2.1</version>
            </dependency>
    
  2. 将user6500项目中com.zzx.utils包下的JWTUtils复制到gateway9527项目的com.zzx.utils包下

    package com.zzx.utils;
    
    import com.auth0.jwt.JWT;
    import com.auth0.jwt.JWTVerifier;
    import com.auth0.jwt.algorithms.Algorithm;
    import com.auth0.jwt.exceptions.JWTVerificationException;
    import com.auth0.jwt.interfaces.DecodedJWT;
    
    import java.util.Date;
    
    public class JWTUtils {
        // 签发人
        private static final String ISSUSER = "zzx";
        // 过期时间 1分钟
        private static final long TOKEN_EXPIRE_TIME = 60*1000;
        // 秘钥
        public static final String SECRET_KEY = "zzx-13256";
        /**
         * 生成令牌
         * @return
         */
        public static String token(){
            Date now = new Date();
            Algorithm hmac256 = Algorithm.HMAC256(SECRET_KEY);
            // 1.创建JWT
            String token = JWT.create().
                    // 签发人
                    withIssuer(ISSUSER)
                    // 签发时间
                    .withIssuedAt(now)
                    // 过期时间
                    .withExpiresAt(new Date(now.getTime()+TOKEN_EXPIRE_TIME))
                    // 加密算法
                    .sign(hmac256);
            return token;
        }
    
        /**
         * 验证令牌
         * @return
         */
        public static boolean verify(String token){
            try {
                Algorithm hmac256 = Algorithm.HMAC256(SECRET_KEY);
                JWTVerifier verifier = JWT.require(hmac256)
                        // 签发人
                        .withIssuer(ISSUSER)
                        .build();
                // 如果校验有问题则抛出异常
                DecodedJWT verify = verifier.verify(token);
                return true;
            } catch (IllegalArgumentException e) {
                e.printStackTrace();
            } catch (JWTVerificationException e) {
                e.printStackTrace();
            }
            return false;
        }
    
    }
    
    
  3. 修改application.yml文件

    server:
      port: 9527
    
    eureka:
      instance:
        # 注册名
        instance-id: cloud-gateway-gateway9527
      client:
        service-url:
          # Eureka server的地址
          #集群
          defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka
          #单机
          #defaultZone: http://localhost:7001/eureka/
    
    org:
      my:
        jwt:
          # 跳过认证路由
          skipAuthUrls:
            - /user/login
    
    spring:
      application:
        #设置应用名
        name: cloud-gateway
      cloud:
        gateway:
          # 路由配置
          routes:
            # 路由ID,没有固定规则但要求唯一,建议配合服务名
            - id: cloud-auth-user
              # 匹配后提供服务的路由地址 (即目标服务地址) lb后跟提供服务的微服务的名字
              uri: lb://CLOUD-AUTH-USER
              # 断言会接收一个输入参数,返回一个布尔值结果
              predicates:
                # 路径相匹配的进行路由
                - Path=/user/*
            # 路由ID,没有固定规则但要求唯一,建议配合服务名
            - id: cloud-payment-provider
              # 匹配后提供服务的路由地址 (即目标服务地址) lb后跟提供服务的微服务的名字
              uri: lb://CLOUD-PAYMENT-PROVIDER
              # 断言会接收一个输入参数,返回一个布尔值结果
              predicates:
                # 路径相匹配的进行路由
                - Path=/payment/*
                # 在这个时间点之后才能访问
    #            - After=2030-04-28T11:50:49.213572400+08:00[Asia/Shanghai]
                # 在这个时间点之前才能访问
    #            - Before=2030-04-28T11:50:49.213572400+08:00[Asia/Shanghai]
                # 在两个时间内才能访问
    #            - Between=2030-04-28T11:50:49.213572400+08:00[Asia/Shanghai],2030-04-28T11:50:49.213572400+08:00[Asia/Shanghai]
    #            - Cookie=username,zzx
    #            - Header=X-Request-Id,\d+
    #            - Host=127.0.0.1,localhost
    #            - Method=GET,POST
    #            - Query=age,\d+
              #过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
              filters:
                # 修改原始响应的状态码
    #            - SetStatus=250
                # 控制日志是否开启
                - Log=true
          globalcors:
            cors-configurations:
              '[/**]':
                allowCredentials: true
                allowedOriginPatterns: "*"
                allowedMethods: "*"
                allowedHeaders: "*"
            add-to-simple-url-handler-mapping: true
    

    即需要添加一个user微服务的路由,以及跳过权限验证的Path路径

  4. 将gateway9527项目的com.zzx.config包下原先的用户鉴权类AuthGlobalFilter上面的@Component注解注释掉,即不使用这个类来鉴权;创建使用另一个类UserAuthGlobalFilter来鉴权

    package com.zzx.config;
    
    import com.alibaba.fastjson.JSONObject;
    import com.zzx.common.Response;
    import com.zzx.utils.JWTUtils;
    import io.micrometer.common.util.StringUtils;
    import lombok.Data;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    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.server.reactive.ServerHttpResponse;
    import org.springframework.stereotype.Component;
    import org.springframework.web.server.ServerWebExchange;
    import reactor.core.publisher.Flux;
    import reactor.core.publisher.Mono;
    
    import java.nio.charset.StandardCharsets;
    
    /**
     * 用户鉴权全局过滤器
     */
    @Data
    @ConfigurationProperties("org.my.jwt")
    @Component
    @Slf4j
    public class UserAuthGlobalFilter implements GlobalFilter, Ordered {
        private String[] skipAuthUrls;
        /**
         * 过滤器逻辑
         * @param exchange
         * @param chain
         * @return
         */
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            // 获取请求url地址
            String path = exchange.getRequest().getURI().getPath();
            // 跳过不需要验证的路径
            if(skipAuthUrls!=null && isSKip(path)){
                return chain.filter(exchange);
            }
            // 1.从请求头中获取token
            String token = exchange.getRequest().getHeaders().getFirst("token");
            // 2.判断token
            if(StringUtils.isEmpty(token)){
                // 3.设置响应
                ServerHttpResponse response = exchange.getResponse();
                // 4.设置响应状态码
                response.setStatusCode(HttpStatus.OK);
                // 5.设置响应头
                response.getHeaders().add("Content-Type","application/json;charset=UTF-8");
                // 6.创建响应对象
                Response res = new Response(200, "token 参数缺失");
                // 7.对象转字符串
                byte[] bytes = JSONObject.toJSONString(res).getBytes(StandardCharsets.UTF_8);
                // 8.数据流返回数据
                DataBuffer wrap = response.bufferFactory().wrap(bytes);
                return response.writeWith(Flux.just(wrap));
    
            }
            // 验证token
            boolean verify = JWTUtils.verify(token);
            if(!verify){
                // 3.设置响应
                ServerHttpResponse response = exchange.getResponse();
                // 4.设置响应状态码
                response.setStatusCode(HttpStatus.OK);
                // 5.设置响应头
                response.getHeaders().add("Content-Type","application/json;charset=UTF-8");
                // 6.创建响应对象
                Response res = new Response(200, "token 失效");
                // 7.对象转字符串
                byte[] bytes = JSONObject.toJSONString(res).getBytes(StandardCharsets.UTF_8);
                // 8.数据流返回数据
                DataBuffer wrap = response.bufferFactory().wrap(bytes);
                return response.writeWith(Flux.just(wrap));
            }
            // token 令牌通过
            return chain.filter(exchange);
        }
    
        @Override
        public int getOrder() {
            return 0;
        }
    
        private boolean isSKip(String url){
            for (String skipAuthUrl :skipAuthUrls) {
                if(url.startsWith(skipAuthUrl)){
                    return true;
                }
            }
            return false;
        }
    }
    
    
  5. 测试
    1)先启动eureka7001和eureka7002,还有Payment8001和Payment8002,以及user6500和gateway9527服务。
    SpringCloud_Gateway服务网关_第16张图片

2)使用postman工具来测试,先进行登录,拿到用户的token
SpringCloud_Gateway服务网关_第17张图片
3)再切换到之前9527的url测试
token有效时
SpringCloud_Gateway服务网关_第18张图片
token过期失效时
SpringCloud_Gateway服务网关_第19张图片
没有token时(即未登录时)
SpringCloud_Gateway服务网关_第20张图片

总结

  1. Spring Cloud Gateway 用"Netty + Webflux"实现,不需要导入Web依赖。
    1)Webflux模式替换了旧的Servlet线程模型。用少量的线程处理request和response io操作,这些线程称为Loop线程。Webflux中的Loop线程不仅可以处理请求和响应请求,还可以对业务中不阻塞的操作进行处理,从而提高它的利用率。阻塞的操作由work线程进行处理。
    Webflux底层使用的还是Netty,Netty是目前业界认可的最高性能的通信框架。
    2)SpringCloudGateway的三大核心概念,分别是路由、断言、过滤。
    即根据url请求进行匹配到指定路由;每个路由上面都有断言,根据断言来判断是否可以进行路由;最后对该url请求进行一个过滤,例如监控、限流和日志输出等操作。
  2. 1)SpringCloudGateway的搭建,需要先引入依赖,然后创建主启动类,最后配置Gateway的配置文件。- id属性值需要唯一;uri的属性值即对应的服务器ip地址+端口号;predicates断言的属性值,例如OrderController第一层@RequestMapping注解的url属性值,即判断url是否跟该值一致。
    2)服务网关Gateway通过Java API构建时需要实现RouteLocator接口构建路由规则。即先将yml文件中等价的gateway配置注释掉,然后创建一个config配置类,在配置类中,创建一个方法使用RouteLocator接口来构建路由。并在该方法上添加@Bean注解,即由SpringIOC容器进行管理。
    3)SpringCloudGateway的动态路由功能,即在yml文件中将原本路由的uri改成lb://服务提供者的微服务的名字;然后需要引入EureakaClient和Gateway等依赖即可实现Gateway的动态路由功能。
    也就是说需要配置和使用Eureka,但是可以设置不把自身注册到Eureka服务中。
    4)SpringCloudGateway的路由断言,路由断言分别有After、Before、Between、Cookie、Header、Host、Method、Query等。其中After、Before、Between都是跟时间有关的;Cookie、Header、Host都是在头文件Headers中携带的参数;Method是匹配指定的请求方法;Query是在Params中检查参数的合法性。
    断言是在YML文件的spring.cloud.gateway.routes.predicates下进行配置的。
    5)SpringCloudGateway的过滤器,在用户访问各个服务前,应在网关层统一做好鉴权、限流等工作。过滤器Filter的生命周期分为PRE和POST,即PRE是路由之前,POST是路由之后。它作用范围分为GatewayFilter和GlobalFilter,即GatewayFilter是网关路由器,是应用在单个路由或一个分组的路由上的,而GlobalFilter是全局路由器,会应用在所有路由上的。
  3. 1)内置过滤器,即在YML文件中,在filters下添加内置过滤器。
    2)自定义网关过滤器,即需要创建一个配置类,类名必须叫做XxxGatewayFilterFactory,在该类上使用@Component注解;该类需要创建一个静态内部类Config,里面的属性为配置文件中配置的参数;必须继承AbstractGatewayFilterFactory;重写shortcutFieldOrder()方法,返回List参数列表为Config中属性集合;创建无参构造方法,方法体为super(Config.class);编写过滤逻辑 public GatewayFilter apply(Config config)。
    3)自定义全局过滤器,当客户端第一次请求服务时,服务端对用户进行信息认证(登录);认证通过,将用户信息进行加密形成token,返回给客户端,作为登录凭证;以后每次请求,客户端都携带认证的token;服务端对token进行解密,判断是否有效,有效则继续允许访问服务,无效则不允许访问服务。
    此时需要实现GlobalFilter, Ordered接口,一个是全局过滤器接口,一个是全局过滤器执行顺序的接口。
    即在Ordered接口的实现类中返回一个数值,该值越小,当前过滤器的优先级越高。
    GlobalFilter接口的实现类,对请求参数进行一个过滤操作。
  4. 1)配置跨域,即在yml文件配置允许跨域即可。
    2)JsonWebToken,是一种用于双方之间传递安全信息的简洁的、URL安全的声明规范。适用于分布式的单点登录(SSO)。
    客户端收到服务器返回的 JWT,会把数据保存到loadStorage。JWT签证默认的算法是 HMAC SHA256(HS256)。
    3)用户登录并生成token返回的业务流程,登录成功时JWT的Token通过JWTUtils工具类生成,状态码为200,消息为成功,以及返回值的类型需要封装为Result实体类;登录失败时不生成token,状态码500,消息为用户名或密码错误,同时返回值的类型也是需要封装为Result实体类。此时该方法为Post请求,因为涉及帐号信息。
    并且需要引入JWT和fastjson依赖。
  5. 使用gateway网关进行用户鉴权,在application.yml文件中配置跳过login登录的鉴权,即其他的url请求都要进行用户鉴权;需要引入JWT和fastjson的依赖;使用JWTUtils的生成token方法以及验证token是否有效的方法;在用户鉴权时,需要先获取跳过的路径,进行匹配,匹配成功则跳过鉴权进行下一步的业务操作;匹配失败,则说明该请求需要验证token,首先需要从request请求的请求头中获取token,如果token为空,则返回一个response对象,包含状态码和字符串消息;如果token不为空,则进行下一步验证,即使用JWTUtils的token验证方法,如果返回false,则表示token无效或者失效,则返回一个response对象,包含状态码和字符串消息;如果token不为空且token有效,则进行下一步的业务操作。
    即用户鉴权,实际上就判断该请求是否需要跳过鉴权,以及token是否为空和token是否有效的操作。

你可能感兴趣的:(SpringCloud,spring,cloud,gateway,java)