微服務網關GateWay
Zuul 1.x 是一個基于阻塞 IO 的 API Gateway 以及 Servlet;直到 2018 年 5 月,Zuul 2.x(基于Netty,也是非阻塞的,支援長連接配接)才釋出,但 Spring Cloud 暫時還沒有整合計劃。Spring Cloud Gateway 比 Zuul 1.x 系列的性能和功能整體要好。
Gateway簡介
Spring Cloud Gateway 是 Spring 官方基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技術開的網關,旨在為微服務架構提供一種簡單而有效的統一的 API 路由管理方式,統一通路接口。SpringCloud Gateway 作為 Spring Cloud 生态系中的網關,目标是替代 Netflflix ZUUL,其不僅提供統一的路由方式,并且基于 Filter 鍊的方式提供了網關基本的功能,例如:安全,監控/埋點,和限流等。它是基于Nttey的響應式開發模式。

上表為Spring Cloud Gateway與Zuul的性能對比,從結果可知,Spring Cloud Gateway的RPS是Zuul的1.6倍
核心概念
- 路由(route) 路由是網關最基礎的部分,路由資訊由一個ID、一個目的URL、一組斷言工廠和一 組Filter組成。如果斷言為真,則說明請求URL和配置的路由比對。
-
斷言(predicates) Java8中的斷言函數,Spring Cloud Gateway中的斷言函數輸入類型是
Spring5.0架構中的ServerWebExchange。Spring Cloud Gateway中的斷言函數允許開發者去定義比對來自Http Request中的任何資訊,比如請求頭和參數等。
- 過濾器(fifilter) 一個标準的Spring webFilter,Spring Cloud Gateway中的Filter分為兩種類型,分别是Gateway Filter和Global Filter。過濾器Filter可以對請求和響應進行處理。
入門案例
建立工程導入依賴
在項目中添加新的子產品 gateway_server ,并導入依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
啟動時和spring-boot-startet-web有沖突, 現在将父工程子產品寫的jar轉移到,那個子產品用拷貝的那個子產品下。
配置啟動類
@SpringBootApplication
public class GatewayServerApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayServerApplication.class, args);
}
}
編寫配置檔案
建立 application.yml 配置檔案
server:
port: 8080
eureka:
client:
service-url:
defaultZone: http://localhost:9000/eureka/
instance:
prefer-ip-address: true
spring:
application:
name: gateway-notzull-server
cloud:
gateway:
routes:
- id: user-server
uri: http://127.0.0.1:9004
predicates:
- Path=/user/**
id:我們自定義的路由 ID,保持唯一
uri:目标服務位址
predicates:路由條件,Predicate 接受一個輸入參數,傳回一個布爾值結果。該接口包含多種默
認方法來将 Predicate 組合成其他複雜的邏輯(比如:與,或,非)。
filters:過濾規則,暫時沒用。
上面這段配置的意思是,配置了一個 id 為 user-server的路由規則,當通路網關請求位址以
user開頭時,會自動轉發到位址: http://127.0.0.1:9002/ 。配置完成啟動項目即可在浏覽器
通路進行測試,當我們通路位址 http://localhost:8080/user/show?uid=2 時會展示頁面展示如下:
原始頁面
網關請求
路由規則
Spring Cloud Gateway 的功能很強大,前面我們隻是使用了 predicates 進行了簡單的條件比對,其實 Spring Cloud Gataway 幫我們内置了很多 Predicates 功能。在 Spring Cloud Gateway 中 Spring 利用Predicate 的特性實作了各種路由比對規則,有通過 Header、請求參數等不同的條件來進行作為條件比對到對應的路由。
示例
#路由斷言之後比對
spring:
cloud:
gateway:
routes:
- id: after_route
uri: https://xxxx.com
#路由斷言之前比對
predicates:
- After=xxxxx
#路由斷言之前比對
- id: before_route
uri: https://xxxxxx.com
predicates:
- Before=xxxxxxx
#路由斷言之間
- id: between_route
uri: https://xxxx.com
predicates:
- Between=xxxx,xxxx
#路由斷言Cookie比對,此predicate比對給定名稱(chocolate)和正規表達式(ch.p)
- id: cookie_route
uri: https://xxxx.com
predicates:
- Cookie=chocolate, ch.p
#路由斷言Header比對,header名稱比對X-Request-Id,且正規表達式比對\d+
- id: header_route
uri: https://xxxx.com
predicates:
- Header=X-Request-Id, \d+
#路由斷言比對Host比對,比對下面Host主機清單,**代表可變參數
- id: host_route
uri: https://xxxx.com
predicates:
- Host=**.somehost.org,**.anotherhost.org
#路由斷言Method比對,比對的是請求的HTTP方法
- id: method_route
uri: https://xxxx.com
predicates:
- Method=GET
#路由斷言比對,{segment}為可變參數
- id: host_route
uri: https://xxxx.com
predicates:
- Path=/foo/{segment},/bar/{segment}
#路由斷言Query比對,将請求的參數param(baz)進行比對,也可以進行regexp正規表達式比對 (參數包含 foo,并且foo的值比對ba.)
- id: query_route
uri: https://xxxx.com
predicates:
- Query=baz 或 Query=foo,ba.
#路由斷言RemoteAddr比對,将比對192.168.1.1~192.168.1.254之間的ip位址,其中24為子網路遮罩位數即255.255.255.0
- id: remoteaddr_route
uri: https://example.org
predicates:
- RemoteAddr=192.168.1.1/24
動态路由
和zuul網關類似,在SpringCloud GateWay中也支援動态路由:即自動的從注冊中心中擷取服務清單并通路。
添加注冊中心依賴
在工程的pom檔案中添加注冊中心的用戶端依賴(這裡以Eureka為例)
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
配置動态路由
修改 application.yml 配置檔案,添加eureka注冊中心的相關配置,并修改通路映射的URL為服務名稱
server:
port: 8080
eureka:
client:
service-url:
defaultZone: http://localhost:9000/eureka/
instance:
prefer-ip-address: true #顯示浏覽器中的狀态欄顯示ip
spring:
application:
name: gateway-notzull-server
cloud:
gateway:
routes:
- id: user-server
# uri: http://127.0.0.1:9004
uri: lb://user-consumer-fegin
predicates:
- Path=/user/**
重寫轉發路徑
在SpringCloud Gateway中,路由轉發是直接将比對的路由path直接拼接到映射路徑(URI)之後,那麼在微服務開發中往往沒有那麼便利。這裡就可以通過RewritePath機制來進行路徑重寫。
案例改造
修改 application.yml ,将比對路徑改為 /user-service/**
添加RewritePath重寫轉發路徑
修改 application.yml ,添加重寫規則
spring:
application:
name: gateway-notzull-server
cloud:
gateway:
routes:
- id: user-server
# uri: http://127.0.0.1:9004
uri: lb://user-consumer-fegin
predicates:
- Path=/user-server/**
filters:
- RewritePath=/user-server/(?<segment>.*), /$\{segment}
通過RewritePath配置重寫轉發的url,将/product-service/(?.*),重寫為{segment},然後轉發到訂單微服務。比如在網頁上請求http://localhost:8080/user-service/user,此時會将請求轉發到http://127.0.0.1:9002/user/1( 值得注意的是在yml文檔中 $ 要寫成 $\ )
過濾器
Spring Cloud Gateway除了具備請求路由功能之外,也支援對請求的過濾。通過Zuul網關類似,也是通過過濾器的形式來實作的。那麼接下來我們一起來研究一下Gateway中的過濾器
過濾器基礎
過濾器的生命周期
Spring Cloud Gateway 的 Filter 的生命周期不像 Zuul 的那麼豐富,它隻有兩個:“pre” 和 “post”。
PRE: 這種過濾器在請求被路由之前調用。我們可利用這種過濾器實作身份驗證、在叢集中選擇
請求的微服務、記錄調試資訊等。
POST:這種過濾器在路由到微服務以後執行。這種過濾器可用來為響應添加标準的 HTTP Header、收集統計資訊和名額、将響應從微服務發送給用戶端等
(2) 過濾器類型
Spring Cloud Gateway 的 Filter 從作用範圍可分為另外兩種GatewayFilter 與 GlobalFilter。
GatewayFilter:應用到單個路由或者一個分組的路由上。
GlobalFilter:應用到所有的路由上。
局部過濾器
局部過濾器(GatewayFilter),是針對單個路由的過濾器。可以對通路的URL過濾,進行切面處理。在Spring Cloud Gateway中通過GatewayFilter的形式内置了很多不同類型的局部過濾器。這裡簡單将Spring Cloud Gateway内置的所有過濾器工廠整理成了一張表格,雖然不是很詳細,但能作為速覽使用。如下:
每個過濾器工廠都對應一個實作類,并且這些類的名稱必須以 GatewayFilterFactory 結尾,這是
Spring Cloud Gateway的一個約定,例如 AddRequestHeader 對應的實作類為
AddRequestHeaderGatewayFilterFactory 。對于這些過濾器的使用方式可以參考官方文檔
全局過濾器
全局過濾器(GlobalFilter)作用于所有路由,Spring Cloud Gateway 定義了Global Filter接口,使用者可以自定義實作自己的Global Filter。通過全局過濾器可以實作對權限的統一校驗,安全性驗證等功能,并且全局過濾器也是程式員使用比較多的過濾器。
Spring Cloud Gateway内部也是通過一系列的内置全局過濾器對整個路由轉發進行處理如下:
統一鑒權
内置的過濾器已經可以完成大部分的功能,但是對于企業開發的一些業務功能處理,還是需要我們自己編寫過濾器來實作的,那麼我們一起通過代碼的形式自定義一個過濾器,去完成統一的權限校驗。
鑒權邏輯
開發中的鑒權邏輯:
當用戶端第一次請求服務時,服務端對使用者進行資訊認證(登入)認證通過,将使用者資訊進行加密形成token,傳回給用戶端,作為登入憑證 以後每次請求,用戶端都攜帶認證的token 服務端對token進行解密,判斷是否有效。
如上圖,對于驗證使用者是否已經登入鑒權的過程可以在網關層統一檢驗。檢驗的标準就是請求中是否攜帶token憑證以及token的正确性。
代碼實作
下面的我們自定義一個GlobalFilter,去校驗所有請求的請求參數中是否包含“token”,如何不包含請求參數“token”則不轉發路由,否則執行正常的邏輯。
@Component
public class LoginFilter implements GlobalFilter , Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getQueryParams().getFirst("token");
if(token==null || "".equals(token)){
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
自定義全局過濾器需要實作GlobalFilter和Ordered接口。
在fifilter方法中完成過濾器的邏輯判斷處理
在getOrder方法指定此過濾器的優先級,傳回值越大級别越低
ServerWebExchange 就相當于目前請求和響應的上下文,存放着重要的請求-響應屬性、請求實
例和響應執行個體等等。一個請求中的request,response都可以通過 ServerWebExchange 擷取
調用 chain.filter 繼續向下遊執行
基于Filter的限流
SpringCloudGateway官方就提供了基于令牌桶的限流支援。基于其内置的過濾器工廠
RequestRateLimiterGatewayFilterFactory 實作。在過濾器工廠中是通過Redis和lua腳本結合的方式進行流量控制。
環境搭建
導入redis的依賴
首先在工程的pom檔案中引入gateway的起步依賴和redis的reactive依賴,代碼如下:
<!--監控依賴-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--redis的依賴-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
準備redis
修改application.yml配置檔案
在application.yml配置檔案中加入限流的配置,代碼如下:
server:
port: 8080
eureka:
client:
service-url:
defaultZone: http://localhost:9000/eureka/
instance:
prefer-ip-address: true #顯示浏覽器中的狀态欄顯示ip
spring:
application:
name: gateway-notzull-server
cloud:
gateway:
routes:
- id: user-server
# uri: http://127.0.0.1:9004
uri: lb://user-consumer-fegin
predicates:
- Path=/user-server/**
filters:
- RewritePath=/user-server/(?<segment>.*), /$\{segment}
- name: RequestRateLimiter
args:
key-resolver: '#{@pathKeyResolver}' # 使用SpEL從容器中擷取對象
redis-rate-limiter.replenishRate: 1 # 令牌桶每秒填充平均速率
redis-rate-limiter.burstCapacity: 3 # 令牌桶的總容量
redis:
host: 127.0.0.1
port: 6379
database: 0
在 application.yml 中添加了redis的資訊,并配置了RequestRateLimiter的限流過濾器:
burstCapacity,令牌桶總容量。
replenishRate,令牌桶每秒填充平均速率。
key-resolver,用于限流的鍵的解析器的 Bean 對象的名字。它使用 SpEL 表達式根據#
{@beanName}從 Spring 容器中擷取 Bean 對象。
配置KeyResolver
為了達到不同的限流效果和規則,可以通過實作 KeyResolver 接口,定義不同請求類型的限流鍵。
@Configuration
public class LimitFilter {
@Bean
public KeyResolver pathKeyResolver(){
return new KeyResolver() {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().getPath().toString());
}
};
}
}
根據使用者限流
@Bean
public KeyResolver userKeyResolver(){
return new KeyResolver() {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().getQueryParams().getFirst("uid"));
}
};
}
Yml配置
spring:
application:
name: gateway-notzull-server
cloud:
gateway:
routes:
- id: user-server
# uri: http://127.0.0.1:9004
uri: lb://user-consumer-fegin
predicates:
- Path=/user-server/**
filters:
- RewritePath=/user-server/(?<segment>.*), /$\{segment}
- name: RequestRateLimiter
args:
# key-resolver: '#{@pathKeyResolver}' # 使用SpEL從容器中擷取對象
key-resolver: '#{@userKeyResolver}' # 使用SpEL從容器中擷取對象
redis-rate-limiter.replenishRate: 1 # 令牌桶每秒填充平均速率
redis-rate-limiter.burstCapacity: 3 # 令牌桶的總容量
微服務網關和Jwt令牌
使用者登入
項目中有2個重要角色,分别為管理者和使用者,我們将實作購物下單和支付,使用者如果沒登入是沒法下單和支付的,是以我們這裡需要實作一個登入功能。
網關關聯
在我們平時工作中,并不會直接将微服務暴露出去,一般都會使用網關對接,實作對微服務的一個保護作用,如上圖,當使用者通路/api/user/的時候我們再根據使用者請求調用使用者微服務的指定方法。當然,除了/api/user/還有/api/address/、/api/areas/、/api/cities/、/api/provinces/都需要由user微服務處理,修改網關工程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:
- name: RequestRateLimiter
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/**
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=username&password=password
JWT講解
需求分析
我們之前已經搭建過了網關,使用網關在網關系統中比較适合進行權限校驗。
那麼我們可以采用JWT的方式來實作鑒權校驗。
什麼是JWT
JSON Web Token(JWT)是一個非常輕巧的規範。這個規範允許我們使用JWT在使用者和伺服器之間傳遞安全可靠的資訊。
JWT的構成
一個JWT實際上就是一個字元串,它由三部分組成,頭部、載荷與簽名。
頭部(Header)
頭部用于描述關于該JWT的最基本的資訊,例如其類型以及簽名所用的算法等。這也可以被表示成一個JSON對象。
在頭部指明了簽名算法是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了。
JJWT的介紹和使用
JJWT是一個提供端到端的JWT建立和驗證的Java庫。永遠免費和開源(Apache License,版本2.0),JJWT很容易使用和了解。它被設計成一個以建築為中心的流暢界面,隐藏了它的大部分複雜性。
建立TOKEN
依賴引入
在common-api項目中的pom.xml中添加依賴:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
建立測試
在common-api的/test/java下建立測試類,并設定測試方法
public class JwtTest {
/****
* 建立Jwt令牌
*/
@Test
public void testCreateJwt(){
JwtBuilder builder= Jwts.builder()
.setId("888") //設定唯一編号
.setSubject("小白") //設定主題 可以是JSON資料
.setIssuedAt(new Date()) //設定簽發日期
.signWith(SignatureAlgorithm.HS256,"xzzb");//設定簽名 使用HS256算法,并設定SecretKey(字元串)
//建構 并傳回一個字元串
System.out.println( builder.compact() );
}
}
運作列印結果:
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NjIwNjIyODd9.RBLpZ79USMplQyfJCZFD2muHV_KLks7M1ZsjTu6Aez4
再次運作,會發現每次運作的結果是不一樣的,因為我們的載荷中包含了時間。
TOKEN解析
我們剛才已經建立了token ,在web應用中這個操作是由服務端進行然後發給用戶端,用戶端在下次向服務端發送請求時需要攜帶這個token(這就好像是拿着一張門票一樣),那服務端接到這個token 應該解析出token中的資訊(例如使用者id),根據這些資訊查詢資料庫傳回相應的結果。
/***
* 解析Jwt令牌資料
*/@Test
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMTAiLCJpYXQiOjE1OTIyNzcxNTAsInN1YiI6IntpZDoxLG5hbWU6J2FkbWluJ30ifQ.2dhrzJFH2hSeDVgTSqUWiC6V-riUBtqTz8LzVFL-gFA
public void testParseJwt(){
String compactJwt="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NjIwNjIyODd9.RBLpZ79USMplQyfJCZFD2muHV_KLks7M1ZsjTu6Aez4";
Claims claims = Jwts.parser().
setSigningKey("xzzb").
parseClaimsJws(compactJwt).
getBody();
System.out.println(claims);
}
運作列印效果:
試着将token或簽名秘鑰篡改一下,會發現運作時就會報錯,是以解析token也就是驗證token.
設定過期時間
有很多時候,我們并不希望簽發的token是永久生效的,是以我們可以為token添加一個過期時間。
解釋:
目前時間超過過期時間,則會報錯
自定義claims
我們剛才的例子隻是存儲了id和subject兩個資訊,如果你想存儲更多的資訊(例如角色)可以定義自定義claims。
建立測試類,并設定測試方法:
建立token:
JwtBuilder jwtBuilder = Jwts.builder();
jwtBuilder.setId("888");
jwtBuilder.setSubject("測試JWT");
jwtBuilder.setIssuedAt(new Date());
// jwtBuilder.setExpiration(new Date());
jwtBuilder.signWith(SignatureAlgorithm.HS256,"xzzb");
Map<String,Object> userInfo = new HashMap<>();
userInfo.put("name","張三");
userInfo.put("age",20);
userInfo.put("address","徐州解放路");
jwtBuilder.addClaims(userInfo);
String key = jwtBuilder.compact();
System.out.println(key);
// String key = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLmtYvor5VKV1QiLCJpYXQiOjE1OTIyMDQ0MTksImV4cCI6MTU5MjIwNDQxOX0.yW-ZtZ7I6czQNMaP9Khf7oVU5M6HidSadxZ-z9DQZUI";
Claims claims =Jwts.parser().setSigningKey("xzzb").parseClaimsJws(key).getBody();
System.out.println(claims);
鑒權處理
思路分析
1.使用者通過通路微服務網關調用微服務,同時攜帶頭檔案資訊
2.在微服務網關這裡進行攔截,攔截後擷取使用者要通路的路徑
3.識别使用者通路的路徑是否需要登入,如果需要,識别使用者的身份是否能通路該路徑[這裡可以基于資料庫設計一套權限]
4.如果需要權限通路,使用者已經登入,則放行
5.如果需要權限通路,且使用者未登入,則提示使用者需要登入
6.使用者通過網關通路使用者微服務,進行登入驗證
7.驗證通過後,使用者微服務會頒發一個令牌給網關,網關會将使用者資訊封裝到頭檔案中,并響應使用者
8.使用者下次通路,攜帶頭檔案中的令牌資訊即可識别是否登入
使用者登入簽發TOKEN
生成令牌工具類
common-api下JwtUtil,主要輔助生成Jwt令牌資訊,代碼如下:
public class JwtUtil {
//有效期為
public static final Long JWT_TTL = 3600000L;// 60 * 60 *1000 一個小時
//Jwt令牌資訊
public static final String JWT_KEY = "xzzb";
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();
}
}
使用者登入成功 則 簽發TOKEN,修改登入的方法:
@RequestMapping(value = "/login")
public Dto login(String username,String password){
//查詢使用者資訊
User user = userService.findById(username);
if(user!=null){
//設定令牌資訊
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 DtoUtil.returnSuccess(true,StatusCode.OK,"登入成功!",jwt);
}
return DtoUtil.returnSuccess(false,StatusCode.LOGINERROR,"賬号或者密碼錯誤!");
}
網關過濾器攔截請求處理
gateway-web
AuthorizeFilter代碼如下:
@Component
public class AuthorizeFilter implements GlobalFilter, Ordered {
//令牌頭名字
private static final String AUTHORIZE_TOKEN = "Authorization";
@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);
}
@Override
public int getOrder() {
return 0;
}
}
配置過濾規則
修改網關系統的yml檔案:
測試通路
http://localhost:8080/api/user/login,效果如下:
沒有帶token記錄的
帶token資訊的