目标
- 掌握微服務網關Gateway的系統搭建
- 掌握網關限流的實作
- 能夠使用BCrypt實作對密碼的加密與驗證
- 了解加密算法
- 能夠使用JWT實作微服務鑒權
1.微服務網關Gateway
1.1 微服務網關概述
不同的微服務一般會有不同的網絡位址,而外部用戶端可能需要調用多個服務的接口才能完成一個業務需求,如果讓用戶端直接與各個微服務通信,會有以下的問題:
- 用戶端會多次請求不同的微服務,增加了用戶端的複雜性
- 存在跨域請求,在一定場景下處理相對複雜
- 認證複雜,每個服務都需要獨立認證
- 難以重構,随着項目的疊代,可能需要重新劃分微服務。例如,可能将多個服務合并成一個或者将一個服務拆分成多個。如果用戶端直接與微服務通信,那麼重構将會很難實施
- 某些微服務可能使用了防火牆 / 浏覽器不友好的協定,直接通路會有一定的困難
以上這些問題可以借助網關解決。
網關是介于用戶端和伺服器端之間的中間層,所有的外部請求都會先經過 網關這一層。也就是說,API 的實作方面更多的考慮業務邏輯,而安全、性能、監控可以交由 網關來做,這樣既提高業務靈活性又不缺安全性,典型的架構圖如圖所示:
優點如下:
- 安全 ,隻有網關系統對外進行暴露,微服務可以隐藏在内網,通過防火牆保護。
- 易于監控。可以在網關收集監控資料并将其推送到外部系統進行分析。
- 易于認證。可以在網關上進行認證,然後再将請求轉發到後端的微服務,而無須在每個微服務中進行認證。
- 減少了用戶端與各個微服務之間的互動次數
- 易于統一授權。
總結:微服務網關就是一個系統,通過暴露該微服務網關系統,友善我們進行相關的鑒權,安全控制,日志統一處理,易于監控的相關功能。
實作微服務網關的技術有很多,
- 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
1.2 微服務網關微服務搭建
由于我們開發的系統 有包括前台系統和背景系統,背景的系統給管理者使用。那麼也需要調用各種微服務,是以我們針對管理背景搭建一個網關微服務。分析如下:
搭建步驟:
1)在changgou_gateway工程中,建立 changgou_gateway_system工程,pom.xml:
<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>
2)建立包com.changgou , 建立引導類:GatewayApplication
@SpringBootApplication
@EnableEurekaClient
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
3)在resources下建立application.yml
spring:
application:
name: sysgateway
cloud:
gateway:
routes:
- id: goods
uri: lb://goods
predicates:
- Path=/goods/**
filters:
- StripPrefix= 1
- id: system
uri: lb://system
predicates:
- Path=/system/**
filters:
- StripPrefix= 1
server:
port: 9101
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:6868/eureka
instance:
prefer-ip-address: true
參考官方手冊:
https://cloud.spring.io/spring-cloud-gateway/spring-cloud-gateway.html#_stripprefix_gatewayfilter_factory
1.3 微服務網關跨域
修改application.yml ,在spring.cloud.gateway節點添加配置,
globalcors:
cors-configurations:
'[/**]': # 比對所有請求
allowedOrigins: "*" #跨域處理 允許所有的域
allowedMethods: # 支援的方法
- GET
- POST
- PUT
- DELETE
最終配置檔案如下:
spring:
application:
name: sysgateway
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]': # 比對所有請求
allowedOrigins: "*" #跨域處理 允許所有的域
allowedMethods: # 支援的方法
- GET
- POST
- PUT
- DELETE
routes:
- id: goods
uri: lb://goods
predicates:
- Path=/goods/**
filters:
- StripPrefix= 1
server:
port: 9101
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:6868/eureka
instance:
prefer-ip-address: true
1.4 微服務網關過濾器
我們可以通過網關過濾器,實作一些邏輯的處理,比如ip黑白名單攔截、特定位址的攔截等。下面的代碼中做了兩個過濾器,并且設定的先後順序,隻示範過濾器與運作效果。(具體邏輯處理部分學員實作)
1)changgou_gateway_system建立IpFilter
@Component
public class IpFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("經過第1個過濾器IpFilter");
ServerHttpRequest request = exchange.getRequest();
InetSocketAddress remoteAddress = request.getRemoteAddress();
System.out.println("ip:"+remoteAddress.getHostName());
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 1;
}
}
2)changgou_gateway_system建立UrlFilter
@Component
public class UrlFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("經過第2個過濾器UrlFilter");
ServerHttpRequest request = exchange.getRequest();
String url = request.getURI().getPath();
System.out.println("url:"+url);
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 2;
}
}
測試,觀察控制台輸出。
2 網關限流
我們之前說過,網關可以做很多的事情,比如,限流,當我們的系統 被頻繁的請求的時候,就有可能 将系統壓垮,是以 為了解決這個問題,需要在每一個微服務中做限流操作,但是如果有了網關,那麼就可以在網關系統做限流,因為所有的請求都需要先通過網關系統才能路由到微服務中。
2.1 思路分析
2.2 令牌桶算法
令牌桶算法是比較常見的限流算法之一,大概描述如下:
- 所有的請求在處理之前都需要拿到一個可用的令牌才會被處理;
- 根據限流大小,設定按照一定的速率往桶裡添加令牌;
- 桶設定最大的放置令牌限制,當桶滿時、新添加的令牌就被丢棄或者拒絕;
- 請求達到後首先要擷取令牌桶中的令牌,拿着令牌才可以進行其他的業務邏輯,處理完業務邏輯之後,将令牌直接删除;
- 令牌桶有最低限額,當桶中的令牌達到最低限額的時候,請求處理完之後将不會删除令牌,以此保證足夠的限流
如下圖:
這個算法的實作,有很多技術,Guava(讀音: 瓜哇)是其中之一,redis用戶端也有其實作。
2.3 網關限流代碼實作
需求:每個ip位址1秒内隻能發送1次請求,多出來的請求傳回429錯誤。
代碼實作:
1)spring cloud gateway 預設使用redis的RateLimter限流算法來實作。是以我們要使用首先需要引入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
在GatewayApplicatioin引導類中添加如下代碼,KeyResolver用于計算某一個類型的限流的KEY也就是說,可以通過KeyResolver來指定限流的Key。
//定義一個KeyResolver
@Bean
public KeyResolver ipKeyResolver() {
return new KeyResolver() {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}
};
}
3)修改application.yml中配置項,指定限制流量的配置以及REDIS的配置,修改後最終配置如下:
spring:
application:
name: sysgateway
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]': # 比對所有請求
allowedOrigins: "*" #跨域處理 允許所有的域
allowedMethods: # 支援的方法
- GET
- POST
- PUT
- DELETE
routes:
- id: goods
uri: lb://goods
predicates:
- Path=/goods/**
filters:
- StripPrefix= 1
- name: RequestRateLimiter #請求數限流 名字不能随便寫
args:
key-resolver: "#{@ipKeyResolver}"
redis-rate-limiter.replenishRate: 1
redis-rate-limiter.burstCapacity: 1
- id: system
uri: lb://system
predicates:
- Path=/system/**
filters:
- StripPrefix= 1
# 配置Redis 127.0.0.1可以省略配置
redis:
host: 192.168.200.128
port: 6379
server:
port: 9101
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:6868/eureka
instance:
prefer-ip-address: true
解釋:
- burstCapacity:令牌桶總容量。
- replenishRate:令牌桶每秒填充平均速率。
- key-resolver:用于限流的鍵的解析器的 Bean 對象的名字。它使用 SpEL 表達式根據#{@beanName}從 Spring 容器中擷取 Bean 對象。
通過在replenishRate和中設定相同的值來實作穩定的速率burstCapacity。設定burstCapacity高于時,可以允許臨時突發replenishRate。
在這種情況下,需要在突發之間允許速率限制器一段時間(根據replenishRate),因為2次連續突發将導緻請求被丢棄(HTTP 429 - Too Many Requests)
key-resolver: “#{@userKeyResolver}” 用于通過SPEL表達式來指定使用哪一個KeyResolver.
如上配置:
表示 一秒内,允許 一個請求通過,令牌桶的填充速率也是一秒鐘添加一個令牌。
最大突發狀況 也隻允許 一秒内有一次請求,可以根據業務來調整 。
4)測試
啟動redis->啟動注冊中心->啟動商品微服務->啟動gateway網關
打開浏覽器 http://localhost:9101/goods/brand
快速重新整理,當1秒内發送多次請求,就會傳回429錯誤。
3. BCrypt密碼加密
3.1 BCrypt快速入門
在使用者子產品,對于使用者密碼的保護,通常都會進行加密。我們通常對密碼進行加密,然後存放在資料庫中,在使用者進行登入的時候,将其輸入的密碼進行加密然後與資料庫中存放的密文進行比較,以驗證使用者密碼是否正确。
目前,MD5和BCrypt比較流行。相對來說,BCrypt比MD5更安全。
BCrypt 官網http://www.mindrot.org/projects/jBCrypt/
1)我們從官網下載下傳源碼
2)建立工程,将源碼類BCrypt拷貝到工程
3)建立測試類,main方法中編寫代碼,實作對密碼的加密
String gensalt = BCrypt.gensalt();//這個是鹽 29個字元,随機生成
System.out.println(gensalt);
String password = BCrypt.hashpw("123456", gensalt); //根據鹽對密碼進行加密
System.out.println(password);//加密後的字元串前29位就是鹽
4)建立測試類,main方法中編寫代碼,實作對密碼的校驗。BCrypt不支援反運算,隻支援密碼校驗。
boolean checkpw = BCrypt.checkpw("123456", "$2a$10$61ogZY7EXsMDWeVGQpDq3OBF1.phaUu7.xrwLyWFTOu8woE08zMIW");
System.out.println(checkpw);
3.2 新增管理者密碼加密
3.2.1 需求與表結構分析
新增管理者,使用BCrypt進行密碼加密
3.2.2 代碼實作
1)将BCrypt源碼拷貝到changgou_common工程 org.mindrot.jbcrypt包下
2)修改changgou_service_system項目的AdminServiceImpl
/**
* 增加
* @param admin
*/
@Override
public void add(Admin admin){
String password = BCrypt.hashpw(admin.getPassword(), BCrypt.gensalt());
admin.setPassword(password);
adminMapper.insert(admin);
}
3.3 管理者登入密碼驗證
3.3.1 需求分析
系統管理使用者需要管理背景,需要先輸入使用者名和密碼進行登入,才能進入管理背景。
思路:
- 使用者發送請求,輸入使用者名和密碼
- 背景管理微服務controller接收參數,驗證使用者名和密碼是否正确,如果正确則傳回使用者登入成功結果
3.3.2 代碼實作
1)AdminService新增方法定義
/**
* 登入驗證密碼
* @param admin
* @return
*/
boolean login(Admin admin);
2)AdminServiceImpl實作此方法
@Override
public boolean login(Admin admin) {
//根據登入名查詢管理者
Admin admin1=new Admin();
admin1.setLoginName(admin.getLoginName());
admin1.setStatus("1");
Admin admin2 = adminMapper.selectOne(admin1);//資料庫查詢出的對象
if(admin2==null){
return false;
}else{
//驗證密碼, Bcrypt為spring的包, 第一個參數為明文密碼, 第二個參數為密文密碼
return BCrypt.checkpw(admin.getPassword(),admin2.getPassword());
}
}
3)AdminController新增方法
/**
* 登入
* @param admin
* @return
*/
@PostMapping("/login")
public Result login(@RequestBody Admin admin){
boolean login = adminService.login(admin);
if(login){
return new Result();
}else{
return new Result(false,StatusCode.LOGINERROR,"使用者名或密碼錯誤");
}
}
4.加密算法(了解)
由于在學習JWT的時候會涉及使用很多加密算法, 是以在這裡做下掃盲, 簡單了解就可以
加密算法種類有:
4.1.可逆加密算法
解釋: 加密後, 密文可以反向解密得到密碼原文.
4.1.1. 對稱加密
【檔案加密和解密使用相同的密鑰,即加密密鑰也可以用作解密密鑰】
解釋: 在對稱加密算法中,資料發信方将明文和加密密鑰一起經過特殊的加密算法處理後,使其變成複雜的加密密文發送出去,收信方收到密文後,若想解讀出原文,則需要使用加密時用的密鑰以及相同加密算法的逆算法對密文進行解密,才能使其回複成可讀明文。在對稱加密算法中,使用的密鑰隻有一個,收發雙方都使用這個密鑰,這就需要解密方事先知道加密密鑰。
優點: 對稱加密算法的優點是算法公開、計算量小、加密速度快、加密效率高。
缺點: 沒有非對稱加密安全.
用途: 一般用于儲存使用者手機号、身份證等敏感但能解密的資訊。
常見的對稱加密算法有: AES、DES、3DES、Blowfish、IDEA、RC4、RC5、RC6、HS256
4.1.2. 非對稱加密
【兩個密鑰:公開密鑰(publickey)和私有密鑰,公有密鑰加密,私有密鑰解密】
解釋: 同時生成兩把密鑰:私鑰和公鑰,私鑰隐秘儲存,公鑰可以下發給信任用戶端.
加密與解密:
- 私鑰加密,持有私鑰或公鑰才可以解密
- 公鑰加密,持有私鑰才可解密
簽名:
私鑰簽名, 持有公鑰進行驗證是否被篡改過.
- 優點: 非對稱加密與對稱加密相比,其安全性更好;
- 缺點: 非對稱加密的缺點是加密和解密花費時間長、速度慢,隻适合對少量資料進行加密。
- 用途: 一般用于簽名和認證。私鑰伺服器儲存, 用來加密, 公鑰客戶拿着用于對于令牌或者簽名的解密或者校驗使用.
常見的非對稱加密算法有:RSA、DSA(數字簽名用)、ECC(移動裝置用)、RS256 (采用SHA-256 的 RSA 簽名)
4.2.不可逆加密算法
解釋: 一旦加密就不能反向解密得到密碼原文.
種類: Hash加密算法, 雜湊演算法, 摘要算法等
用途: 一般用于效驗下載下傳檔案正确性,一般在網站上下載下傳檔案都能見到;存儲使用者敏感資訊,如密碼、 卡号等不可解密的資訊。
常見的不可逆加密算法有: MD5、SHA、HMAC
4.3.Base64編碼
Base64是網絡上最常見的用于傳輸8Bit位元組代碼的編碼方式之一。Base64編碼可用于在HTTP環境下傳遞較長的辨別資訊。采用Base64Base64編碼解碼具有不可讀性,即所編碼的資料不會被人用肉眼所直接看到。注意:Base64隻是一種編碼方式,不算加密方法。
線上編碼工具:
http://www.jsons.cn/img2base64/
5. JWT 實作微服務鑒權
JWT一般用于實作單點登入。單點登入:如騰訊下的遊戲有很多,包括lol,飛車等,在qq遊戲對戰平台上登入一次,然後這些不同的平台都可以直接登陸進去了,這就是單點登入的使用場景。 JWT就是實作單點登入的一種技術,其他的還有oath2等。
5.1 什麼是微服務鑒權
我們之前已經搭建過了網關,使用網關在網關系統中比較适合進行權限校驗。
那麼我們可以采用JWT的方式來實作鑒權校驗。
5.2 JWT
JSON Web Token(JWT)是一個非常輕巧的規範。這個規範允許我們使用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了。
5.3 JJWT簽發與驗證token
JJWT是一個提供端到端的JWT建立和驗證的Java庫。永遠免費和開源(Apache License,版本2.0),JJWT很容易使用和了解。它被設計成一個以建築為中心的流暢界面,隐藏了它的大部分複雜性。
官方文檔:
https://github.com/jwtk/jjwt
5.3.1 建立token
1)建立項目中的pom.xml中添加依賴:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
- 建立測試類,代碼如下
JwtBuilder builder= Jwts.builder()
.setId("888") //設定唯一編号
.setSubject("小白")//設定主題 可以是JSON資料
.setIssuedAt(new Date())//設定簽發日期
.signWith(SignatureAlgorithm.HS256,"itcast");//設定簽名 使用HS256算法,并設定SecretKey(字元串)
//建構 并傳回一個字元串
System.out.println( builder.compact() );
運作列印結果:
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NTc5MDQxODF9.ThecMfgYjtoys3JX7dpx3hu6pUm0piZ0tXXreFU_u3Y
再次運作,會發現每次運作的結果是不一樣的,因為我們的載荷中包含了時間。
5.3.2 解析token
我們剛才已經建立了token ,在web應用中這個操作是由服務端進行然後發給用戶端,用戶端在下次向服務端發送請求時需要攜帶這個token(這就好像是拿着一張門票一樣),那服務端接到這個token 應該解析出token中的資訊(例如使用者id),根據這些資訊查詢資料庫傳回相應的結果。
String compactJwt="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NTc5MDQxODF9.ThecMfgYjtoys3JX7dpx3hu6pUm0piZ0tXXreFU_u3Y";
Claims claims = Jwts.parser().setSigningKey("itcast").parseClaimsJws(compactJwt).getBody();
System.out.println(claims);
運作列印效果:
{jti=888, sub=小白, iat=1557904181}
試着将token或簽名秘鑰篡改一下,會發現運作時就會報錯,是以解析token也就是驗證token.
5.3.3 設定過期時間
有很多時候,我們并不希望簽發的token是永久生效的,是以我們可以為token添加一個過期時間。
1)建立token 并設定過期時間
//目前時間
long currentTimeMillis = System.currentTimeMillis();
Date date = new Date(currentTimeMillis);
JwtBuilder builder= Jwts.builder()
.setId("888") //設定唯一編号
.setSubject("小白")//設定主題 可以是JSON資料
.setIssuedAt(new Date())//設定簽發日期
.setExpiration(date)
.signWith(SignatureAlgorithm.HS256,"itcast");//設定簽名 使用HS256算法,并設定SecretKey(字元串)
//建構 并傳回一個字元串
System.out.println( builder.compact() );
解釋:
.setExpiration(date)//用于設定過期時間 ,參數為Date類型資料
運作,列印效果如下:
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NTc5MDUzMDgsImV4cCI6MTU1NzkwNTMwOH0.4q5AaTyBRf8SB9B3Tl-I53PrILGyicJC3fgR3gWbvUI
2)解析TOKEN
String compactJwt="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NTc5MDUzMDgsImV4cCI6MTU1NzkwNTMwOH0.4q5AaTyBRf8SB9B3Tl-I53PrILGyicJC3fgR3gWbvUI";
Claims claims = Jwts.parser().setSigningKey("itcast").parseClaimsJws(compactJwt).getBody();
System.out.println(claims);
列印效果:
目前時間超過過期時間,則會報錯。
5.3.4 自定義claims
我們剛才的例子隻是存儲了id和subject兩個資訊,如果你想存儲更多的資訊(例如角色)可以定義自定義claims。
建立測試類,并設定測試方法:
建立token:
@Test
public void createJWT(){
//目前時間
long currentTimeMillis = System.currentTimeMillis();
currentTimeMillis+=1000000L;
Date date = new Date(currentTimeMillis);
JwtBuilder builder= Jwts.builder()
.setId("888") //設定唯一編号
.setSubject("小白")//設定主題 可以是JSON資料
.setIssuedAt(new Date())//設定簽發日期
.setExpiration(date)//設定過期時間
.claim("roles","admin")//設定角色
.signWith(SignatureAlgorithm.HS256,"itcast");//設定簽名 使用HS256算法,并設定SecretKey(字元串)
//建構 并傳回一個字元串
System.out.println( builder.compact() );
}
運作列印效果:
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NTc5MDU4MDIsImV4cCI6MTU1NzkwNjgwMiwicm9sZXMiOiJhZG1pbiJ9.AS5Y2fNCwUzQQxXh_QQWMpaB75YqfuK-2P7VZiCXEJI
解析TOKEN:
//解析
@Test
public void parseJWT(){
String compactJwt="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NTc5MDU4MDIsImV4cCI6MTU1NzkwNjgwMiwicm9sZXMiOiJhZG1pbiJ9.AS5Y2fNCwUzQQxXh_QQWMpaB75YqfuK-2P7VZiCXEJI";
Claims claims = Jwts.parser().setSigningKey("itcast").parseClaimsJws(compactJwt).getBody();
System.out.println(claims);
}
運作效果:
5.4 暢購微服務鑒權代碼實作
5.4.1 思路分析
- 使用者進入網關開始登陸,網關過濾器進行判斷,如果是登入,則路由到背景管理微服務進行登入
- 使用者登入成功,背景管理微服務簽發JWT TOKEN資訊傳回給使用者
- 使用者再次進入網關開始通路,網關過濾器接收使用者攜帶的TOKEN
- 網關過濾器解析TOKEN ,判斷是否有權限,如果有,則放行,如果沒有則傳回未認證錯誤
5.4.2 系統微服務簽發token
1)在changgou_service_system中建立類:JwtUtil
package com.changgou.system.util;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
/**
* JWT工具類
*/
public class JwtUtil {
//有效期為
public static final Long JWT_TTL = 3600000L;// 60 * 60 *1000 一個小時
//設定秘鑰明文
public static final String JWT_KEY = "itcast";
/**
* 建立token
* @param id
* @param subject
* @param ttlMillis
* @return
*/
public static String createJWT(String id, String subject, Long ttlMillis) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
if(ttlMillis==null){
ttlMillis=JwtUtil.JWT_TTL;
}
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
SecretKey secretKey = generalKey();
JwtBuilder builder = Jwts.builder()
.setId(id) //唯一的ID
.setSubject(subject) // 主題 可以是JSON資料
.setIssuer("admin") // 簽發者
.setIssuedAt(now) // 簽發時間
.signWith(signatureAlgorithm, secretKey) //使用HS256對稱加密算法簽名, 第二個參數為秘鑰
.setExpiration(expDate);// 設定過期時間
return builder.compact();
}
/**
* 生成加密後的秘鑰 secretKey
* @return
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
}
2)修改AdminController的login方法, 使用者登入成功 則 簽發TOKEN
/**
* 登入
* @param admin
* @return
*/
@PostMapping("/login")
public Result login(@RequestBody Admin admin){
boolean login = adminService.login(admin);
if(login){ //如果驗證成功
Map<String,String> info = new HashMap<>();
info.put("username",admin.getLoginName());
String token = JwtUtil.createJWT(UUID.randomUUID().toString(), admin.getLoginName(), null);
info.put("token",token);
return new Result(true, StatusCode.OK,"登入成功",info);
}else{
return new Result(false,StatusCode.LOGINERROR,"使用者名或密碼錯誤");
}
}
使用postman 測試
5.4.3 網關過濾器驗證token
1)在changgou_gateway_system網關系統添加依賴
<!--鑒權-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
2)建立JWTUtil類
package com.changgou.gateway.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
/**
* jwt校驗工具類
*/
public class JwtUtil {
//有效期為
public static final Long JWT_TTL = 3600000L;// 60 * 60 *1000 一個小時
//設定秘鑰明文
public static final String JWT_KEY = "itcast";
/**
* 生成加密後的秘鑰 secretKey
*
* @return
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
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();
}
}
3)建立過濾器,用于token驗證
/**
* 鑒權過濾器 驗證token
*/
@Component
public class AuthorizeFilter implements GlobalFilter, Ordered {
private static final String AUTHORIZE_TOKEN = "token";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//1. 擷取請求
ServerHttpRequest request = exchange.getRequest();
//2. 則擷取響應
ServerHttpResponse response = exchange.getResponse();
//3. 如果是登入請求則放行
if (request.getURI().getPath().contains("/admin/login")) {
return chain.filter(exchange);
}
//4. 擷取請求頭
HttpHeaders headers = request.getHeaders();
//5. 請求頭中擷取令牌
String token = headers.getFirst(AUTHORIZE_TOKEN);
//6. 判斷請求頭中是否有令牌
if (StringUtils.isEmpty(token)) {
//7. 響應中放入傳回的狀态嗎, 沒有權限通路
response.setStatusCode(HttpStatus.UNAUTHORIZED);
//8. 傳回
return response.setComplete();
}
//9. 如果請求頭中有令牌則解析令牌
try {
JwtUtil.parseJWT(token);
} catch (Exception e) {
e.printStackTrace();
//10. 解析jwt令牌出錯, 說明令牌過期或者僞造等不合法情況出現
response.setStatusCode(HttpStatus.UNAUTHORIZED);
//11. 傳回
return response.setComplete();
}
//12. 放行
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
4)測試:
注意: 資料庫中管理者賬戶為 : admin , 密碼為 : 123456
如果不攜帶token直接通路,則傳回401錯誤
如果攜帶正确的token,則傳回查詢結果
來源:blog.csdn.net/qq_36079912/article/details/104831199