黑馬暢購商城---9.Spring Security Oauth2 JWT授權

學習目標

  • 用戶認證分析

  • 認證技術方案瞭解

  • ==SpringSecurity Oauth2.0入門==

 

1
2
3
4
  oauth2.0認證模式
  	授權碼授權模式
  	密碼授權模式
  授權流程

 

  • ==用戶授權認證開發==

1 用戶認證分析

上面流程圖描述了用戶要操作的各個微服務,用戶查看個人信息需要訪問客戶微服務,下單需要訪問訂單微服務,秒殺搶購商品需要訪問秒殺微服務。每個服務都需要認證用戶的身份,身份認證成功後,需要識別用戶的角色然後授權訪問對應的功能。

1.1 認證與授權

身份認證

用戶身份認證即用戶去訪問系統資源時系統要求驗證用戶的身份信息,身份合法方可繼續訪問。常見的用戶身份認證表現形式有:用戶名密碼登錄,指紋打卡等方式。說通俗點,就相當於校驗用戶賬號密碼是否正確。

用戶授權

用戶認證通過後去訪問系統的資源,系統會判斷用戶是否擁有訪問資源的權限,只允許訪問有權限的系統資源,沒有權限的資源將無法訪問,這個過程叫用戶授權。

1.2 單點登錄

 

用戶訪問的項目中,至少有3個微服務需要識別用戶身份,如果用戶訪問每個微服務都登錄一次就太麻煩了,爲了提高用戶的體驗,我們需要實現讓用戶在一個系統中登錄,其他任意受信任的系統都可以訪問,這個功能就叫單點登錄。

單點登錄(Single Sign On),簡稱爲 SSO,是目前比較流行的企業業務整合的解決方案之一。 SSO的定義是在多個應用系統中,用戶只需要登錄一次就可以訪問所有相互信任的應用系統

1.3 第三方賬號登錄

1.3.1 第三方登錄介紹

隨着國內及國外巨頭們的平臺開放戰略以及移動互聯網的發展,第三方登錄已經不是一個陌生的產品設計概念了。 所謂的第三方登錄,是說基於用戶在第三方平臺上已有的賬號和密碼來快速完成己方應用的登錄或者註冊的功能。而這裏的第三方平臺,一般是已經擁有大量用戶的平臺,國外的比如Facebook,Twitter等,國內的比如微博、微信、QQ等。

1.3.2 第三方登錄優點

1
2
3
4
5
6
7
8
1.相比於本地註冊,第三方登錄一般來說比較方便、快捷,能夠顯著降低用戶的註冊和登錄成本,方便用戶實現快捷登錄或註冊。
2.不用費盡心思地應付本地註冊對賬戶名和密碼的各種限制,如果不考慮暱稱的重複性要求,幾乎可以直接一個賬號走遍天下,再也不用在大腦或者什麼地方記住N多不同的網站或App的賬號和密碼,整個世界一下子清靜了。
3.在第一次綁定成功之後,之後用戶便可以實現一鍵登錄,使得後續的登錄操作比起應用內的登錄來容易了很多。
4.對於某些喜歡社交,並希望將更多自己的生活內容展示給朋友的人來說,第三方登錄可以實現把用戶在應用內的活動同步到第三方平臺上,省去了用戶手動發佈動態的麻煩。但對於某些比較注重個人隱私的用戶來說,則會有一些擔憂,所以龍哥所說的這個優點是有前提的。
5.因爲降低了用戶的註冊或登錄成本,從而減少由於本地註冊的繁瑣性而帶來的隱形用戶流失,最終提高註冊轉化率。
6.對於某些應用來說,使用第三方登錄完全可以滿足自己的需要,因此不必要設計和開發一套自己的賬戶體系。
7.通過授權,可以通過在第三方平臺上分享用戶在應用內的活動在第三方平臺上宣傳自己,從而增加產品知名度。
8.通過授權,可以獲得該用戶在第三方平臺上的好友或粉絲等社交信息,從而後續可以針對用戶的社交關係網進行有目的性的營銷宣傳,爲產品的市場推廣提供另一種渠道。

1.3.3 第三方認證

當需要訪問第三方系統的資源時需要首先通過第三方系統的認證(例如:微信認證),由第三方系統對用戶認證通過,並授權資源的訪問權限。

2 認證技術方案

2.1 單點登錄技術方案

分佈式系統要實現單點登錄,通常將認證系統獨立抽取出來,並且將用戶身份信息存儲在單獨的存儲介質,比如: MySQL、Redis,考慮性能要求,通常存儲在Redis中,如下圖:

單點登錄的特點是:

1
2
3
1、認證系統爲獨立的系統。 
2、各子系統通過Http或其它協議與認證系統通信,完成用戶認證。 
3、用戶身份信息存儲在Redis集羣。

Java中有很多用戶認證的框架都可以實現單點登錄:

1
2
3
 1、Apache Shiro. 
 2、CAS 
 3、Spring security CAS    

2.2 Oauth2認證

OAuth(開放授權)是一個開放標準,允許用戶授權第三方移動應用訪問他們存儲在另外的服務提供者上的信息,而不需要將用戶名和密碼提供給第三方移動應用或分享他們數據的所有內容,OAuth2.0是OAuth協議的延續版本。

2.2.1 Oauth2認證流程

第三方認證技術方案最主要是解決認證協議的通用標準 問題,因爲要實現 跨系統認證,各系統之間要遵循一定的接口協議。
OAUTH協議爲用戶資源的授權提供了一個安全的、開放而又簡易的標準。同時,任何第三方都可以使用OAUTH認證服務,任何服務提供商都可以實現自身的OAUTH認證服務,因而OAUTH是開放的。業界提供了OAUTH的多種實現如PHP、JavaScript,Java,Ruby等各種語言開發包,大大節約了程序員的時間,因而OAUTH是簡易的。互聯網很多服務如Open API,很多大公司如Google,Yahoo,Microsoft等都提供了OAUTH認證服務,這些都足以說明OAUTH標準逐漸成爲開放資源授權的標準。
Oauth協議目前發展到2.0版本,1.0版本過於複雜,2.0版本已得到廣泛應用。
參考:https://baike.baidu.com/item/oAuth/7153134?fr=aladdin
Oauth協議:https://tools.ietf.org/html/rfc6749
下邊分析一個Oauth2認證的例子,黑馬程序員網站使用微信認證的過程:

1.客戶端請求第三方授權

用戶進入黑馬程序的登錄頁面,點擊微信的圖標以微信賬號登錄系統,用戶是自己在微信裏信息的資源擁有者。

點擊“用QQ賬號登錄”出現一個二維碼,此時用戶掃描二維碼,開始給黑馬程序員授權。

2.資源擁有者同意給客戶端授權

資源擁有者掃描二維碼錶示資源擁有者同意給客戶端授權,微信會對資源擁有者的身份進行驗證, 驗證通過後,QQ會詢問用戶是否給授權黑馬程序員訪問自己的QQ數據,用戶點擊“確認登錄”表示同意授權,QQ認證服務器會 頒發一個授權碼,並重定向到黑馬程序員的網站。

3.客戶端獲取到授權碼,請求認證服務器申請令牌 此過程用戶看不到,客戶端應用程序請求認證服務器,請求攜帶授權碼。

4.認證服務器向客戶端響應令牌 認證服務器驗證了客戶端請求的授權碼,如果合法則給客戶端頒發令牌,令牌是客戶端訪問資源的通行證。 此交互過程用戶看不到,當客戶端拿到令牌後,用戶在黑馬程序員看到已經登錄成功。

5.客戶端請求資源服務器的資源 客戶端攜帶令牌訪問資源服務器的資源。 黑馬程序員網站攜帶令牌請求訪問微信服務器獲取用戶的基本信息。

6.資源服務器返回受保護資源 資源服務器校驗令牌的合法性,如果合法則向用戶響應資源信息內容。 注意:資源服務器和認證服務器可以是一個服務也可以分開的服務,如果是分開的服務資源服務器通常要請求認證 服務器來校驗令牌的合法性。

Oauth2.0認證流程如下: 引自Oauth2.0協議rfc6749 https://tools.ietf.org/html/rfc6749

Oauth2包括以下角色:

1、客戶端 本身不存儲資源,需要通過資源擁有者的授權去請求資源服務器的資源,比如:暢購在線Android客戶端、暢購在 線Web客戶端(瀏覽器端)、微信客戶端等。

2、資源擁有者 通常爲用戶,也可以是應用程序,即該資源的擁有者。

3、授權服務器(也稱認證服務器) 用來對資源擁有的身份進行認證、對訪問資源進行授權。客戶端要想訪問資源需要通過認證服務器由資源擁有者授 權後方可訪問。

4、資源服務器 存儲資源的服務器,比如,暢購網用戶管理服務器存儲了暢購網的用戶信息等。客戶端最終訪問資源服務器獲取資源信息。

2.2.2 Oauth2在項目的應用

Oauth2是一個標準的開放的授權協議,應用程序可以根據自己的要求去使用Oauth2,本項目使用Oauth2實現如 下目標:

1、暢購訪問第三方系統的資源

2、外部系統訪問暢購的資源

3、暢購前端(客戶端) 訪問暢購微服務的資源。

4、暢購微服務之間訪問資源,例如:微服務A訪問微服務B的資源,B訪問A的資源。

2.3 Spring security Oauth2認證解決方案

本項目採用 Spring security + Oauth2完成用戶認證及用戶授權,Spring security 是一個強大的和高度可定製的身份驗證和訪問控制框架,Spring security 框架集成了Oauth2協議,下圖是項目認證架構圖:

1、用戶請求認證服務完成認證。

2、認證服務下發用戶身份令牌,擁有身份令牌表示身份合法。

3、用戶攜帶令牌請求資源服務,請求資源服務必先經過網關。

4、網關校驗用戶身份令牌的合法,不合法表示用戶沒有登錄,如果合法則放行繼續訪問。

5、資源服務獲取令牌,根據令牌完成授權。

6、資源服務完成授權則響應資源信息。

3 Security Oauth2.0入門

3.1 學習知識點說明

 

本項目認證服務基於Spring Security Oauth2進行構建,並在其基礎上作了一些擴展,採用JWT令牌機制,並自定 義了用戶身份信息的內容。 本教程的主要目標是學習在項目中集成Spring Security Oauth2的方法和流程,通過 spring Security Oauth2的研究需要達到以下目標:

1、理解Oauth2的授權碼認證流程及密碼認證的流程。

2、理解spring Security Oauth2的工作流程。

3、掌握資源服務集成spring Security框架完成Oauth2認證的流程。

3.2 搭建認證服務器

關於oauth2.0服務搭建的詳細流程,如果有興趣可以參考課件中提供的oauth2.o搭建手冊完成搭建。

3.2.1 導入認證工程

將課件中day09\oauth2.0\changgou-user-oauth的工程導入到項目中去,如下圖:

3.2.2 application.yml配置

3.2.2 啓動授權認證服務

啓動之前,記得先啓動eureka,再啓動該授權認證工程。

3.3 Oauth2授權模式

3.3.1 Oauth2授權模式

Oauth2有以下授權模式:

1
2
3
4
1.授權碼模式(Authorization Code)
2.隱式授權模式(Implicit) 
3.密碼模式(Resource Owner Password Credentials) 
4.客戶端模式(Client Credentials) 

其中授權碼模式和密碼模式應用較多,本小節介紹授權碼模式。

3.3.2 授權碼授權實現

上邊例舉的黑馬程序員網站使用QQ認證的過程就是授權碼模式,流程如下:

1、客戶端請求第三方授權

2、用戶(資源擁有者)同意給客戶端授權

3、客戶端獲取到授權碼,請求認證服務器申請 令牌

4、認證服務器向客戶端響應令牌

5、客戶端請求資源服務器的資源,資源服務校驗令牌合法性,完成授權

6、資源服務器返回受保護資源

(1)申請授權碼

請求認證服務獲取授權碼:

1
2
Get請求:
http://localhost:9001/oauth/authorize?client_id=changgou&response_type=code&scop=app&redirect_uri=http://localhost

參數列表如下:

1
2
3
4
client_id:客戶端id,和授權配置類中設置的客戶端id一致。 
response_type:授權碼模式固定爲code 
scop:客戶端範圍,和授權配置類中設置的scop一致。 
redirect_uri:跳轉uri,當授權碼申請成功後會跳轉到此地址,並在後邊帶上code參數(授權碼)

首先跳轉到登錄頁面:

輸入賬號和密碼,點擊Login。 Spring Security接收到請求會調用UserDetailsService接口的loadUserByUsername方法查詢用戶正確的密碼。 當前導入的基礎工程中客戶端ID爲changgou,祕鑰也爲changgou即可認證通過。

接下來進入授權頁面:

點擊Authorize,接下來返回授權碼: 認證服務攜帶授權碼跳轉redirect_uri,code=k45iLY就是返回的授權碼

(2)申請令牌

拿到授權碼後,申請令牌。 Post請求:http://localhost:9001/oauth/token 參數如下:

1
2
3
grant_type:授權類型,填寫authorization_code,表示授權碼模式 
code:授權碼,就是剛剛獲取的授權碼,注意:授權碼只使用一次就無效了,需要重新申請。 
redirect_uri:申請授權碼時的跳轉url,一定和申請授權碼時用的redirect_uri一致。 

此鏈接需要使用 http Basic認證。 什麼是http Basic認證? http協議定義的一種認證方式,將客戶端id和客戶端密碼按照“客戶端ID:客戶端密碼”的格式拼接,並用base64編 碼,放在header中請求服務端,一個例子: Authorization:Basic WGNXZWJBcHA6WGNXZWJBcHA=WGNXZWJBcHA6WGNXZWJBcHA= 是用戶名:密碼的base64編碼。 認證失敗服務端返回 401 Unauthorized。

以上測試使用postman完成:

http basic認證:

客戶端Id和客戶端密碼會匹配數據庫oauth_client_details表中的客戶端id及客戶端密碼。

點擊發送: 申請令牌成功

返回信如下:

1
2
3
4
5
6
access_token:訪問令牌,攜帶此令牌訪問資源 
token_type:有MAC Token與Bearer Token兩種類型,兩種的校驗算法不同,RFC 6750建議Oauth2採用 Bearer Token(http://www.rfcreader.com/#rfc6750)。 
refresh_token:刷新令牌,使用此令牌可以延長訪問令牌的過期時間。 
expires_in:過期時間,單位爲秒。 
scope:範圍,與定義的客戶端範圍一致。    
jti:當前token的唯一標識

(3)令牌校驗

Spring Security Oauth2提供校驗令牌的端點,如下:

Get: http://localhost:9001/oauth/check_token?token= [access_token]

參數:

token:令牌

使用postman測試如下:

如果令牌校驗失敗,會出現如下結果:

如果令牌過期了,會如下如下結果:

(4)刷新令牌

刷新令牌是當令牌快過期時重新生成一個令牌,它於授權碼授權和密碼授權生成令牌不同,刷新令牌不需要授權碼 也不需要賬號和密碼,只需要一個刷新令牌、客戶端id和客戶端密碼。

測試如下: Post:http://localhost:9001/oauth/token

參數:

grant_type: 固定爲 refresh_token

refresh_token:刷新令牌(注意不是access_token,而是refresh_token)

3.3.3 密碼授權實現

(1)認證

密碼模式(Resource Owner Password Credentials)與授權碼模式的區別是申請令牌不再使用授權碼,而是直接 通過用戶名和密碼即可申請令牌。

測試如下:

Post請求:http://localhost:9001/oauth/token

參數:

grant_type:密碼模式授權填寫password

username:賬號

password:密碼

並且此鏈接需要使用 http Basic認證。

測試數據如下:

(2)校驗令牌

Spring Security Oauth2提供校驗令牌的端點,如下:

Get: http://localhost:9001/oauth/check_token?token=

參數:

token:令牌

使用postman測試如下:

返回結果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
    "companyId": null,
    "userpic": null,
    "scope": [
        "app"
    ],
    "name": null,
    "utype": null,
    "active": true,
    "id": null,
    "exp": 1990221534,
    "jti": "5b96666e-436b-4301-91b5-d89f9bbe6edb",
    "client_id": "changgou",
    "username": "szitheima"
}

exp:過期時間,long類型,距離1970年的秒數(new Date().getTime()可得到當前時間距離1970年的毫秒數)。

user_name: 用戶名

client_id:客戶端Id,在oauth_client_details中配置

scope:客戶端範圍,在oauth_client_details表中配置

jti:與令牌對應的唯一標識 companyId、userpic、name、utype、

id:這些字段是本認證服務在Spring Security基礎上擴展的用戶身份信息

(3)刷新令牌

刷新令牌是當令牌快過期時重新生成一個令牌,它於授權碼授權和密碼授權生成令牌不同,刷新令牌不需要授權碼 也不需要賬號和密碼,只需要一個刷新令牌、客戶端id和客戶端密碼。

測試如下: Post:http://localhost:9001/oauth/token

參數:

grant_type: 固定爲 refresh_token

refresh_token:刷新令牌(注意不是access_token,而是refresh_token)

刷新令牌成功,會重新生成新的訪問令牌和刷新令牌,令牌的有效期也比舊令牌長。

刷新令牌通常是在令牌快過期時進行刷新 。

4 資源服務授權

4.1 資源服務授權流程

 

(1)傳統授權流程

資源服務器授權流程如上圖,客戶端先去授權服務器申請令牌,申請令牌後,攜帶令牌訪問資源服務器,資源服務器訪問授權服務校驗令牌的合法性,授權服務會返回校驗結果,如果校驗成功會返回用戶信息給資源服務器,資源服務器如果接收到的校驗結果通過了,則返回資源給客戶端。

傳統授權方法的問題是用戶每次請求資源服務,資源服務都需要攜帶令牌訪問認證服務去校驗令牌的合法性,並根 據令牌獲取用戶的相關信息,性能低下。

(2)公鑰私鑰授權流程

傳統的授權模式性能低下,每次都需要請求授權服務校驗令牌合法性,我們可以利用公鑰私鑰完成對令牌的加密,如果加密解密成功,則表示令牌合法,如果加密解密失敗,則表示令牌無效不合法,合法則允許訪問資源服務器的資源,解密失敗,則不允許訪問資源服務器資源。

上圖的業務流程如下:

1
2
3
4
5
1、客戶端請求認證服務申請令牌
2、認證服務生成令牌認證服務採用非對稱加密算法,使用私鑰生成令牌。
3、客戶端攜帶令牌訪問資源服務客戶端在Http header 中添加: Authorization:Bearer 令牌。
4、資源服務請求認證服務校驗令牌的有效性資源服務接收到令牌,使用公鑰校驗令牌的合法性。
5、令牌有效,資源服務向客戶端響應資源信息

4.2 公鑰私鑰

在對稱加密的時代,加密和解密用的是同一個密鑰,這個密鑰既用於加密,又用於解密。這樣做有一個明顯的缺點,如果兩個人之間傳輸文件,兩個人都要知道密鑰,如果是三個人呢,五個人呢?於是就產生了非對稱加密,用一個密鑰進行加密(公鑰),用另一個密鑰進行解密(私鑰)。

4.2.1 公鑰私鑰原理

張三有兩把鑰匙,一把是公鑰,另一把是私鑰。

張三把公鑰送給他的朋友們—-李四、王五、趙六—-每人一把。

李四要給張三寫一封保密的信。她寫完後用張三的公鑰加密,就可以達到保密的效果。

張三收信後,用私鑰解密,就看到了信件內容。這裏要強調的是,只要張三的私鑰不泄露,這封信就是安全的,即使落在別人手裏,也無法解密。

張三給李四回信,決定採用“數字簽名”。他寫完後先用Hash函數,生成信件的摘要(digest)。張三將這個簽名,附在信件下面,一起發給李四。

李四收信後,取下數字簽名,用張三的公鑰解密,得到信件的摘要。由此證明,這封信確實是張三發出的。李四再對信件本身使用Hash函數,將得到的結果,與上一步得到的摘要進行對比。如果兩者一致,就證明這封信未被修改過。

4.2.2 生成私鑰公鑰

Spring Security 提供對JWT的支持,本節我們使用Spring Security 提供的JwtHelper來創建JWT令牌,校驗JWT令牌 等操作。 這裏JWT令牌我們採用非對稱算法進行加密,所以我們要先生成公鑰和私鑰。

(1)生成密鑰證書 下邊命令生成密鑰證書,採用RSA 算法每個證書包含公鑰和私鑰

創建一個文件夾,在該文件夾下執行如下命令行:

keytool -genkeypair -alias changgou -keyalg RSA -keypass changgou -keystore changgou.jks -storepass changgou 

Keytool 是一個java提供的證書管理工具

1
2
3
4
5
-alias:密鑰的別名 
-keyalg:使用的hash算法 
-keypass:密鑰的訪問密碼 
-keystore:密鑰庫文件名,xc.keystore保存了生成的證書 
-storepass:密鑰庫的訪問密碼 

(2)查詢證書信息

keytool -list -keystore changgou.jks

(3)刪除別名

keytool -delete -alias changgou -keystore changgou.jsk

4.2.3 導出公鑰

openssl是一個加解密工具包,這裏使用openssl來導出公鑰信息。

安裝 openssl:http://slproweb.com/products/Win32OpenSSL.html

安裝資料目錄下的Win64OpenSSL-1_1_0g.exe

配置openssl的path環境變量,如下圖:

本教程配置在C:\OpenSSL-Win64\bin

cmd進入changgou.jks文件所在目錄執行如下命令(如下命令在windows下執行,會把-變成中文方式,請將它改成英文的-):

keytool -list -rfc --keystore changgou.jks | openssl x509 -inform pem -pubkey

下面段內容是公鑰

1
2
3
4
5
6
7
8
9
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvFsEiaLvij9C1Mz+oyAm
t47whAaRkRu/8kePM+X8760UGU0RMwGti6Z9y3LQ0RvK6I0brXmbGB/RsN38PVnh
cP8ZfxGUH26kX0RK+tlrxcrG+HkPYOH4XPAL8Q1lu1n9x3tLcIPxq8ZZtuIyKYEm
oLKyMsvTviG5flTpDprT25unWgE4md1kthRWXOnfWHATVY7Y/r4obiOL1mS5bEa/
iNKotQNnvIAKtjBM4RlIDWMa6dmz+lHtLtqDD2LF1qwoiSIHI75LQZ/CNYaHCfZS
xtOydpNKq8eb1/PGiLNolD4La2zf0/1dlcr5mkesV570NxRmU1tFm8Zd3MZlZmyv
9QIDAQAB
-----END PUBLIC KEY-----

將上邊的公鑰拷貝到文本public.key文件中,合併爲一行,可以將它放到需要實現授權認證的工程中。

4.2.4 JWT令牌

(1)創建令牌數據

在changgou-user-oauth工程中創建測試類com.changgou.token.CreateJwtTest,使用它來創建令牌信息,代碼如下:

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
public class CreateJwtTest {

    /***
     * 創建令牌測試
     */
    @Test
    public void testCreateToken(){
        //證書文件路徑
        String key_location="changgou.jks";
        //祕鑰庫密碼
        String key_password="changgou";
        //祕鑰密碼
        String keypwd = "changgou";
        //祕鑰別名
        String alias = "changgou";

        //訪問證書路徑
        ClassPathResource resource = new ClassPathResource(key_location);

        //創建祕鑰工廠
        KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(resource,key_password.toCharArray());

        //讀取祕鑰對(公鑰、私鑰)
        KeyPair keyPair = keyStoreKeyFactory.getKeyPair(alias,keypwd.toCharArray());

        //獲取私鑰
        RSAPrivateKey rsaPrivate = (RSAPrivateKey) keyPair.getPrivate();

        //定義Payload
        Map<String, Object> tokenMap = new HashMap<>();
        tokenMap.put("id", "1");
        tokenMap.put("name", "itheima");
        tokenMap.put("roles", "ROLE_VIP,ROLE_USER");

        //生成Jwt令牌
        Jwt jwt = JwtHelper.encode(JSON.toJSONString(tokenMap), new RsaSigner(rsaPrivate));

        //取出令牌
        String encoded = jwt.getEncoded();
        System.out.println(encoded);
    }
}

運行後的結果如下:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6IlJPTEVfVklQLFJPTEVfVVNFUiIsIm5hbWUiOiJpdGhlaW1hIiwiaWQiOiIxIn0.IR9Qu9ZqYZ2gU2qgAziyT38UhEeL4Oi69ko-dzC_P9-Vjz40hwZDqxl8wZ-W2WAw1eWGIHV1EYDjg0-eilogJZ5UikyWw1bewXCpvlM-ZRtYQQqHFTlfDiVcFetyTayaskwa-x_BVS4pTWAskiaIKbKR4KcME2E5o1rEek-3YPkqAiZ6WP1UOmpaCJDaaFSdninqG0gzSCuGvLuG40x0Ngpfk7mPOecsIi5cbJElpdYUsCr9oXc53ROyfvYpHjzV7c2D5eIZu3leUPXRvvVAPJFEcSBiisxUSEeiGpmuQhaFZd1g-yJ1WQrixFvehMeLX2XU6W1nlL5ARTpQf_Jjiw

(2)解析令牌

上面創建令牌後,我們可以對JWT令牌進行解析,這裏解析需要用到公鑰,我們可以將之前生成的公鑰public.key拷貝出來用字符串變量token存儲,然後通過公鑰解密。

在changgou-user-oauth創建測試類com.changgou.token.ParseJwtTest實現解析校驗令牌數據,代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ParseJwtTest {

    /***
     * 校驗令牌
     */
    @Test
    public void testParseToken(){
        //令牌
        String token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6IlJPTEVfVklQLFJPTEVfVVNFUiIsIm5hbWUiOiJpdGhlaW1hIiwiaWQiOiIxIn0.IR9Qu9ZqYZ2gU2qgAziyT38UhEeL4Oi69ko-dzC_P9-Vjz40hwZDqxl8wZ-W2WAw1eWGIHV1EYDjg0-eilogJZ5UikyWw1bewXCpvlM-ZRtYQQqHFTlfDiVcFetyTayaskwa-x_BVS4pTWAskiaIKbKR4KcME2E5o1rEek-3YPkqAiZ6WP1UOmpaCJDaaFSdninqG0gzSCuGvLuG40x0Ngpfk7mPOecsIi5cbJElpdYUsCr9oXc53ROyfvYpHjzV7c2D5eIZu3leUPXRvvVAPJFEcSBiisxUSEeiGpmuQhaFZd1g-yJ1WQrixFvehMeLX2XU6W1nlL5ARTpQf_Jjiw";

        //公鑰
        String publickey = "-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvFsEiaLvij9C1Mz+oyAmt47whAaRkRu/8kePM+X8760UGU0RMwGti6Z9y3LQ0RvK6I0brXmbGB/RsN38PVnhcP8ZfxGUH26kX0RK+tlrxcrG+HkPYOH4XPAL8Q1lu1n9x3tLcIPxq8ZZtuIyKYEmoLKyMsvTviG5flTpDprT25unWgE4md1kthRWXOnfWHATVY7Y/r4obiOL1mS5bEa/iNKotQNnvIAKtjBM4RlIDWMa6dmz+lHtLtqDD2LF1qwoiSIHI75LQZ/CNYaHCfZSxtOydpNKq8eb1/PGiLNolD4La2zf0/1dlcr5mkesV570NxRmU1tFm8Zd3MZlZmyv9QIDAQAB-----END PUBLIC KEY-----";

        //校驗Jwt
        Jwt jwt = JwtHelper.decodeAndVerify(token, new RsaVerifier(publickey));

        //獲取Jwt原始內容
        String claims = jwt.getClaims();
        System.out.println(claims);
        //jwt令牌
        String encoded = jwt.getEncoded();
        System.out.println(encoded);
    }
}

運行後的結果如下:

5 認證開發

5.1 需求分析

用戶登錄的流程圖如下:

執行流程:

1
2
3
4
5
1、用戶登錄,請求認證服務 
2、認證服務認證通過,生成jwt令牌,將jwt令牌及相關信息寫入cookie 
3、用戶訪問資源頁面,帶着cookie到網關 
4、網關從cookie獲取token,如果存在token,則校驗token合法性,如果不合法則拒絕訪問,否則放行 
5、用戶退出,請求認證服務,刪除cookie中的token 

5.2 認證服務

5.2.1 認證需求分析

認證服務需要實現的功能如下:

1、登錄接口

前端post提交賬號、密碼等,用戶身份校驗通過,生成令牌,並將令牌寫入cookie。

2、退出接口 校驗當前用戶的身份爲合法並且爲已登錄狀態。 將令牌從cookie中刪除。

5.2.2 工具封裝

在changgou-user-oauth工程中添加如下工具對象,方便操作令牌信息。

創建com.changgou.oauth.util.AuthToken類,存儲用戶令牌數據,代碼如下:

1
2
3
4
5
6
7
8
9
10
11
public class AuthToken implements Serializable{

    //令牌信息
    String accessToken;
    //刷新token(refresh_token)
    String refreshToken;
    //jwt短令牌
    String jti;
    
    //...get...set
}

創建com.changgou.oauth.util.CookieUtil類,操作Cookie,代碼如下:

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
public class CookieUtil {

    /**
     * 設置cookie
     *
     * @param response
     * @param name     cookie名字
     * @param value    cookie值
     * @param maxAge   cookie生命週期 以秒爲單位
     */
    public static void addCookie(HttpServletResponse response, String domain, String path, String name,
                                 String value, int maxAge, boolean httpOnly) {
        Cookie cookie = new Cookie(name, value);
        cookie.setDomain(domain);
        cookie.setPath(path);
        cookie.setMaxAge(maxAge);
        cookie.setHttpOnly(httpOnly);
        response.addCookie(cookie);
    }

    /**
     * 根據cookie名稱讀取cookie
     * @param request
     * @return map<cookieName,cookieValue>
     */

    public static Map<String,String> readCookie(HttpServletRequest request, String ... cookieNames) {
        Map<String,String> cookieMap = new HashMap<String,String>();
            Cookie[] cookies = request.getCookies();
            if (cookies != null) {
                for (Cookie cookie : cookies) {
                    String cookieName = cookie.getName();
                    String cookieValue = cookie.getValue();
                    for(int i=0;i<cookieNames.length;i++){
                        if(cookieNames[i].equals(cookieName)){
                            cookieMap.put(cookieName,cookieValue);
                        }
                    }
                }
            }
        return cookieMap;

    }
}

創建com.changgou.oauth.util.UserJwt類,封裝SpringSecurity中User信息以及用戶自身基本信息,代碼如下:

1
2
3
4
5
6
7
8
9
10
public class UserJwt extends User {
    private String id;    //用戶ID
    private String name;  //用戶名字

    public UserJwt(String username, String password, Collection<? extends GrantedAuthority> authorities) {
        super(username, password, authorities);
    }

    //...get...set
}

5.2.3 業務層

如上圖,我們現在實現一個認證流程,用戶從頁面輸入賬號密碼,到認證服務的Controller層,Controller層調用Service層,Service層調用OAuth2.0的認證地址,進行密碼授權認證操作,如果賬號密碼正確了,就返回令牌信息給Service層,Service將令牌信息給Controller層,Controller層將數據存入到Cookie中,再響應用戶。

創建com.changgou.oauth.service.AuthService接口,並添加授權認證方法:

1
2
3
4
5
6
7
public interface AuthService {

    /***
     * 授權認證方法
     */
    AuthToken login(String username, String password, String clientId, String clientSecret);
}

創建com.changgou.oauth.service.impl.AuthServiceImpl實現類,實現獲取令牌數據,這裏認證獲取令牌採用的是密碼授權模式,用的是RestTemplate向OAuth服務發起認證請求,代碼如下:

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
@Service
public class AuthServiceImpl implements AuthService {

    @Autowired
    private LoadBalancerClient loadBalancerClient;

    @Autowired
    private RestTemplate restTemplate;

    /***
     * 授權認證方法
     * @param username
     * @param password
     * @param clientId
     * @param clientSecret
     * @return
     */
    @Override
    public AuthToken login(String username, String password, String clientId, String clientSecret) {
        //申請令牌
        AuthToken authToken = applyToken(username,password,clientId, clientSecret);
        if(authToken == null){
            throw new RuntimeException("申請令牌失敗");
        }
        return authToken;
    }


    /****
     * 認證方法
     * @param username:用戶登錄名字
     * @param password:用戶密碼
     * @param clientId:配置文件中的客戶端ID
     * @param clientSecret:配置文件中的祕鑰
     * @return
     */
    private AuthToken applyToken(String username, String password, String clientId, String clientSecret) {
        //選中認證服務的地址
        ServiceInstance serviceInstance = loadBalancerClient.choose("user-auth");
        if (serviceInstance == null) {
            throw new RuntimeException("找不到對應的服務");
        }
        //獲取令牌的url
        String path = serviceInstance.getUri().toString() + "/oauth/token";
        //定義body
        MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
        //授權方式
        formData.add("grant_type", "password");
        //賬號
        formData.add("username", username);
        //密碼
        formData.add("password", password);
        //定義頭
        MultiValueMap<String, String> header = new LinkedMultiValueMap<>();
        header.add("Authorization", httpbasic(clientId, clientSecret));
        //指定 restTemplate當遇到400或401響應時候也不要拋出異常,也要正常返回值
        restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {
            @Override
            public void handleError(ClientHttpResponse response) throws IOException {
                //當響應的值爲400或401時候也要正常響應,不要拋出異常
                if (response.getRawStatusCode() != 400 && response.getRawStatusCode() != 401) {
                    super.handleError(response);
                }
            }
        });
        Map map = null;
        try {
            //http請求spring security的申請令牌接口
            ResponseEntity<Map> mapResponseEntity = restTemplate.exchange(path, HttpMethod.POST,new HttpEntity<MultiValueMap<String, String>>(formData, header), Map.class);
            //獲取響應數據
            map = mapResponseEntity.getBody();
        } catch (RestClientException e) {
            throw new RuntimeException(e);
        }
        if(map == null || map.get("access_token") == null || map.get("refresh_token") == null || map.get("jti") == null) {
            //jti是jwt令牌的唯一標識作爲用戶身份令牌
            throw new RuntimeException("創建令牌失敗!");
        }

        //將響應數據封裝成AuthToken對象
        AuthToken authToken = new AuthToken();
        //訪問令牌(jwt)
        String accessToken = (String) map.get("access_token");
        //刷新令牌(jwt)
        String refreshToken = (String) map.get("refresh_token");
        //jti,作爲用戶的身份標識
        String jwtToken= (String) map.get("jti");
        authToken.setJti(jwtToken);
        authToken.setAccessToken(accessToken);
        authToken.setRefreshToken(refreshToken);
        return authToken;
    }


    /***
     * base64編碼
     * @param clientId
     * @param clientSecret
     * @return
     */
    private String httpbasic(String clientId,String clientSecret){
        //將客戶端id和客戶端密碼拼接,按“客戶端id:客戶端密碼”
        String string = clientId+":"+clientSecret;
        //進行base64編碼
        byte[] encode = Base64Utils.encode(string.getBytes());
        return "Basic "+new String(encode);
    }
}

5.2.4 控制層

創建控制層com.changgou.oauth.controller.AuthController,編寫用戶登錄授權方法,代碼如下:

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
@RestController
@RequestMapping(value = "/user")
public class AuthController {

    //客戶端ID
    @Value("${auth.clientId}")
    private String clientId;

    //祕鑰
    @Value("${auth.clientSecret}")
    private String clientSecret;

    //Cookie存儲的域名
    @Value("${auth.cookieDomain}")
    private String cookieDomain;

    //Cookie生命週期
    @Value("${auth.cookieMaxAge}")
    private int cookieMaxAge;

    @Autowired
    AuthService authService;

    @PostMapping("/login")
    public Result login(String username, String password) {
        if(StringUtils.isEmpty(username)){
            throw new RuntimeException("用戶名不允許爲空");
        }
        if(StringUtils.isEmpty(password)){
            throw new RuntimeException("密碼不允許爲空");
        }
        //申請令牌
        AuthToken authToken =  authService.login(username,password,clientId,clientSecret);

        //用戶身份令牌
        String access_token = authToken.getAccessToken();
        //將令牌存儲到cookie
        saveCookie(access_token);

        return new Result(true, StatusCode.OK,"登錄成功!");
    }

    /***
     * 將令牌存儲到cookie
     * @param token
     */
    private void saveCookie(String token){
        HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
        CookieUtil.addCookie(response,cookieDomain,"/","Authorization",token,cookieMaxAge,false);
    }
}

5.2.5 測試認證接口

使用postman測試:

Post請求:http://localhost:9001/user/login

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