第8章 微服務網關和Jwt令牌
學習目標
-
掌握微服務網關的系統搭建
-
瞭解什麼是微服務網關以及它的作用
-
掌握系統中心微服務的搭建
-
掌握用戶密碼加密存儲bcrypt
-
瞭解JWT鑑權的介紹
-
掌握JWT的鑑權的使用
使用Jwt令牌來存儲用戶登錄信息,在微服務網關中識別登錄信息(用戶的身份)
-
掌握網關使用JWT進行校驗
-
掌握網關限流
1 微服務網關
1.1 微服務網關的概述
不同的微服務一般會有不同的網絡地址,而外部客戶端可能需要調用多個服務的接口才能完成一個業務需求,如果讓客戶端直接與各個微服務通信,會有以下的問題:
- 客戶端會多次請求不同的微服務,增加了客戶端的複雜性
- 存在跨域請求,在一定場景下處理相對複雜
- 認證複雜,每個服務都需要獨立認證
- 難以重構,隨着項目的迭代,可能需要重新劃分微服務。例如,可能將多個服務合併成一個或者將一個服務拆分成多個。如果客戶端直接與微服務通信,那麼重構將會很難實施
- 某些微服務可能使用了防火牆 / 瀏覽器不友好的協議,直接訪問會有一定的困難
以上這些問題可以藉助網關解決。
網關是介於客戶端和服務器端之間的中間層,所有的外部請求都會先經過 網關這一層。也就是說,API 的實現方面更多的考慮業務邏輯,而安全、性能、監控可以交由 網關來做,這樣既提高業務靈活性又不缺安全性,典型的架構圖如圖所示:
優點如下:
- 安全 ,只有網關係統對外進行暴露,微服務可以隱藏在內網,通過防火牆保護。
- 易於監控。可以在網關收集監控數據並將其推送到外部系統進行分析。
- 易於認證。可以在網關上進行認證,然後再將請求轉發到後端的微服務,而無須在每個微服務中進行認證。
- 減少了客戶端與各個微服務之間的交互次數
- 易於統一授權。
總結:微服務網關就是一個系統,通過暴露該微服務網關係統,方便我們進行相關的鑑權,安全控制,日誌統一處理,易於監控的相關功能。
1.2 微服務網關技術
實現微服務網關的技術有很多,
- nginx Nginx (tengine 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如下:
<?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:
<?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,代碼如下:
@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,代碼如下:
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,添加如下代碼:
spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]': # 匹配所有請求
allowedOrigins: "*" #跨域處理 允許所有的域
allowedMethods: # 支持的方法
- GET
- POST
- PUT
- DELETE
最終文件如下:
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服務處理,如下配置:
上圖配置如下:
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
服務處理,如下配置:
上圖配置如下:
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
,如下配置:
上圖配置如下:
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功能來實現路徑的過濾操作,如下配置:
上圖配置如下:
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配置文件,代碼如下:
上圖配置如下:
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的依賴
<!--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,如下代碼:
/***
* 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的配置,如圖
修改如下圖:
配置代碼如下:
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
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引入如下依賴:
<?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,代碼如下:
@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配置,代碼如下:
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添加登錄方法,代碼如下:
/***
* 用戶登錄
*/
@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配置文件,如下代碼:
上圖代碼如下:
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)標準中註冊的聲明(建議但不強制使用)
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中添加依賴:
<!--鑑權-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
(2)創建測試
在changgou-common的/test/java下創建測試類,並設置測試方法
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),根據這些信息查詢數據庫返回相應的結果。
/***
* 解析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
/***
* 解析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:
/***
* 解析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.用戶下次訪問,攜帶頭文件中的令牌信息即可識別是否登錄
4.5.2用戶登錄簽發TOKEN
(1)生成令牌工具類
在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();
}
}
(2) 用戶登錄成功 則 簽發TOKEN,修改登錄的方法:
代碼如下:
/***
* 用戶登錄
*/
@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代碼如下:
@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文件:
上述代碼如下:
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.UserController
的login
方法,代碼如下:
4.6.2 過濾器獲取令牌數據
每次在網關中通過過濾器獲取Cookie中的令牌,然後對令牌數據進行解析,修改微服務網關changgou-gateway-web中的AuthorizeFilter,代碼如下:
登錄後測試,可以識別用戶身份,不登錄無法識別。如下訪問http://localhost:8001/api/user
會攜帶令牌數據:
4.6.3 添加Header信息
我們還可以在Gateway的全局過濾器中添加請求頭信息,例如可以講令牌信息添加到請求頭中,在微服務中獲取頭信息,如下代碼:
修改微服務網關中的AuthorizeFilter過濾器,在令牌信息校驗那塊將令牌加入到請求頭中,如下代碼:
在changgou-service-user微服務的UserController的findAll方法中獲取請求頭測試,代碼如下:
後臺輸出令牌數據如下: