黑馬暢購商城---8.微服務網關Gateway和Jwt令牌

學習目標

  • 掌握微服務網關的系統搭建
  • 瞭解什麼是微服務網關以及它的作用
  • 掌握系統中心微服務的搭建
  • 掌握用戶密碼加密存儲bcrypt
  • 瞭解JWT鑑權的介紹
  • 掌握JWT的鑑權的使用
  • 掌握網關使用JWT進行校驗
  • 掌握網關限流

1 微服務網關

 

1.1 微服務網關的概述

不同的微服務一般會有不同的網絡地址,而外部客戶端可能需要調用多個服務的接口才能完成一個業務需求,如果讓客戶端直接與各個微服務通信,會有以下的問題:

  • 客戶端會多次請求不同的微服務,增加了客戶端的複雜性
  • 存在跨域請求,在一定場景下處理相對複雜
  • 認證複雜,每個服務都需要獨立認證
  • 難以重構,隨着項目的迭代,可能需要重新劃分微服務。例如,可能將多個服務合併成一個或者將一個服務拆分成多個。如果客戶端直接與微服務通信,那麼重構將會很難實施
  • 某些微服務可能使用了防火牆 / 瀏覽器不友好的協議,直接訪問會有一定的困難

以上這些問題可以藉助網關解決。

網關是介於客戶端和服務器端之間的中間層,所有的外部請求都會先經過 網關這一層。也就是說,API 的實現方面更多的考慮業務邏輯,而安全、性能、監控可以交由 網關來做,這樣既提高業務靈活性又不缺安全性,典型的架構圖如圖所示:

優點如下:

  • 安全 ,只有網關係統對外進行暴露,微服務可以隱藏在內網,通過防火牆保護。
  • 易於監控。可以在網關收集監控數據並將其推送到外部系統進行分析。
  • 易於認證。可以在網關上進行認證,然後再將請求轉發到後端的微服務,而無須在每個微服務中進行認證。
  • 減少了客戶端與各個微服務之間的交互次數
  • 易於統一授權。

總結:微服務網關就是一個系統,通過暴露該微服務網關係統,方便我們進行相關的鑑權,安全控制,日誌統一處理,易於監控的相關功能。

1.2 微服務網關技術

實現微服務網關的技術有很多,

  • nginx Nginx (engine x) 是一個高性能的HTTP反向代理web服務器,同時也提供了IMAP/POP3/SMTP服務
  • zuul ,Zuul 是 Netflix 出品的一個基於 JVM 路由和服務端的負載均衡器。
  • spring-cloud-gateway, 是spring 出品的 基於spring 的網關項目,集成斷路器,路徑重寫,性能比Zuul好。

我們使用gateway這個網關技術,無縫銜接到基於spring cloud的微服務開發中來。

gateway官網:

https://spring.io/projects/spring-cloud-gateway

2 網關係統使用

 

2.1 需求分析

​ 由於我們開發的系統 有包括前臺系統和後臺系統,後臺的系統 給管理員使用。那麼也需要調用各種微服務,所以我們針對 系統管理搭建一個網關係統。分析如下:

2.2 搭建後臺網關係統

2.2.1 搭建分析

由上可知道,由於 需要有多個網關,所以爲了管理方便。我們新建一個項目,打包方式爲pom,在裏面建立各種網關係統模塊即可。如圖所示:

2.2.2 工程搭建

(1)引入依賴

修改changgou-gateway工程,打包方式爲pom

pom.xml如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?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>changgou-parent</artifactId>
        <groupId>com.changgou</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>changgou-gateway</artifactId>
    <packaging>pom</packaging>
    <modules>
        <module>changgou-gateway-web</module>
    </modules>

    <!--網關依賴-->
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</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-eureka-client</artifactId>
        </dependency>
    </dependencies>

</project>

在changgou-gateway工程中,創建 changgou-gateway-web工程,該網關主要用於對後臺微服務進行一個調用操作,將多個微服務串聯到一起。

pom.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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>changgou-gateway</artifactId>
        <groupId>com.changgou</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>changgou-gateway-web</artifactId>
    <description>
        普通web請求網關
    </description>
</project>

(2)引導類

在changgou-gateway-web中創建一個引導類com.changgou.GatewayWebApplication,代碼如下:

1
2
3
4
5
6
7
8
@SpringBootApplication
@EnableEurekaClient
public class GatewayWebApplication {

    public static void main(String[] args) {
        SpringApplication.run(GatewayWebApplication.class,args);
    }
}

(3)application.yml配置

在changgou-gateway-web的resources下創建application.yml,代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
spring:
  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

2.3 跨域配置

有時候,我們需要對所有微服務跨域請求進行處理,則可以在gateway中進行跨域支持。修改application.yml,添加如下代碼:

1
2
3
4
5
6
7
8
9
10
11
12
spring:
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]': # 匹配所有請求
              allowedOrigins: "*" #跨域處理 允許所有的域
              allowedMethods: # 支持的方法
                - GET
                - POST
                - PUT
                - DELETE

最終文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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

2.4 網關過濾配置

 

路由過濾器允許以某種方式修改傳入的HTTP請求或傳出的HTTP響應。 路徑過濾器的範圍限定爲特定路徑。 Spring Cloud Gateway包含許多內置的GatewayFilter工廠。如上圖,根據請求路徑路由到不同微服務去,這塊可以使用Gateway的路由過濾功能實現。

過濾器 有 20 多個 實現 類, 包括 頭部 過濾器、 路徑 類 過濾器、 Hystrix 過濾器 和 變更 請求 URL 的 過濾器, 還有 參數 和 狀態 碼 等 其他 類型 的 過濾器。

內置的過濾器工廠有22個實現類,包括 頭部過濾器、路徑過濾器、Hystrix 過濾器 、請求URL 變更過濾器,還有參數和狀態碼等其他類型的過濾器。根據過濾器工廠的用途來劃分,可以分爲以下幾種:Header、Parameter、Path、Body、Status、Session、Redirect、Retry、RateLimiter和Hystrix。

2.4.1 Host 路由

比如用戶請求cloud.itheima.com的時候,可以將請求路由給http://localhost:18081服務處理,如下配置:

上圖配置如下:

1
2
3
4
5
      routes:
            - id: changgou_goods_route
              uri: http://localhost:18081
              predicates:
              - Host=cloud.itheima.com**

測試請求http://cloud.itheima.com:8001/brand,效果如下:

注意:此時要想讓cloud.itheima.com訪問本地計算機,要配置C:\Windows\System32\drivers\etc\hosts文件,映射配置如下:

127.0.0.1 cloud.itheima.com

2.4.2 路徑匹配過濾配置

我們還可以根據請求路徑實現對應的路由過濾操作,例如請求中以/brand/路徑開始的請求,都直接交給http://localhost:180801服務處理,如下配置:

上圖配置如下:

1
2
3
4
5
      routes:
            - id: changgou_goods_route
              uri: http://localhost:18081
              predicates:
              - Path=/brand/**

測試請求http://localhost:8001/brand,效果如下:

2.4.3 PrefixPath 過濾配置

用戶每次請求路徑的時候,我們可以給真實請求加一個統一前綴,例如用戶請求http://localhost:8001的時候我們讓它請求真實地址http://localhost:8001/brand,如下配置:

上圖配置如下:

1
2
3
4
5
6
7
8
      routes:
            - id: changgou_goods_route
              uri: http://localhost:18081
              predicates:
              #- Host=cloud.itheima.com**
              - Path=/**
              filters:
              - PrefixPath=/brand

測試請求http://localhost:8001/效果如下:

2.4.4 StripPrefix 過濾配置

很多時候也會有這麼一種請求,用戶請求路徑是/api/brand,而真實路徑是/brand,這時候我們需要去掉/api纔是真實路徑,此時可以使用SttripPrefix功能來實現路徑的過濾操作,如下配置:

上圖配置如下:

1
2
3
4
5
6
7
8
9
      routes:
            - id: changgou_goods_route
              uri: http://localhost:18081
              predicates:
              #- Host=cloud.itheima.com**
              - Path=/**
              filters:
              #- PrefixPath=/brand
              - StripPrefix=1

測試請求http://localhost:8001/api/brand,效果如下:

2.4.5 LoadBalancerClient 路由過濾器(客戶端負載均衡)

上面的路由配置每次都會將請求給指定的URL處理,但如果在以後生產環境,併發量較大的時候,我們需要根據服務的名稱判斷來做負載均衡操作,可以使用LoadBalancerClientFilter來實現負載均衡調用。LoadBalancerClientFilter會作用在url以lb開頭的路由,然後利用loadBalancer來獲取服務實例,構造目標requestUrl,設置到GATEWAY_REQUEST_URL_ATTR屬性中,供NettyRoutingFilter使用。

修改application.yml配置文件,代碼如下:

上圖配置如下:

1
2
3
4
5
6
7
8
9
10
      routes:
            - id: changgou_goods_route
              #uri: http://localhost:18081
              uri: lb://goods
              predicates:
              #- Host=cloud.itheima.com**
              - Path=/**
              filters:
              #- PrefixPath=/brand
              - StripPrefix=1

測試請求路徑http://localhost:8001/api/brand

2.5 網關限流

 

網關可以做很多的事情,比如,限流,當我們的系統 被頻繁的請求的時候,就有可能 將系統壓垮,所以 爲了解決這個問題,需要在每一個微服務中做限流操作,但是如果有了網關,那麼就可以在網關係統做限流,因爲所有的請求都需要先通過網關係統才能路由到微服務中。

2.5.1 思路分析

2.5.2 令牌桶算法

 

令牌桶算法是比較常見的限流算法之一,大概描述如下:
1)所有的請求在處理之前都需要拿到一個可用的令牌纔會被處理;
2)根據限流大小,設置按照一定的速率往桶裏添加令牌;
3)桶設置最大的放置令牌限制,當桶滿時、新添加的令牌就被丟棄或者拒絕;
4)請求達到後首先要獲取令牌桶中的令牌,拿着令牌纔可以進行其他的業務邏輯,處理完業務邏輯之後,將令牌直接刪除;
5)令牌桶有最低限額,當桶中的令牌達到最低限額的時候,請求處理完之後將不會刪除令牌,以此保證足夠的限流

如下圖:

這個算法的實現,有很多技術,Guaua是其中之一,redis客戶端也有其實現。

2.5.3 使用令牌桶進行請求次數限流

spring cloud gateway 默認使用redis的RateLimter限流算法來實現。所以我們要使用首先需要引入redis的依賴

(1)引入redis依賴

在changgou-gateway的pom.xml中引入redis的依賴

1
2
3
4
5
6
<!--redis-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
    <version>2.1.3.RELEASE</version>
</dependency>

(2)定義KeyResolver

在Applicatioin引導類中添加如下代碼,KeyResolver用於計算某一個類型的限流的KEY也就是說,可以通過KeyResolver來指定限流的Key。

我們可以根據IP來限流,比如每個IP每秒鐘只能請求一次,在GatewayWebApplication定義key的獲取,獲取客戶端IP,將IP作爲key,如下代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/***
 * IP限流
 * @return
 */
@Bean(name="ipKeyResolver")
public KeyResolver userKeyResolver() {
    return new KeyResolver() {
        @Override
        public Mono<String> resolve(ServerWebExchange exchange) {
            //獲取遠程客戶端IP
            String hostName = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress();
            System.out.println("hostName:"+hostName);
            return Mono.just(hostName);
        }
    };
}

(3)修改application.yml中配置項,指定限制流量的配置以及REDIS的配置,如圖

修改如下圖:

配置代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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 #請求數限流 名字不能隨便寫 ,使用默認的facatory
                args:
                  key-resolver: "#{@ipKeyResolver}"
                  redis-rate-limiter.replenishRate: 1
                  redis-rate-limiter.burstCapacity: 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

解釋:

redis-rate-limiter.replenishRate是您希望允許用戶每秒執行多少請求,而不會丟棄任何請求。這是令牌桶填充的速率

redis-rate-limiter.burstCapacity是指令牌桶的容量,允許在一秒鐘內完成的最大請求數,將此值設置爲零將阻止所有請求。

key-resolver: “#{@ipKeyResolver}” 用於通過SPEL表達式來指定使用哪一個KeyResolver.

如上配置:

表示 一秒內,允許 一個請求通過,令牌桶的填充速率也是一秒鐘添加一個令牌。

最大突發狀況 也只允許 一秒內有一次請求,可以根據業務來調整 。

多次請求會發生如下情況

3 用戶登錄

項目中有2個重要角色,分別爲管理員和用戶,下面幾章我們將實現購物下單和支付,用戶如果沒登錄是沒法下單和支付的,所以我們這裏需要實現一個登錄功能。

3.1 表結構介紹

changgou_user表如下:

用戶信息表tb_user

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
CREATE TABLE `tb_user` (
  `username` varchar(50) NOT NULL COMMENT '用戶名',
  `password` varchar(100) NOT NULL COMMENT '密碼,加密存儲',
  `phone` varchar(20) DEFAULT NULL COMMENT '註冊手機號',
  `email` varchar(50) DEFAULT NULL COMMENT '註冊郵箱',
  `created` datetime NOT NULL COMMENT '創建時間',
  `updated` datetime NOT NULL COMMENT '修改時間',
  `source_type` varchar(1) DEFAULT NULL COMMENT '會員來源:1:PC,2:H5,3:Android,4:IOS',
  `nick_name` varchar(50) DEFAULT NULL COMMENT '暱稱',
  `name` varchar(50) DEFAULT NULL COMMENT '真實姓名',
  `status` varchar(1) DEFAULT NULL COMMENT '使用狀態(1正常 0非正常)',
  `head_pic` varchar(150) DEFAULT NULL COMMENT '頭像地址',
  `qq` varchar(20) DEFAULT NULL COMMENT 'QQ號碼',
  `is_mobile_check` varchar(1) DEFAULT '0' COMMENT '手機是否驗證 (0否  1是)',
  `is_email_check` varchar(1) DEFAULT '0' COMMENT '郵箱是否檢測(0否  1是)',
  `sex` varchar(1) DEFAULT '1' COMMENT '性別,1男,0女',
  `user_level` int(11) DEFAULT NULL COMMENT '會員等級',
  `points` int(11) DEFAULT NULL COMMENT '積分',
  `experience_value` int(11) DEFAULT NULL COMMENT '經驗值',
  `birthday` datetime DEFAULT NULL COMMENT '出生年月日',
  `last_login_time` datetime DEFAULT NULL COMMENT '最後登錄時間',
  PRIMARY KEY (`username`),
  UNIQUE KEY `username` (`username`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用戶表';

3.2 用戶微服務創建

創建工程之前,先使用代碼生成器生成對應的業務代碼。

(1)公共API創建

在changgou-service-api中創建changgou-service-user-api,並將pojo拷貝到工程中,如下圖:

在changgou-service中創建changgou-service-user微服務,並引入生成的業務邏輯代碼,如下圖:

(2)依賴

在changgou-service-user的pom.xml引入如下依賴:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?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>changgou-service</artifactId>
        <groupId>com.changgou</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>changgou-service-user</artifactId>

    <!--依賴-->
    <dependencies>
        <dependency>
            <groupId>com.changgou</groupId>
            <artifactId>changgou-service-user-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>
</project>

(3)啓動類創建

在changgou-service-user微服務中創建啓動類com.changgou.UserApplication,代碼如下:

1
2
3
4
5
6
7
8
9
@SpringBootApplication
@EnableEurekaClient
@MapperScan("com.changgou.user.dao")
public class UserApplication {

    public static void main(String[] args) {
        SpringApplication.run(UserApplication.class,args);
    }
}

(4)application.yml配置

在changgou-service-user的resources中創建application.yml配置,代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
server:
  port: 18089
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

3.3 登錄

登錄的時候,需要進行密碼校驗,這裏採用了BCryptPasswordEncoder進行加密,需要將資料中的BCrypt導入到common工程中,其中BCrypt.checkpw(“明文”,“密文”)用於對比密碼是否一致。

修改changgou-service-user的com.changgou.user.controller.UserController添加登錄方法,代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
/***
 * 用戶登錄
 */
@RequestMapping(value = "/login")
public Result login(String username,String password){
    //查詢用戶信息
    User user = userService.findById(username);

    if(user!=null && BCrypt.checkpw(password,user.getPassword())){
        return new Result(true,StatusCode.OK,"登錄成功!",user);
    }
    return  new Result(false,StatusCode.LOGINERROR,"賬號或者密碼錯誤!");
}

注意:這裏密碼進行了加密。

使用Postman測試如下:

3.4 網關關聯

在我們平時工作中,並不會直接將微服務暴露出去,一般都會使用網關對接,實現對微服務的一個保護作用,如上圖,當用戶訪問/api/user/的時候我們再根據用戶請求調用用戶微服務的指定方法。當然,除了/api/user/還有/api/address//api/areas//api/cities//api/provinces/都需要由user微服務處理,修改網關工程changgou-gateway-web的application.yml配置文件,如下代碼:

上圖代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
spring:
  cloud:
    gateway:
      globalcors:
        corsConfigurations:
          '[/**]': # 匹配所有請求
              allowedOrigins: "*" #跨域處理 允許所有的域
              allowedMethods: # 支持的方法
                - GET
                - POST
                - PUT
                - DELETE
      routes:
            - id: changgou_goods_route
              uri: lb://goods
              predicates:
              - Path=/api/goods/**
              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

使用Postman訪問http://localhost:8001/api/user/login?username=changgou&password=changgou,效果如下:

4 JWT講解

4.1 需求分析

我們之前已經搭建過了網關,使用網關在網關係統中比較適合進行權限校驗。

那麼我們可以採用JWT的方式來實現鑑權校驗。

4.2 什麼是JWT

JSON Web Token(JWT)是一個非常輕巧的規範。這個規範允許我們使用JWT在用戶和服務器之間傳遞安全可靠的信息。

4.3 JWT的構成

一個JWT實際上就是一個字符串,它由三部分組成,頭部、載荷與簽名。

頭部(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 中提供了非常方便的 **BASE64Encoder** 和 **BASE64Decoder**,用它們可以非常方便的完成基於 BASE64 的編碼和解碼

載荷(playload)

載荷就是存放有效信息的地方。這個名字像是特指飛機上承載的貨品,這些有效信息包含三個部分

(1)標準中註冊的聲明(建議但不強制使用)

1
2
3
4
5
6
7
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了。

4.4 JJWT的介紹和使用

JJWT是一個提供端到端的JWT創建和驗證的Java庫。永遠免費和開源(Apache License,版本2.0),JJWT很容易使用和理解。它被設計成一個以建築爲中心的流暢界面,隱藏了它的大部分複雜性。

官方文檔:

https://github.com/jwtk/jjwt

4.4.1 創建TOKEN

(1)依賴引入

在changgou-parent項目中的pom.xml中添加依賴:

1
2
3
4
5
6
<!--鑑權-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

(2)創建測試

在changgou-common的/test/java下創建測試類,並設置測試方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class JwtTest {

    /****
     * 創建Jwt令牌
     */
    @Test
    public void testCreateJwt(){
        JwtBuilder builder= Jwts.builder()
                .setId("888")             //設置唯一編號
                .setSubject("小白")       //設置主題  可以是JSON數據
                .setIssuedAt(new Date())  //設置簽發日期
                .signWith(SignatureAlgorithm.HS256,"itcast");//設置簽名 使用HS256算法,並設置SecretKey(字符串)
        //構建 並返回一個字符串
        System.out.println( builder.compact() );
    }
}

運行打印結果:

eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NjIwNjIyODd9.RBLpZ79USMplQyfJCZFD2muHV_KLks7M1ZsjTu6Aez4

再次運行,會發現每次運行的結果是不一樣的,因爲我們的載荷中包含了時間。

4.4.2 TOKEN解析

我們剛纔已經創建了token ,在web應用中這個操作是由服務端進行然後發給客戶端,客戶端在下次向服務端發送請求時需要攜帶這個token(這就好像是拿着一張門票一樣),那服務端接到這個token 應該解析出token中的信息(例如用戶id),根據這些信息查詢數據庫返回相應的結果。

1
2
3
4
5
6
7
8
9
10
11
12
/***
 * 解析Jwt令牌數據
 */
@Test
public void testParseJwt(){
    String compactJwt="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NjIwNjIyODd9.RBLpZ79USMplQyfJCZFD2muHV_KLks7M1ZsjTu6Aez4";
    Claims claims = Jwts.parser().
            setSigningKey("itcast").
            parseClaimsJws(compactJwt).
            getBody();
    System.out.println(claims);
}

運行打印效果:

{jti=888, sub=小白, iat=1562062287}

試着將token或簽名祕鑰篡改一下,會發現運行時就會報錯,所以解析token也就是驗證token.

4.4.3 設置過期時間

有很多時候,我們並不希望簽發的token是永久生效的,所以我們可以爲token添加一個過期時間。

4.4.3.1 token過期設置

解釋:

.setExpiration(date)//用於設置過期時間 ,參數爲Date類型數據

運行,打印效果如下:

eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NjIwNjI5MjUsImV4cCI6MTU2MjA2MjkyNX0._vs4METaPkCza52LuN0-2NGGWIIO7v51xt40DHY1U1Q

4.4.3.2 解析TOKEN

1
2
3
4
5
6
7
8
9
10
11
12
/***
 * 解析Jwt令牌數據
 */
@Test
public void testParseJwt(){
    String compactJwt="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NjIwNjI5MjUsImV4cCI6MTU2MjA2MjkyNX0._vs4METaPkCza52LuN0-2NGGWIIO7v51xt40DHY1U1Q";
    Claims claims = Jwts.parser().
            setSigningKey("itcast").
            parseClaimsJws(compactJwt).
            getBody();
    System.out.println(claims);
}

打印效果:

當前時間超過過期時間,則會報錯。

4.4.4 自定義claims

我們剛纔的例子只是存儲了id和subject兩個信息,如果你想存儲更多的信息(例如角色)可以定義自定義claims。

創建測試類,並設置測試方法:

創建token:

運行打印效果:

eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NjIwNjMyOTIsImFkZHJlc3MiOiLmt7HlnLPpu5Hpqazorq3nu4PokKXnqIvluo_lkZjkuK3lv4MiLCJuYW1lIjoi546L5LqUIiwiYWdlIjoyN30.ZSbHt5qrxz0F1Ma9rVHHAIy4jMCBGIHoNaaPQXxV_dk

解析TOKEN:

1
2
3
4
5
6
7
8
9
10
11
12
/***
 * 解析Jwt令牌數據
 */
@Test
public void testParseJwt(){
    String compactJwt="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NjIwNjMyOTIsImFkZHJlc3MiOiLmt7HlnLPpu5Hpqazorq3nu4PokKXnqIvluo_lkZjkuK3lv4MiLCJuYW1lIjoi546L5LqUIiwiYWdlIjoyN30.ZSbHt5qrxz0F1Ma9rVHHAIy4jMCBGIHoNaaPQXxV_dk";
    Claims claims = Jwts.parser().
            setSigningKey("itcast").
            parseClaimsJws(compactJwt).
            getBody();
    System.out.println(claims);
}

運行效果:

4.5 鑑權處理

4.5.1 思路分析

1
2
3
4
5
6
7
8
1.用戶通過訪問微服務網關調用微服務,同時攜帶頭文件信息
2.在微服務網關這裏進行攔截,攔截後獲取用戶要訪問的路徑
3.識別用戶訪問的路徑是否需要登錄,如果需要,識別用戶的身份是否能訪問該路徑[這裏可以基於數據庫設計一套權限]
4.如果需要權限訪問,用戶已經登錄,則放行
5.如果需要權限訪問,且用戶未登錄,則提示用戶需要登錄
6.用戶通過網關訪問用戶微服務,進行登錄驗證
7.驗證通過後,用戶微服務會頒發一個令牌給網關,網關會將用戶信息封裝到頭文件中,並響應用戶
8.用戶下次訪問,攜帶頭文件中的令牌信息即可識別是否登錄

4.5.2用戶登錄簽發TOKEN

(1)生成令牌工具類

在changgou-common中創建類entity.JwtUtil,主要輔助生成Jwt令牌信息,代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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();
    }
}

(2) 用戶登錄成功 則 簽發TOKEN,修改登錄的方法:

代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/***
 * 用戶登錄
 */
@RequestMapping(value = "/login")
public Result login(String username,String password){
    //查詢用戶信息
    User user = userService.findById(username);

    if(user!=null && BCrypt.checkpw(password,user.getPassword())){
        //設置令牌信息
        Map<String,Object> info = new HashMap<String,Object>();
        info.put("role","USER");
        info.put("success","SUCCESS");
        info.put("username",username);
        //生成令牌
        String jwt = JwtUtil.createJWT(UUID.randomUUID().toString(), JSON.toJSONString(info),null);
        return new Result(true,StatusCode.OK,"登錄成功!",jwt);
    }
    return  new Result(false,StatusCode.LOGINERROR,"賬號或者密碼錯誤!");
}

4.5.3 網關過濾器攔截請求處理

拷貝JwtUtil到changgou-gateway-web中

4.5.4 自定義全局過濾器

創建 過濾器類,如圖所示:

AuthorizeFilter代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
@Component
public class AuthorizeFilter implements GlobalFilter, Ordered {

    //令牌頭名字
    private static final String AUTHORIZE_TOKEN = "Authorization";

    /***
     * 全局過濾器
     * @param exchange
     * @param chain
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //獲取Request、Response對象
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();

        //獲取請求的URI
        String path = request.getURI().getPath();

        //如果是登錄、goods等開放的微服務[這裏的goods部分開放],則直接放行,這裏不做完整演示,完整演示需要設計一套權限系統
        if (path.startsWith("/api/user/login") || path.startsWith("/api/brand/search/")) {
            //放行
            Mono<Void> filter = chain.filter(exchange);
            return filter;
        }

        //獲取頭文件中的令牌信息
        String tokent = request.getHeaders().getFirst(AUTHORIZE_TOKEN);

        //如果頭文件中沒有,則從請求參數中獲取
        if (StringUtils.isEmpty(tokent)) {
            tokent = request.getQueryParams().getFirst(AUTHORIZE_TOKEN);
        }

        //如果爲空,則輸出錯誤代碼
        if (StringUtils.isEmpty(tokent)) {
            //設置方法不允許被訪問,405錯誤代碼
           response.setStatusCode(HttpStatus.METHOD_NOT_ALLOWED);
           return response.setComplete();
        }

        //解析令牌數據
        try {
            Claims claims = JwtUtil.parseJWT(tokent);
        } catch (Exception e) {
            e.printStackTrace();
            //解析失敗,響應401錯誤
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }

        //放行
        return chain.filter(exchange);
    }


    /***
     * 過濾器執行順序
     * @return
     */
    @Override
    public int getOrder() {
        return 0;
    }
}

4.5.5 配置過濾規則

修改網關係統的yml文件:

上述代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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/user/login?username=changgou&password=changgou,效果如下:

測試訪問http://localhost:8001/api/user,效果如下:

參考官方手冊:

https://cloud.spring.io/spring-cloud-gateway/spring-cloud-gateway.html#_stripprefix_gatewayfilter_factory

4.6 會話保持

 

用戶每次請求的時候,我們都需要獲取令牌數據,方法有多重,可以在每次提交的時候,將數據提交到頭文件中,也可以將數據存儲到Cookie中,每次從Cookie中校驗數據,還可以每次將令牌數據以參數的方式提交到網關,這裏面採用Cookie的方式比較容易實現。

4.6.1 登錄封裝Cookie

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

4.6.2 過濾器獲取令牌數據

每次在網關中通過過濾器獲取Cookie中的令牌,然後對令牌數據進行解析,修改微服務網關changgou-gateway-web中的AuthorizeFilter,代碼如下:

登錄後測試,可以識別用戶身份,不登錄無法識別。如下訪問http://localhost:8001/api/user會攜帶令牌數據:

4.6.3 添加Header信息

我們還可以在Gateway的全局過濾器中添加請求頭信息,例如可以講令牌信息添加到請求頭中,在微服務中獲取頭信息,如下代碼:

修改微服務網關中的AuthorizeFilter過濾器,在令牌信息校驗那塊將令牌加入到請求頭中,如下代碼:

在changgou-service-user微服務的UserController的findAll方法中獲取請求頭測試,代碼如下:

後臺輸出令牌數據如下:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章