畅购商城第8章-微服务网关和Jwt令牌

微服务网关

搭建changgou-gateway-web工程

application.yml配置

spring:
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]': # 匹配所有请求
            allowedOrigins: "*" #跨域处理 允许所有的域
            allowedMethods: # 支持的方法
              - GET
              - POST
              - PUT
              - DELETE
  application:
    name: gateway-web
server:
  port: 8001
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:7001/eureka
  instance:
    prefer-ip-address: true
management:
  endpoint:
    gateway:
      enabled: true
    web:
      exposure:
        include: true

Host 路由配置

      routes:
            - id: changgou_goods_route
              uri: http://localhost:18081
              predicates:
              - Host=cloud.changgou.com**

**:匹配任意目录,可以是一个“*”或者多个“*”,没有区别

修改本地Host文件

127.0.0.1 cloud.changgou.com

测试结果:畅购商城第8章-微服务网关和Jwt令牌_第1张图片
路径匹配过滤配置

      routes:
            - id: changgou_goods_route
              uri: http://localhost:18081
              predicates:
              - Path=/brand/**


# /brand:开头必须是"/brand"请求的url
# /*:匹配一级目录,例:http://localhost:8001/brand/1115
# /**:匹配多级目录,例:http://localhost:8001/brand/search/1/6

指定请求路径的前缀

      routes:
            - id: changgou_goods_route
              uri: http://localhost:18081
              predicates:
              #- Host=cloud.itheima.com**
              - Path=/brand/**
              filters:
              - PrefixPath=/brand

# /**:匹配多级目录
# - PrefixPath:指定请求的前缀路径(也就是在访问的时候可以不同加“/brand”)

注意:

	- Path=/brand/**和- PrefixPath=/brand
	测试时如果这两个配置同时存在输入http://cloud.changgou.com:8001/
	- Path=/brand/**会先判断是否存在/brand前缀,然后在加上前缀/brand
	导致冲突,无法同时使用

StripPrefix 网关的过滤配置

routes:
        - id: changgou-goods-route
          uri: http://localhost:18081
          predicates:
          - Host=cloud.changgou.com**
          - Path=/api/brand/**
          filters:
            - StripPrefix=1

测试结果:

畅购商城第8章-微服务网关和Jwt令牌_第2张图片

客户端负载均衡

使用LoadBalancerClientFilter来实现负载均衡调用。LoadBalancerClientFilter会作用在url以lb开头的路由,然后利用loadBalancer来获取服务实例,构造目标requestUrl,设置到GATEWAY_REQUEST_URL_ATTR属性中,供NettyRoutingFilter使用

application.yml

 routes:
        - id: changgou-goods-route
          uri: lb://goods

畅购商城第8章-微服务网关和Jwt令牌_第3张图片 ### 网关限流

令牌桶算法

在nginx中:通过令牌桶限流。

桶:队列来实现。redis 队列来实现。 分布式锁:Redis SETNX

实现:4r/s 计数器 限定单个客户端 ip + 4次

令牌桶算法是比较常见的限流算法之一,大概描述如下:
1)所有的请求在处理之前都需要拿到一个可用的令牌才会被处理;
2)根据限流大小,设置按照一定的速率往桶里添加令牌;
3)桶设置最大的放置令牌限制,当桶满时、新添加的令牌就被丢弃或者拒绝;
4)请求达到后首先要获取令牌桶中的令牌,拿着令牌才可以进行其他的业务逻辑,处理完业务逻辑之后,将令牌直接删除;
5)令牌桶有最低限额,当桶中的令牌达到最低限额的时候,请求处理完之后将不会删除令牌,以此保证足够的限流

使用令牌桶进行请求次数限流

  1. 引入redis依赖

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-redis-reactiveartifactId>
            <version>2.1.3.RELEASEversion>
        dependency>
  1. 修改application.yml配置,添加限流配置
spring:
  cloud:
    gateway:
      globalcors:
        corsConfigurations:
          '[/**]': # 匹配所有请求
            allowedOrigins: "*" #跨域处理 允许所有的域
            allowedMethods: # 支持的方法
              - GET
              - POST
              - PUT
              - DELETE
      routes:
        - id: changgou_goods_route
          uri: lb://goods
          predicates:
            - Path=/api/brand/**
          filters:
            - StripPrefix=1
            - name: RequestRateLimiter #请求数限流 名字不能随便写
              args:
                key-resolver: "#{@ipKeyResolver}"
                redis-rate-limiter.replenishRate: 2
                redis-rate-limiter.burstCapacity: 4

  application:
    name: gateway-web
  #Redis配置
  redis:
    host: 192.168.211.132
    port: 6379

server:
  port: 8001
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:7001/eureka
  instance:
    prefer-ip-address: true
management:
  endpoint:
    gateway:
      enabled: true
    web:
      exposure:
        include: true

解释:

redis-rate-limiter.replenishRate是您希望允许用户每秒执行多少请求,而不会丢弃任何请求。这是令牌桶填充的速率

redis-rate-limiter.burstCapacity是指令牌桶的容量,允许在一秒钟内完成的最大请求数,将此值设置为零将阻止所有请求。

key-resolver: “#{@ipKeyResolver}” 用于通过SPEL表达式来指定使用哪一个KeyResolver.

如上配置:

表示 一秒内,允许 一个请求通过,令牌桶的填充速率也是一秒钟添加一个令牌。

最大突发状况 也只允许 一秒内有一次请求,可以根据业务来调整 。

  1. 在启动类添加定义KeyResolver

KeyResolver用于计算某一个类型的限流的KEY也就是说,可以通过KeyResolver来指定限流的Key。

我们可以根据IP来限流,比如每个IP每秒钟只能请求一次,在GatewayWebApplication定义key的获取,获取客户端IP,将IP作为key,如下代码:

/**
     * IP限流
     * @return
     */
    @Bean(value = "ipKeyResolver")
    public KeyResolver keyResolver(){
        return new KeyResolver() {
            @Override
            public Mono<String> resolve(ServerWebExchange exchange) {
                String address = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress();
                return Mono.just(address);
            }
        };
    }

测试:

畅购商城第8章-微服务网关和Jwt令牌_第4张图片

用户登录

1.创建changgou-service-user-api和changgou-service-user工程导入依赖
畅购商城第8章-微服务网关和Jwt令牌_第5张图片

	<dependencies>
        <dependency>
            <groupId>com.changgougroupId>
            <artifactId>changgou-service-user-apiartifactId>
            <version>1.0-SNAPSHOTversion>
        dependency>
    dependencies>

2.用模板创建user的pojo等并导入
畅购商城第8章-微服务网关和Jwt令牌_第6张图片3.配置文件和启动类

server:
  port: 18087
spring:
  application:
    name: user
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.211.132:3306/changgou_user?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
    username: root
    password: 123456
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:7001/eureka
  instance:
    prefer-ip-address: true
feign:
  hystrix:
    enabled: true

4.实现用户登录方法

/**
     * 用户登录
     * @param username
     * @param password
     * @return
     */
    @RequestMapping("/login")
    public Result userLogin(String username,String password){
        //判断输入的用户名是否为空
        if (!StringUtils.isEmpty(username)){
            //通过主键:用户名查找用户
            User user = userService.findById(username);
            if (user!=null){
                //用户名不为空,存在该用户,比较密码
                if (BCrypt.checkpw(password,user.getPassword())){
                    //密码正确登录成功
                    return new Result(true,StatusCode.OK,"登录成功");
                }
            }
        }
        return new Result(true,StatusCode.LOGINERROR,"用户名或密码错误");
    }

测试:

畅购商城第8章-微服务网关和Jwt令牌_第7张图片

网关关联

修改网关工程changgou-gateway-web的application.yml配置文件,如下代码:

spring:
  cloud:
    gateway:
      globalcors:
        corsConfigurations:
          '[/**]': # 匹配所有请求
            allowedOrigins: "*" #跨域处理 允许所有的域
            allowedMethods: # 支持的方法
              - GET
              - POST
              - PUT
              - DELETE
      routes:
        - id: changgou_goods_route
          uri: lb://goods
          predicates:
            - Path=/api/album/**,/api/brand/**,/api/cache/**,/api/categoryBrand/**,/api/category/**,/api/para/**,/api/pref/**,/api/sku/**,/api/spec/**,/api/spu/**,/api/stockBack/**,/api/template/**
          filters:
            - StripPrefix=1
            - name: RequestRateLimiter #请求数限流 名字不能随便写 ,使用默认的facatory
              args:
                key-resolver: "#{@ipKeyResolver}"
                redis-rate-limiter.replenishRate: 1
                redis-rate-limiter.burstCapacity: 1
        #用户微服务
        - id: changgou_user_route
          uri: lb://user
          predicates:
            - Path=/api/user/**,/api/address/**,/api/areas/**,/api/cities/**,/api/provinces/**
          filters:
            - StripPrefix=1
  application:
    name: gateway-web
  #Redis配置
  redis:
    host: 192.168.211.132
    port: 6379

server:
  port: 8001
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:7001/eureka
  instance:
    prefer-ip-address: true
management:
  endpoint:
    gateway:
      enabled: true
    web:
      exposure:
        include: true

测试:
畅购商城第8章-微服务网关和Jwt令牌_第8张图片

Jwd权限校验介绍

  • 用户认证:多服务场景

畅购商城第8章-微服务网关和Jwt令牌_第9张图片

  • 加密-解密(数字签名)

畅购商城第8章-微服务网关和Jwt令牌_第10张图片

需求分析

我们之前已经搭建过了网关,使用网关在网关系统中比较适合进行权限校验。

那么我们可以采用JWT的方式来实现鉴权校验。
畅购商城第8章-微服务网关和Jwt令牌_第11张图片

什么是JWT

JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息(token)

JWT的构成

一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。

畅购商城第8章-微服务网关和Jwt令牌_第12张图片

头部(Header)

头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象。



{"typ":"JWT","alg":"HS256"}在头部指明了签名算法是HS256算法。 我们进行BASE64编码http://base64.xpcha.com/,编码后的字符串如下:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

小知识:Base64是一种基于64个可打印字符来表示二进制数据的表示方法。由于2的6次方等于64,所以每6个比特为一个单元,对应某个可打印字符。三个字节有24个比特,对应于4个Base64单元,即3个字节需要用4个可打印字符来表示。JDK 中提供了非常方便的 BASE64EncoderBASE64Decoder,用它们可以非常方便的完成基于 BASE64 的编码和解码

@Test
public void testEncoder() throws Exception{
    String msg = "www.itheima.com";
    // 编码
    byte[] encode = Base64.getEncoder().encode(msg.getBytes("UTF-8"));
    String encodeMsg = new String(encode, "UTF-8");
    System.out.println("编码后:" + encodeMsg);
    // 解码
    byte[] decode = Base64.getDecoder().decode(encode);
    String decodeMsg = new String(decode, "UTF-8");
    System.out.println("解码后:" + decodeMsg);
}

头部:

  • 记录的是令牌的信息【备注】(比如记录使用的签名的算法) + 通过base64进行编码
  • 不是必须的。但是按照JWT的规范,生成的令牌包含头信息。

载荷(playload)

载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分

(1)标准中注册的声明(建议但不强制使用)

iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

(2)公共的声明

公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.

(3)私有的声明

私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。

这个指的就是自定义的claim。比如下面面结构举例中的admin和name都属于自定的claim。这些claim跟JWT标准规定的claim区别在于:JWT规定的claim,JWT的接收方在拿到JWT之后,都知道怎么对这些标准的claim进行验证(还不知道是否能够验证);而private claims不会验证,除非明确告诉接收方要对这些claim进行验证以及规则才行。

定义一个payload:

{"sub":"1234567890","name":"John Doe","admin":true}

然后将其进行base64加密,得到Jwt的第二部分。

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

载荷:

  • 记录用户的身份信息(账号、密码、角色)
  • 载荷信息是必须的(对载荷信息进行编码)

签证(signature)

jwt的第三部分是一个签证信息,这个签证信息由三部分组成:

header (base64后的)

payload (base64后的)

secret

这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。

TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

将这三部分用.连接成一个完整的字符串,构成了最终的jwt:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。

签名:

  • 对信息进行加密
  • 签名是必须的。

头 + 载荷 + 签名 == token

JJWT的介绍和使用

JJWT是一个提供端到端的JWT创建和验证的Java库。永远免费和开源(Apache License,版本2.0),JJWT很容易使用和理解。它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。

官方文档:

https://github.com/jwtk/jjwt

创建TOKEN

(1)依赖引入

在changgou-parent项目中的pom.xml中添加依赖:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-neB380ef-1574248242740)(assets/1566280945845.png)]


<dependency>
    <groupId>io.jsonwebtokengroupId>
    <artifactId>jjwtartifactId>
    <version>0.9.0version>
dependency>

(2)创建测试

在changgou-common的/test/java下创建测试类,并设置测试方法

// 创建token
@Test
public void createToken(){
    // 创建JWT
    JwtBuilder builder = Jwts.builder();
    // 构建头信息
    Map<String, Object> map = new HashMap<>();
    map.put("alg", "HS256");
    map.put("keyId", "JWT");
    builder.setHeader(map);
    // 构建载荷信息
    builder.setId("001");
    builder.setIssuer("张三");
    builder.setIssuedAt(new Date());
    // 添加签名
    builder.signWith(SignatureAlgorithm.HS256, "itheima");

    // 生成token
    String token = builder.compact();
    System.out.println("token:" + token);
}

运行打印结果:

eyJrZXlJZCI6IkpXVCIsImFsZyI6IkhTMjU2In0.eyJqdGkiOiIwMDEiLCJpc3MiOiLlvKDkuIkiLCJpYXQiOjE1NjYyODE5MDd9.ZsaAc2g5EvSssz11xJPloKoRmWwn63ek3jr0TrvNgiY

再次运行,会发现每次运行的结果是不一样的,因为我们的载荷中包含了时间。

TOKEN解析

我们刚才已经创建了token ,在web应用中这个操作是由服务端进行然后发给客户端,客户端在下次向服务端发送请求时需要携带这个token(这就好像是拿着一张门票一样),那服务端接到这个token 应该解析出token中的信息(例如用户id),根据这些信息查询数据库返回相应的结果。

@Test
public void testParseToken(){
    // 被解析的令牌
    String token = "eyJrZXlJZCI6IkpXVCIsImFsZyI6IkhTMjU2In0.eyJqdGkiOiIwMDEiLCJpc3MiOiLlvKDkuIkiLCJpYXQiOjE1NjYyODI3MjEsImV4cCI6MTU2NjI4Mjc1MX0.c8Tw0HWXypEb6c9bhJ7SuAA7I1tnEyUBUHZ-p_acm3M";
    // 创建解析对象
    JwtParser parser = Jwts.parser();
    parser.setSigningKey("itheima");
    Claims claims = parser.parseClaimsJws(token).getBody();= parser.parseClaimsJws(token).getBody();
    System.out.println(claims);
}

运行打印效果:

header={keyId=JWT, alg=HS256},body={jti=001, iss=张三, iat=1566281907},signature=ZsaAc2g5EvSssz11xJPloKoRmWwn63ek3jr0TrvNgiY

试着将token或签名秘钥篡改一下,会发现运行时就会报错,所以解析token也就是验证token.

设置过期时间

有很多时候,我们并不希望签发的token是永久生效的,所以我们可以为token添加一个过期时间。

token过期设置

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4djvTDy3-1574248242740)(assets/1566282784577.png)]

解释:

builder.setExpiration(new Date(System.currentTimeMillis() + 30000));    // 30秒后过期

超过30s后,执行相关如下:

解析TOKEN

@Test
public void testParseToken(){
    // 被解析的令牌
    String token = "eyJrZXlJZCI6IkpXVCIsImFsZyI6IkhTMjU2In0.eyJqdGkiOiIwMDEiLCJpc3MiOiLlvKDkuIkiLCJpYXQiOjE1NjYyODI3MjEsImV4cCI6MTU2NjI4Mjc1MX0.c8Tw0HWXypEb6c9bhJ7SuAA7I1tnEyUBUHZ-p_acm3M";
    // 创建解析对象
    JwtParser parser = Jwts.parser();
    parser.setSigningKey("itheima");
    Jws<Claims> claimsJws = parser.parseClaimsJws(token);
    System.out.println(claimsJws);
}

打印效果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nvcRHGyP-1574248242741)(assets/1566282831696.png)]

当前时间超过过期时间,则会报错。

自定义claims

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n6SvWI1R-1574248242741)(assets/1574222654169.png)]

我们刚才的例子只是存储了id和subject两个信息,如果你想存储更多的信息(例如角色)可以定义自定义claims。

创建测试类,并设置测试方法:

创建token:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a1RPW62p-1574248242742)(assets/1566283158341.png)]

运行打印效果:

eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NjIwNjMyOTIsImFkZHJlc3MiOiLmt7HlnLPpu5Hpqazorq3nu4PokKXnqIvluo_lkZjkuK3lv4MiLCJuYW1lIjoi546L5LqUIiwiYWdlIjoyN30.ZSbHt5qrxz0F1Ma9rVHHAIy4jMCBGIHoNaaPQXxV_dk

解析TOKEN:

@Test
public void testParseToken(){
    // 被解析的令牌
    String token = "eyJrZXlJZCI6IkpXVCIsImFsZyI6IkhTMjU2In0.eyJhZGRyZXNzIjoi5YyX5LqU546vIiwic2Nob29sIjoi5LqU6YGT5Y-j6IGM5Lia5oqA5pyv5a2m6ZmiIn0.bJRDjdLsLMbgFuuOYlLR6qPK9MrTHqbeW8Ggbm7JDaU";
    // 创建解析对象
    JwtParser parser = Jwts.parser();
    parser.setSigningKey("itheima");
    Claims claims = parser.parseClaimsJws(token).getBody();
    System.out.println(claims);
}

运行效果:

畅购商城第8章-微服务网关和Jwt令牌_第13张图片

鉴权处理

为什么需要鉴权处理:
在用户的某些访问中,需要登录才能够去处理,使用鉴权处理,可以在网关中获取用户携带的令牌并进行识别用户访问的路径是否需要登录,如果需要,识别用户的身份是否能访问该路径

6.1 思路分析

畅购商城第8章-微服务网关和Jwt令牌_第14张图片

1.用户通过访问微服务网关调用微服务,同时携带头文件信息
2.在微服务网关这里进行拦截,拦截后获取用户要访问的路径
3.识别用户访问的路径是否需要登录,如果需要,识别用户的身份是否能访问该路径[这里可以基于数据库设计一套权限]
4.如果需要权限访问,用户已经登录,则放行
5.如果需要权限访问,且用户未登录,则提示用户需要登录
6.用户通过网关访问用户微服务,进行登录验证
7.验证通过后,用户微服务会颁发一个令牌给网关,网关会将用户信息封装到头文件中,并响应用户
8.用户下次访问,携带头文件中的令牌信息即可识别是否登录

添加依赖

在父工程中添加依赖


<dependency>
    <groupId>io.jsonwebtokengroupId>
    <artifactId>jjwtartifactId>
    <version>0.9.0version>
dependency>

添加TOKEN工具类

在changgou-common中创建类entity.JwtUtil(创建令牌),主要辅助生成Jwt令牌信息

    public class JwtUtil {

    //有效期为
    public static final Long JWT_TTL = 3600000L;// 60 * 60 *1000  一个小时

    //Jwt令牌信息
    public static final String JWT_KEY = "itcast";

    public static String createJWT(String id, String subject, Long ttlMillis) {
        //指定算法
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

        //当前系统时间
        long nowMillis = System.currentTimeMillis();
        //令牌签发时间
        Date now = new Date(nowMillis);

        //如果令牌有效期为null,则默认设置有效期1小时
        if(ttlMillis==null){
            ttlMillis=JwtUtil.JWT_TTL;
        }

        //令牌过期时间设置
        long expMillis = nowMillis + ttlMillis;
        Date expDate = new Date(expMillis);

        //生成秘钥
        SecretKey secretKey = generalKey();

        //封装Jwt令牌信息
        JwtBuilder builder = Jwts.builder()
                .setId(id)                    //唯一的ID
                .setSubject(subject)          // 主题  可以是JSON数据
                .setIssuer("admin")          // 签发者
                .setIssuedAt(now)             // 签发时间
                .signWith(signatureAlgorithm, secretKey) // 签名算法以及密匙
                .setExpiration(expDate);      // 设置过期时间
        return builder.compact();
    }

    /**
     * 生成加密 secretKey
     * @return
     */
    public static SecretKey generalKey() {
        byte[] encodedKey = Base64.getEncoder().encode(JwtUtil.JWT_KEY.getBytes());
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
        return key;
    }


    /**
     * 解析令牌数据
     * @param jwt
     * @return
     * @throws Exception
     */
    public static Claims parseJWT(String jwt) throws Exception {
        SecretKey secretKey = generalKey();
        return Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(jwt)
                .getBody();
    }
}

拷贝JwtUtil到changgou-gateway-web(解析令牌)中(该工程并未依赖common工程,因此需要添加该工具类)

自定义全局过滤器,在changgou-gateway-web中创建过滤器类

package com.changgou.filter;

import com.changgou.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import io.netty.handler.codec.http.cookie.Cookie;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * @author :屈雪耀
 * @date :Created in 2019/11/20 19:28
 * @description:
 * @modified By:
 * @version: $
 */
@Component
public class AuthorizeFilter implements GlobalFilter, Ordered {

    private static final String AUTHORIZE_TOKEN = "Authorization";
    private static final String LOGINURL = "/api/user/login";

    /**
     * 用户鉴权
     * @param exchange
     * @param chain
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        // 1.判断用户是否是登录操作,如果是,则直接放行
        String url = request.getURI().getPath();
        if (url.startsWith(LOGINURL)) {
            return chain.filter(exchange);
        }
        //url,请求头,cookie  获取用户携带的token
        String token = request.getQueryParams().getFirst(AUTHORIZE_TOKEN);
        if (StringUtils.isEmpty(token)) {
            //如果请求头中没有携带token,在判断Cookie中是否携带token
            HttpCookie cookie = request.getCookies().getFirst(AUTHORIZE_TOKEN);
            if (cookie != null) {
                // cookie不为空,获取token的值
                token = cookie.getValue();
            }
        }
        // 判断token是否为空,为空则拒绝访问
        if (StringUtils.isEmpty(token)){
            //设置相应状态吗
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            //返回状态码
            return response.setComplete();
        }
        // token 不为空,进行鉴权
        try {
            // 鉴权通过,放行
            Claims claims = JwtUtil.parseJWT(JwtUtil.JWT_KEY);
        } catch (Exception e) {
            e.printStackTrace();
            //抛出异常,没有权限
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

配置文件配置过滤路径

spring:
  cloud:
    gateway:
      globalcors:
        corsConfigurations:
          '[/**]': # 匹配所有请求
            allowedOrigins: "*" #跨域处理 允许所有的域
            allowedMethods: # 支持的方法
              - GET
              - POST
              - PUT
              - DELETE
      routes:
        - id: changgou_goods_route
          uri: lb://goods
          predicates:
            - Path=/api/album/**,/api/brand/**,/api/cache/**,/api/categoryBrand/**,/api/category/**,/api/para/**,/api/pref/**,/api/sku/**,/api/spec/**,/api/spu/**,/api/stockBack/**,/api/template/**
          filters:
            - StripPrefix=1
            - name: RequestRateLimiter #请求数限流 名字不能随便写 ,使用默认的facatory
              args:
                key-resolver: "#{@ipKeyResolver}"
                redis-rate-limiter.replenishRate: 1
                redis-rate-limiter.burstCapacity: 1
        #用户微服务
        - id: changgou_user_route
          uri: lb://user
          predicates:
            - Path=/api/user/**,/api/address/**,/api/areas/**,/api/cities/**,/api/provinces/**
          filters:
            - StripPrefix=1
  application:
    name: gateway-web
  #Redis配置
  redis:
    host: 192.168.211.132
    port: 6379

server:
  port: 8001
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:7001/eureka
  instance:
    prefer-ip-address: true
management:
  endpoint:
    gateway:
      enabled: true
    web:
      exposure:
        include: true

测试:http://localhost:8001/api/brand

畅购商城第8章-微服务网关和Jwt令牌_第15张图片

会话保持
用户每次请求的时候,我们都需要获取令牌数据,方法有多重,可以在每次提交的时候,将数据提交到头文件中,也可以将数据存储到Cookie中,每次从Cookie中校验数据,还可以每次将令牌数据以参数的方式提交到网关,这里面采用Cookie的方式比较容易实现。

修改user微服务,每次登录的时候,添加令牌信息到Cookie中,修改changgou-service-user的com.changgou.user.controller.UserControllerlogin方法,代码如下:

/**
     * 用户登录
     * @param username
     * @param password
     * @return
     */
    @RequestMapping("/login")
    public Result userLogin(String username, String password, HttpServletResponse response){
        //判断输入的用户名是否为空
        if (!StringUtils.isEmpty(username)){
            //通过主键:用户名查找用户
            User user = userService.findById(username);
            if (user!=null){
                //用户名不为空,存在该用户,比较密码
                if (BCrypt.checkpw(password,user.getPassword())){
                    // 设置一个唯一的id即可
                    String id = UUID.randomUUID().toString();
                    // 将需要封装的信息到subject中,并转为json字符串
                    Map<String,String> maps = new HashMap<>(16);
                    maps.put("username",user.getUsername());
                    //登录状态
                    maps.put("status","success");
                    maps.put("name",user.getName());
                    String subject = JSON.toJSONString(maps);
                    //创建token令牌
                    String token = JwtUtil.createJWT(id, subject, null);
                    // 将token添加到cookie和请求头中
                    response.setHeader("Authorization",token);
                    Cookie cookie = new Cookie("Authorization",token);
                    //共享cookie
                    //写入这个参数之后所有主域名为.test.com的项目都可以调用这个cookie
                    cookie.setDomain("cloud.changgou.com");
                    //设置cookie的path
                    //path这里直接写 / 表示所有路径都可以访问
                    cookie.setPath("/");
                    //HttpServletResponse 返回cookie
                    response.addCookie(cookie);
                    //密码正确登录成功
                    return new Result(true,StatusCode.OK,"登录成功");
                }
            }
        }
        return new Result(true,StatusCode.LOGINERROR,"用户名或密码错误");
    }

测试:

未登录时:

畅购商城第8章-微服务网关和Jwt令牌_第16张图片

登录后,在测试

畅购商城第8章-微服务网关和Jwt令牌_第17张图片

畅购商城第8章-微服务网关和Jwt令牌_第18张图片

你可能感兴趣的:(畅购商城)