登入模組 使用者認證 SpringSecurity +Oauth2+Jwt

Ybb_studyRecord發表於2020-10-14

SpringSecurity Oauth2 jwt

SpringSecurity Oauth2 jwt

1 使用者認證分析

在這裡插入圖片描述

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

1.1 單點登入

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

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

1.2 第三方賬號登入

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

2 認證解決方案

2.1 單點登入技術方案

分散式系統要實現單點登入,通常將認證系統獨立抽取出來,並且將使用者身份資訊儲存在單獨的儲存介質,比如: MySQL、Redis,考慮效能要求,通常儲存在Redis中,如下圖:
在這裡插入圖片描述
Java中有很多使用者認證的框架都可以實現單點登入:

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

2.2 第三方登入技術方案

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. 使用者訪問第三方資源,第三方請求微信伺服器傳送授權請求給使用者
  2. 使用者確認授權後,返回授權碼給第三方
  3. 第三方拿到授權碼後,攜帶授權碼向微信申請令牌
  4. 第三方拿到令牌後,攜帶令牌向微信請求使用者的基本資訊
  5. 微信資源伺服器根據訪問令牌,返回給第三方使用者的基本資訊
    3-5的互動過程使用者看不到,使用者只能看到登入成功

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+JWT完成使用者認證及使用者授權,Spring security 是一個強大的和高度可定製的身份驗證和訪問控制框架,Spring security 框架整合了Oauth2協議,下圖是專案認證架構圖:
在這裡插入圖片描述

上面的是Oauth2(第三方登入的流程)這裡是賬號密碼登入的流程

這裡微服務閘道器是一個,這裡只是因為流程的關係分開畫了,然後在認證服務中會連結資料庫查詢一些使用者基本資訊放入token中,這樣訪問各個服務的時候使用者資訊就顯示了

  1. 賬號密碼登入,閘道器對於登入和一些靜態資源是放行的,可以訪問。然後這裡是驗證登入和生成令牌的過程,這裡採用jwt的方式生成令牌,用私鑰來生成token,公鑰去解析。
  2. 在認證服務的過程中會去連結資料庫,訪問一個是oauth_client_token(自動訪問)取生成token的引數。有使用者名稱密碼查詢正確,把uid和生成令牌的jti形成鍵值對放入cookie中,redis存放的是jti和對應的token資訊,這樣就能通過cookie中的uid找到jti,再找到redis中的token可以進行解析了
  3. 當使用者訪問服務的時候,會經過微服務閘道器,這裡有兩種配置解析的方式。
    1.公鑰存放在閘道器,判斷cookie,redis中的指定值是否存在,最後通過公鑰去解析令牌是否合法,如果都沒問題就放行
    2.如果私鑰配置在微服務上的話,閘道器判斷cookie和redis非空後,設定一下請求的響應頭
    request.mutate().header(“Authorization”,"Bearer "+jwt);
    這就是令牌的傳遞,這樣其他微服務就不需要再去cookie和redis去找令牌了,過來的時候就攜帶了

3 Jwt令牌回顧

JSON Web Token(JWT)是一個開放的行業標準(RFC 7519),它定義了一種簡介的、自包含的協議格式,用於 在通訊雙方傳遞json物件,傳遞的資訊經過數字簽名可以被驗證和信任。JWT可以使用HMAC演算法或使用RSA的公 鑰/私鑰對來簽名,防止被篡改。

官網:https://jwt.io/

標準:https://tools.ietf.org/html/rfc7519

JWT令牌的優點:

1、jwt基於json,非常方便解析。 
2、可以在令牌中自定義豐富的內容,易擴充套件。 
3、通過非對稱加密演算法及數字簽名技術,JWT防止篡改,安全性高。 
4、資源服務使用JWT可不依賴認證服務即可完成授權。

缺點:

1、JWT令牌較長,佔儲存空間比較大。    

3.1 令牌結構

通過學習JWT令牌結構為自定義jwt令牌打好基礎。

JWT令牌由三部分組成,每部分中間使用點(.)分隔,比如:xxxxx.yyyyy.zzzzz

Header

頭部包括令牌的型別(即JWT)及使用的雜湊演算法(如HMAC SHA256或RSA)

一個例子如下:

下邊是Header部分的內容

{
	"alg": "HS256",
	"typ": "JWT"
}

將上邊的內容使用Base64Url編碼,得到一個字串就是JWT令牌的第一部分。

Payload

第二部分是負載,內容也是一個json物件,它是存放有效資訊的地方,它可以存放jwt提供的現成欄位,比 如:iss(簽發者),exp(過期時間戳), sub(面向的使用者)等,也可自定義欄位。

此部分不建議存放敏感資訊,因為此部分可以解碼還原原始內容。

最後將第二部分負載使用Base64Url編碼,得到一個字串就是JWT令牌的第二部分。

一個例子:

{
	"sub": "1234567890",
	"name": "456",
	"admin": true
}

Signature

第三部分是簽名,此部分用於防止jwt內容被篡改。

這個部分使用base64url將前兩部分進行編碼,編碼後使用點(.)連線組成字串,最後使用header中宣告 簽名演算法進行簽名。

一個例子:

HMACSHA256(
	base64UrlEncode(header) + "." +
	base64UrlEncode(payload),
	secret)

base64UrlEncode(header):jwt令牌的第一部分。

base64UrlEncode(payload):jwt令牌的第二部分。

secret:簽名所使用的金鑰。

3.2 生成私鑰公鑰

JWT令牌生成採用非對稱加密演算法

1、生成金鑰證照 下邊命令生成金鑰證照,採用RSA 演算法每個證照包含公鑰和私鑰

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

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

-alias:金鑰的別名 
-keyalg:使用的hash演算法 
-keypass:金鑰的訪問密碼 
-keystore:金鑰庫檔名,changgou.jks儲存了生成的證照 
-storepass:金鑰庫的訪問密碼 

查詢證照資訊:

keytool -list -keystore changgou.jks

2、匯出公鑰

openssl是一個加解密工具包,這裡使用openssl來匯出公鑰資訊。

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

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

配置openssl的path環境變數,

cmd進入changgou.jks檔案所在目錄執行如下命令:

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

在這裡插入圖片描述
下面段內容是公鑰

-----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檔案中,合併為一行,可以將它放到需要實現授權認證的工程中。

3.3 基於私鑰生成jwt令牌

3.3.1匯入認證服務

  1. 將課件中changgou_user_auth的工程匯入到專案中去,如下圖:
    在這裡插入圖片描述
  2. 啟動eureka,再啟動認證服務
    在這裡插入圖片描述

3.3.2 認證服務中建立測試類

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);
    }
}

3.4 基於公鑰解析jwt令牌

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

在changgou-user-oauth建立測試類com.changgou.token.ParseJwtTest實現解析校驗令牌資料,程式碼如下:

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);
    }
}

4 Oauth2.0入門

4.1 準備工作

  1. 搭建認證伺服器之前,先在使用者系統表結構中增加如下表結構:
CREATE TABLE `oauth_client_details` (
  `client_id` varchar(48) NOT NULL COMMENT '客戶端ID,主要用於標識對應的應用',
  `resource_ids` varchar(256) DEFAULT NULL,
  `client_secret` varchar(256) DEFAULT NULL COMMENT '客戶端祕鑰,BCryptPasswordEncoder加密',
  `scope` varchar(256) DEFAULT NULL COMMENT '對應的範圍',
  `authorized_grant_types` varchar(256) DEFAULT NULL COMMENT '認證模式',
  `web_server_redirect_uri` varchar(256) DEFAULT NULL COMMENT '認證後重定向地址',
  `authorities` varchar(256) DEFAULT NULL,
  `access_token_validity` int(11) DEFAULT NULL COMMENT '令牌有效期',
  `refresh_token_validity` int(11) DEFAULT NULL COMMENT '令牌重新整理週期',
  `additional_information` varchar(4096) DEFAULT NULL,
  `autoapprove` varchar(256) DEFAULT NULL,
  PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

匯入1條初始化資料,其中加密字元明文為changgou:

INSERT INTO `oauth_client_details` VALUES ('changgou', null, '$2a$10$Yvkp3xzDcri6MAsPIqnzzeGBHez1QZR3A079XDdmNU4R725KrkXi2', 'app', 'authorization_code,password,refresh_token,client_credentials', 'http://localhost', null, '43200', '43200', null, null);

4.2 Oauth2授權模式介紹

Oauth2有以下授權模式:

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

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

4.2.1 授權碼模式

4.2.1.1 授權碼授權流程

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

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

2、使用者同意給客戶端授權

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

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

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

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

4.2.1.2 申請授權碼

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

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

引數列表如下:

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就是返回的授權碼, 每一個授權碼只能使用一次
在這裡插入圖片描述

4.2.1.3 申請令牌

拿到授權碼後,申請令牌。

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

引數如下:

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及客戶端密碼。

點選傳送: 申請令牌成功
在這裡插入圖片描述
返回信如下:

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的唯一標識
4.2.1.4 令牌校驗

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

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

引數:

token:令牌

使用postman測試如下:
在這裡插入圖片描述
如果令牌校驗失敗,會出現如下結果:
在這裡插入圖片描述
如果令牌過期了,會如下如下結果:
在這裡插入圖片描述

4.2.1.5 重新整理令牌

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

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

引數:

grant_type: 固定為 refresh_token

refresh_token:重新整理令牌(注意不是access_token,而是refresh_token)
在這裡插入圖片描述

4.2.2 密碼模式

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

4.2.2.1 申請令牌

測試如下:

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

攜帶引數: 
grant_type:密碼模式授權填寫password 
username:賬號 
password:密碼 

並且此連結需要使用 http Basic認證。
在這裡插入圖片描述
在這裡插入圖片描述
在這裡插入圖片描述

4.3 資源服務授權

資源服務擁有要訪問的受保護資源,客戶端攜帶令牌訪問資源服務,如果令牌合法則可成功訪問資源服務中的資源,如下圖:
在這裡插入圖片描述
上圖的業務流程如下:

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

4.3.1 使用者服務對接Oauth2

基本上所有微服務都是資源服務,這裡我們在課程管理服務上配置授權控制,當配置了授權控制後如要訪問課程信 息則必須提供令牌。

1、配置公鑰 ,將 changggou_user_auth 專案中public.key複製到changgou_service_user中
在這裡插入圖片描述
2、新增依賴

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>

3、配置每個系統的Http請求路徑安全控制策略以及讀取公鑰資訊識別令牌,如下:

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)//啟用方法上的PreAuthorize註解
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    //公鑰
    private static final String PUBLIC_KEY = "public.key";

    /***
     * 定義JwtTokenStore
     * @param jwtAccessTokenConverter
     * @return
     */
    @Bean
    public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
        return new JwtTokenStore(jwtAccessTokenConverter);
    }

    /***
     * 定義JJwtAccessTokenConverter
     * @return
     */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setVerifierKey(getPubKey());
        return converter;
    }
    /**
     * 獲取非對稱加密公鑰 Key
     * @return 公鑰 Key
     */
    private String getPubKey() {
        Resource resource = new ClassPathResource(PUBLIC_KEY);
        try {
            InputStreamReader inputStreamReader = new InputStreamReader(resource.getInputStream());
            BufferedReader br = new BufferedReader(inputStreamReader);
            return br.lines().collect(Collectors.joining("\n"));
        } catch (IOException ioe) {
            return null;
        }
    }

    /***
     * Http安全配置,對每個到達系統的http請求連結進行校驗
     * @param http
     * @throws Exception
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        //所有請求必須認證通過
        http.authorizeRequests()
                //下邊的路徑放行
                .antMatchers(
                        "/user/add"). //配置地址放行
                permitAll()
                .anyRequest().
                authenticated();    //其他地址需要認證授權
    }
}

4.3.2 資源服務授權測試

不攜帶令牌訪問http://localhost:9005/user

由於該地址受訪問限制,需要授權,所以出現如下錯誤:

{
    "error": "unauthorized",
    "error_description": "Full authentication is required to access this resource"
}

攜帶令牌訪問http://localhost:9005/user

在http header中新增 Authorization: Bearer 令牌
在這裡插入圖片描述
當輸入錯誤的令牌也無法正常訪問資源。

5 認證開發

5.1 需求分析

功能流程圖如下:
在這裡插入圖片描述
執行流程:

1、使用者登入,請求認證服務 
2、認證服務認證通過,生成jwt令牌,將jwt令牌及相關資訊寫入Redis,並且將身份令牌寫入cookie 
3、使用者訪問資源頁面,帶著cookie到閘道器 
4、閘道器從cookie獲取token,並查詢Redis校驗token,如果token不存在則拒絕訪問,否則放行 
5、使用者退出,請求認證服務,清除redis中的token,並且刪除cookie中的token 

使用redis儲存使用者的身份令牌有以下作用:

1、實現使用者退出登出功能,服務端清除令牌後,即使客戶端請求攜帶token也是無效的。 
2、由於jwt令牌過長,不宜儲存在cookie中,所以將jwt令牌儲存在redis,由客戶端請求服務端獲取並在客戶端儲存。    

5.2 Redis配置

將認證服務changgou_user_auth中的application.yml配置檔案中的Redis配置改成自己對應的埠和密碼。

5.3 認證服務

5.3.1 認證需求分析

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

1、登入介面

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

2、退出介面 校驗當前使用者的身份為合法並且為已登入狀態。 將令牌從redis刪除。 刪除cookie中的令牌。
在這裡插入圖片描述

5.3.2 授權引數配置

修改changgou_user_auth中application.yml配置檔案,修改對應的授權配置

auth:
  ttl: 1200  #token儲存到redis的過期時間
  clientId: changgou	#客戶端ID
  clientSecret: changgou	#客戶端祕鑰
  cookieDomain: localhost	#Cookie儲存對應的域名
  cookieMaxAge: -1			#Cookie過期時間,-1表示瀏覽器關閉則銷燬

5.3.3 申請令牌測試

為了不破壞Spring Security的程式碼,我們在Service方法中通過RestTemplate請求Spring Security所暴露的申請令 牌介面來申請令牌,下邊是測試程式碼:

@SpringBootTest
@RunWith(SpringRunner.class)
public class TokenTest {

    @Autowired
    private LoadBalancerClient loadBalancerClient;

    @Autowired
    private RestTemplate restTemplate;

    /****
     * 傳送Http請求建立令牌
     */
    @Test
    public void testCreateToken() throws InterruptedException {
        //採用客戶端負載均衡,從eureka獲取認證服務的ip 和埠
        ServiceInstance serviceInstance = loadBalancerClient.choose("USER-AUTH");
        URI uri = serviceInstance.getUri();
        //申請令牌地址
        String authUrl = uri + "/oauth/token";

        //1、header資訊,包括了http basic認證資訊
        MultiValueMap<String, String> headers = new LinkedMultiValueMap<String, String>();
        //進行Base64編碼,並將編碼後的認證資料放到標頭檔案中
        String httpbasic = httpbasic("changgou", "changgou");
        headers.add("Authorization", httpbasic);
        //2、指定認證型別、賬號、密碼
        MultiValueMap<String, String> body = new LinkedMultiValueMap<String, String>();
    	body.add("grant_type","password");
        body.add("username","itheima");
        body.add("password","123456");
        HttpEntity<MultiValueMap<String, String>> multiValueMapHttpEntity = new HttpEntity<MultiValueMap<String, String>>(body, headers);
        //指定 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);
                }
            }
        });

        //遠端呼叫申請令牌
        ResponseEntity<Map> exchange = restTemplate.exchange(authUrl, HttpMethod.POST, multiValueMapHttpEntity, Map.class);
        Map result = exchange.getBody();
        System.out.println(result);
    }

    /***
     * 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.3.4 業務層

AuthService介面:

public interface AuthService {
    AuthToken login(String username, String password, String clientId, String clientSecret);
}

AuthServiceImpl實現類:

基於剛才寫的測試實現申請令牌的service方法如下:

@Service
public class AuthServiceImpl implements AuthService {

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private LoadBalancerClient loadBalancerClient;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Value("${auth.ttl}")
    private long ttl;

    /**
     * 申請令牌
     * @param username
     * @param password
     * @param clientId
     * @param clientSecret
     * @return
     */
    @Override
    public AuthToken applyToken(String username, String password, String clientId, String clientSecret) {

        ServiceInstance serviceInstance = loadBalancerClient.choose("user-auth");
        URI uri = serviceInstance.getUri();
        String url = uri+"/oauth/token";

        MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
        body.add("grant_type","password");
        body.add("username",username);
        body.add("password",password);


        MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
        headers.add("Authorization",this.getHttpBasic(clientId,clientSecret));

        HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(body,headers);

        restTemplate.setErrorHandler(new DefaultResponseErrorHandler(){
            @Override
            public void handleError(ClientHttpResponse response) throws IOException {
                if (response.getRawStatusCode()!=400 && response.getRawStatusCode()!=401){
                    super.handleError(response);
                }
            }
        });
        ResponseEntity<Map> responseEntity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, Map.class);

        Map map = responseEntity.getBody();

        if (map==null || map.get("access_token")==null || map.get("refresh_token")==null || map.get("jti")==null){
            throw new RuntimeException("申請令牌失敗");
        }

        AuthToken authToken = new AuthToken();
        authToken.setAccessToken((String) map.get("access_token"));
        authToken.setRefreshToken((String) map.get("refresh_token"));
        authToken.setJti((String) map.get("jti"));

        stringRedisTemplate.boundValueOps(authToken.getJti()).set(authToken.getAccessToken(),ttl, TimeUnit.SECONDS);

        return authToken;
    }

    private String getHttpBasic(String clientId, String clientSecret) {

        String value = clientId+":"+clientSecret;
        byte[] encode = Base64Utils.encode(value.getBytes());
        return "Basic "+new String(encode);
    }
}

5.3.5 控制層

AuthController編寫使用者登入授權方法,程式碼如下:

@RestController
@RequestMapping("/oauth")
public class AuthController {

    @Autowired
    private AuthService authService;

    @Value("${auth.clientId}")
    private String clientId;

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

    @Value("${auth.cookieDomain}")
    private String cookieDomain;

    @Value("${auth.cookieMaxAge}")
    private int cookieMaxAge;

    @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.applyToken(username,password,clientId,clientSecret);

        this.saveJtiToCookie(authToken.getJti());
        
        return new Result(true, StatusCode.OK,"登入成功");

    }

    private void saveJtiToCookie(String jti) {
        HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
        CookieUtil.addCookie(response,cookieDomain,"/","uid",jti,cookieMaxAge,false);
    }
}

5.3.6 登入請求放行

修改認證服務WebSecurityConfig類中configure(),新增放行路徑
在這裡插入圖片描述

5.3.7 測試認證介面

使用postman測試:

1)Post請求:http://localhost:9200/oauth/login
在這裡插入圖片描述

5.3.8 動態獲取使用者資訊

​ 當前在認證服務中,使用者密碼是寫死在使用者認證類中。所以使用者登入時,無論帳號輸入什麼,只要密碼是itheima都可以訪問。 因此需要動態獲取使用者帳號與密碼.

5.3.8.1 定義被訪問介面

使用者微服務對外暴露根據使用者名稱獲取使用者資訊介面

@GetMapping("/load/{username}")
public User findUserInfo(@PathVariable("username") String username){
    return userService.findById(username);
}
5.3.8.2 放行該介面,修改ResourceServerConfig類

在這裡插入圖片描述

5.3.8.3定義feign介面

changgou_user_server_api新增feign介面

@FeignClient(name="user")
public interface UserFeign {

    @GetMapping("/user/load/{username}")
    public User findUserInfo(@PathVariable("username") String username);
}
5.3.8.4 認證服務新增依賴
<dependency>
    <groupId>com.changgou</groupId>
    <artifactId>changgou_service_user_api</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>
5.3.8.5 修改認證服務啟動類
@EnableFeignClients(basePackages = "com.changgou.user.feign")
5.3.8.6 修改使用者認證類

在這裡插入圖片描述
測試: 重新啟動服務並申請令牌

6 認證服務對接閘道器

6.1 新建閘道器工程changgou_gateway_web

  1. changgou_gateway新增依賴
 <!--閘道器依賴-->
    <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>
        <!--redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
            <version>2.1.3.RELEASE</version>
        </dependency>
    </dependencies>
  1. 新建工程changgou_gateway_web,並建立啟動類
@SpringBootApplication
@EnableDiscoveryClient
public class WebGateWayApplication {

    public static void main(String[] args) {
        SpringApplication.run(WebGateWayApplication.class,args);
    }
}
  1. 建立application.yml
spring:
  application:
    name: gateway-web
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]': # 匹配所有請求
            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:
            #- PrefixPath=/brand
            - StripPrefix=1
          #使用者微服務
        - id: changgou_user_route
          uri: lb://user
          predicates:
            - Path=/api/user/**,/api/address/**,/api/areas/**,/api/cities/**,/api/provinces/**
          filters:
            - StripPrefix=1
          #認證微服務
        - id: changgou_oauth_user
          uri: lb://user-auth
          predicates:
            - Path=/api/oauth/**
          filters:
            - StripPrefix=1
  redis:
    host: 192.168.200.128
server:
  port: 8001
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:6868/eureka
  instance:
    prefer-ip-address: true
management:
  endpoint:
    gateway:
      enabled: true
    web:
      exposure:
        include: true

6.2 閘道器全域性過濾器

在這裡插入圖片描述
新建過濾器類AuthorizeFilter,對請求進行過濾

業務邏輯:

1)判斷當前請求是否為登入請求,是的話,則放行

  1. 判斷cookie中是否存在資訊, 沒有的話,拒絕訪問

3)判斷redis中令牌是否存在,沒有的話,拒絕訪問

@Component
public class AuthFilter implements GlobalFilter, Ordered {

    public static final String Authorization = "Authorization";

    @Autowired
    private AuthService authService;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        //獲取當前請求物件
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();

        String path = request.getURI().getPath();
        if ("/api/oauth/login".equals(path)){
            //放行
            return chain.filter(exchange);
        }

        //判斷cookie上是否存在jti
        String jti = authService.getJtiFromCookie(request);
        if (StringUtils.isEmpty(jti)){
            //拒絕訪問,請求跳轉
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }


        //判斷redis中token是否存在
        String redisToken = authService.getTokenFromRedis(jti);
        if (StringUtils.isEmpty(redisToken)){
            //拒絕訪問,請求跳轉
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }

        //校驗通過 , 請求頭增強,放行
        request.mutate().header(Authorization,"Bearer "+redisToken);
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

新建業務邏輯類AuthService

@Service
public class AuthService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 判斷cookie中jti是否存在
     * @param request
     * @return
     */
    public String getJtiFromCookie(ServerHttpRequest request) {
        HttpCookie cookie = request.getCookies().getFirst("uid");
        if (cookie!=null){
            return cookie.getValue();
        }
        return null;
    }

    /**
     * 判斷redis中令牌是否過期
     * @param jti
     * @return
     */
    public String getTokenFromRedis(String jti) {
        String token = stringRedisTemplate.boundValueOps(jti).get();
        return token;
    }
}

測試:

清除postman中的cookie資料,在未登入的情況下,並訪問: http://localhost:8001/api/user 。會返回401異常資訊

訪問:http://localhost:8001/api/oauth/login,並重新測試。可以發現測試通過,拿到返回結果資料

7 自定義登入頁面

7.1 認證服務新增依賴

<!--thymeleaf-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

7.2 資源\成品頁面\登入頁面

在這裡插入圖片描述

7.3 把頁面放到下面的專案中

在這裡插入圖片描述

7.4 靜態資源放行

修改WebSecurityConfig類

在這裡插入圖片描述

web.ignoring().antMatchers(
                "/oauth/login",
                "/oauth/logout","/oauth/toLogin","/login.html","/css/**","/data/**","/fonts/**","/img/**","/js/**");

新增開啟表單登入
在這裡插入圖片描述

7.5 LoginRedirectController

@Controller
@RequestMapping("/oauth")
public class LoginRedirectController {

    @RequestMapping("/toLogin")
    public String toLogin(){
        return "login";
    }
}

7.6 修改登入頁面實現使用者登入

在這裡插入圖片描述

7.7 定義login方法

<script th:inline="javascript">
	var app = new Vue({
		el:"#app",
		data:{
			username:"",
			password:"",
			msg:""
		},
		methods:{
			login:function(){
				app.msg="正在登入";
				axios.post("/api/oauth/login?username="+app.username+"&password="+app.password).then(function (response) {
					if (response.data.flag){
						app.msg="登入成功";
					}else{
						app.msg="登入失敗";
					}
				})
			}
		}
	})
</script>

定義路徑過濾

public class URLFilter {

    public static String filterPath = "/api/wseckillorder,/api/seckill,/api/wxpay,/api/wxpay/**,/api/worder/**,/api/user/**,/api/address/**,/api/wcart/**,/api/cart/**,/api/categoryReport/**,/api/orderConfig/**,/api/order/**,/api/orderItem/**,/api/orderLog/**,/api/preferential/**,/api/returnCause/**,/api/returnOrder/**,/api/returnOrderItem/**";

    public static boolean hasAuthorize(String url){

        String[] split = filterPath.replace("**", "").split(",");

        for (String value : split) {

            if (url.startsWith(value)){
                return true;
            }
        }

        return false;
    }
}

7.8 測試

訪問:http://localhost:9200/oauth/toLogin

在這裡插入圖片描述

oauth全部程式碼

在這裡插入圖片描述

pox.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-user-oauth</artifactId>
    <description>
        OAuth2.0認證環境搭建
    </description>

    <dependencies>
        <!--查詢資料庫資料-->
        <dependency>
            <groupId>com.changgou</groupId>
            <artifactId>changgou_common_db</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-data</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>com.changgou</groupId>
            <artifactId>changgou_service_user_api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <!--thymeleaf-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
    </dependencies>

</project>

yml

server:
  port: 9200
spring:
  application:
    name: user-auth
  redis:
    host: 192.168.200.128
    port: 6379
    password:
    jedis:
      pool:
        max-active: 8
        max-idle: 8
        min-idle: 0
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://192.168.200.128:3306/changgou_user?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true&serverTimezone=UTC
    username: root
    password: root
  main:
    allow-bean-definition-overriding: true
eureka:
  instance:
    prefer-ip-address: true
  client:
    service-url:
      defaultZone: http://127.0.0.1:6868/eureka
auth:
  ttl: 3600  #token儲存到redis的過期時間
  clientId: changgou
  clientSecret: changgou
  cookieDomain: localhost
  cookieMaxAge: -1
encrypt:
  key-store:
    location: classpath:/changgou.jks
    secret: changgou
    alias: changgou
    password: changgou

util中的
AuthToken

package com.changgou.oauth.util;

import java.io.Serializable;


public class AuthToken implements Serializable{

    //令牌資訊 jwt
    String accessToken;
    //重新整理token(refresh_token)
    String refreshToken;
    //jwt短令牌
    String jti;

    public String getAccessToken() {
        return accessToken;
    }

    public void setAccessToken(String accessToken) {
        this.accessToken = accessToken;
    }

    public String getRefreshToken() {
        return refreshToken;
    }

    public void setRefreshToken(String refreshToken) {
        this.refreshToken = refreshToken;
    }

    public String getJti() {
        return jti;
    }

    public void setJti(String jti) {
        this.jti = jti;
    }
}

CookieUtil

package com.changgou.oauth.util;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;

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;
    }
}

UserJwt

package com.changgou.oauth.util;


import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;

import java.util.Collection;


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);
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

啟動類

package com.changgou;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
import tk.mybatis.spring.annotation.MapperScan;

@SpringBootApplication
@EnableDiscoveryClient
@MapperScan(basePackages = "com.changgou.auth.dao")
@EnableFeignClients(basePackages = {"com.changgou.user.feign"})
public class OAuthApplication {

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

    @Bean
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}

重點的config
AuthorizationServerConfig

package com.changgou.oauth.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.bootstrap.encrypt.KeyProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;

import javax.annotation.Resource;
import javax.sql.DataSource;
import java.security.KeyPair;


@Configuration
@EnableAuthorizationServer
class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    //資料來源,用於從資料庫獲取資料進行認證操作,測試可以從記憶體中獲取
    @Autowired
    private DataSource dataSource;
    //jwt令牌轉換器
    @Autowired
    private JwtAccessTokenConverter jwtAccessTokenConverter;
    //SpringSecurity 使用者自定義授權認證類
    @Autowired
    UserDetailsService userDetailsService;
    //授權認證管理器
    @Autowired
    AuthenticationManager authenticationManager;
    //令牌持久化儲存介面
    @Autowired
    TokenStore tokenStore;
    @Autowired
    private CustomUserAuthenticationConverter customUserAuthenticationConverter;

    /***
     * 客戶端資訊配置
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.jdbc(dataSource).clients(clientDetails());
    }

    /***
     * 授權伺服器端點配置
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.accessTokenConverter(jwtAccessTokenConverter)
                .authenticationManager(authenticationManager)//認證管理器
                .tokenStore(tokenStore)                       //令牌儲存
                .userDetailsService(userDetailsService);     //使用者資訊service
    }

    /***
     * 授權伺服器的安全配置
     * @param oauthServer
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
        oauthServer.allowFormAuthenticationForClients()
                .passwordEncoder(new BCryptPasswordEncoder())
                .tokenKeyAccess("permitAll()")
                .checkTokenAccess("isAuthenticated()");
    }


    //讀取金鑰的配置
    @Bean("keyProp")
    public KeyProperties keyProperties(){
        return new KeyProperties();
    }

    @Resource(name = "keyProp")
    private KeyProperties keyProperties;

    //客戶端配置
    @Bean
    public ClientDetailsService clientDetails() {
        return new JdbcClientDetailsService(dataSource);
    }

    @Bean
    @Autowired
    public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
        return new JwtTokenStore(jwtAccessTokenConverter);
    }

    /****
     * JWT令牌轉換器
     * @param customUserAuthenticationConverter
     * @return
     */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(CustomUserAuthenticationConverter customUserAuthenticationConverter) {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        KeyPair keyPair = new KeyStoreKeyFactory(
                keyProperties.getKeyStore().getLocation(),                          //證照路徑 changgou.jks
                keyProperties.getKeyStore().getSecret().toCharArray())              //證照祕鑰 changgouapp
                .getKeyPair(
                        keyProperties.getKeyStore().getAlias(),                     //證照別名 changgou
                        keyProperties.getKeyStore().getPassword().toCharArray());   //證照密碼 changgou
        converter.setKeyPair(keyPair);
        //配置自定義的CustomUserAuthenticationConverter
        DefaultAccessTokenConverter accessTokenConverter = (DefaultAccessTokenConverter) converter.getAccessTokenConverter();
        accessTokenConverter.setUserTokenConverter(customUserAuthenticationConverter);
        return converter;
    }
}


CustomUserAuthenticationConverter

package com.changgou.oauth.config;

import com.changgou.oauth.util.UserJwt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.provider.token.DefaultUserAuthenticationConverter;
import org.springframework.stereotype.Component;

import java.util.LinkedHashMap;
import java.util.Map;

@Component
public class CustomUserAuthenticationConverter extends DefaultUserAuthenticationConverter {

    @Autowired
    UserDetailsService userDetailsService;

    @Override
    public Map<String, ?> convertUserAuthentication(Authentication authentication) {
        LinkedHashMap response = new LinkedHashMap();
        String name = authentication.getName();
        response.put("username", name);

        Object principal = authentication.getPrincipal();
        UserJwt userJwt = null;
        if(principal instanceof  UserJwt){
            userJwt = (UserJwt) principal;
        }else{
            //refresh_token預設不去呼叫userdetailService獲取使用者資訊,這裡我們手動去呼叫,得到 UserJwt
            UserDetails userDetails = userDetailsService.loadUserByUsername(name);
            userJwt = (UserJwt) userDetails;
        }
        response.put("name", userJwt.getName());
        response.put("id", userJwt.getId());
        if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) {
            response.put("authorities", AuthorityUtils.authorityListToSet(authentication.getAuthorities()));
        }
        return response;
    }

}

UserDeatilsServiceImpl

package com.changgou.oauth.config;

import com.changgou.oauth.util.UserJwt;
import com.changgou.user.feign.UserFeign;
import com.netflix.discovery.converters.Auto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

/*****
 * 自定義授權認證類
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    ClientDetailsService clientDetailsService;

    @Autowired
    private UserFeign userFeign;

    /****
     * 自定義授權認證
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //取出身份,如果身份為空說明沒有認證
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        //沒有認證統一採用httpbasic認證,httpbasic中儲存了client_id和client_secret,開始認證client_id和client_secret
        if(authentication==null){
            ClientDetails clientDetails = clientDetailsService.loadClientByClientId(username);
            if(clientDetails!=null){
                //祕鑰
                String clientSecret = clientDetails.getClientSecret();
                //靜態方式
                //return new User(username,new BCryptPasswordEncoder().encode(clientSecret), AuthorityUtils.commaSeparatedStringToAuthorityList(""));
                //資料庫查詢方式
                return new User(username,clientSecret, AuthorityUtils.commaSeparatedStringToAuthorityList(""));
            }
        }

        if (StringUtils.isEmpty(username)) {
            return null;
        }

        //根據使用者名稱查詢使用者資訊
        //String pwd = new BCryptPasswordEncoder().encode("itheima");
        com.changgou.user.pojo.User user = userFeign.findUserInfo(username);
        //建立User物件
        String permissions = "goods_list,seckill_list";
        UserJwt userDetails = new UserJwt(username,user.getPassword(),AuthorityUtils.commaSeparatedStringToAuthorityList(permissions));
        return userDetails;
    }
}

WebSecurityConfig

package com.changgou.oauth.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableWebSecurity
@Order(-1)
class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    /***
     * 忽略安全攔截的URL
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/oauth/login",
                "/oauth/logout","/oauth/toLogin","/login.html","/css/**","/data/**","/fonts/**","/img/**","/js/**");
    }

    /***
     * 建立授權管理認證物件
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        AuthenticationManager manager = super.authenticationManagerBean();
        return manager;
    }

    /***
     * 採用BCryptPasswordEncoder對密碼進行編碼
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /****
     *
     * @param http
     * @throws Exception
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .httpBasic()        //啟用Http基本身份驗證
                .and()
                .formLogin()       //啟用表單身份驗證
                .and()
                .authorizeRequests()    //限制基於Request請求訪問
                .anyRequest()
                .authenticated();       //其他請求都需要經過驗證

        //開啟表單登入
        http.formLogin().loginPage("/oauth/toLogin")//設定訪問登入頁面的路徑
                .loginProcessingUrl("/oauth/login");//設定執行登入操作的路徑
    }
}

AuthService

package com.changgou.oauth.service;

import com.changgou.oauth.util.AuthToken;

public interface AuthService {

    AuthToken login(String username,String password,String clientId,String clientSecret);
}

AuthServiceImpl

package com.changgou.oauth.service.impl;

import com.changgou.oauth.service.AuthService;
import com.changgou.oauth.util.AuthToken;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Service;
import org.springframework.util.Base64Utils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestTemplate;

import java.io.IOException;
import java.net.URI;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@Service
public class AuthServiceImpl implements AuthService {

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private LoadBalancerClient loadBalancerClient;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Value("${auth.ttl}")
    private long ttl;

    @Override
    public AuthToken login(String username, String password, String clientId, String clientSecret) {
        //1.申請令牌
        ServiceInstance serviceInstance = loadBalancerClient.choose("user-auth");
        URI uri = serviceInstance.getUri();
        String url=uri+"/oauth/token";

        MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
        body.add("grant_type","password");
        body.add("username",username);
        body.add("password",password);

        MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
        headers.add("Authorization",this.getHttpBasic(clientId,clientSecret));
        HttpEntity<MultiValueMap<String,String>> requestEntity = new HttpEntity<>(body,headers);

        restTemplate.setErrorHandler(new DefaultResponseErrorHandler(){
            @Override
            public void handleError(ClientHttpResponse response) throws IOException {
                if (response.getRawStatusCode()!=400 && response.getRawStatusCode()!=401){
                    super.handleError(response);
                }
            }
        });

        ResponseEntity<Map> responseEntity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, Map.class);
        Map map = responseEntity.getBody();
        if (map == null || map.get("access_token") == null || map.get("refresh_token") == null || map.get("jti") == null){
            //申請令牌失敗
            throw new RuntimeException("申請令牌失敗");
        }

        //2.封裝結果資料
        AuthToken authToken = new AuthToken();
        authToken.setAccessToken((String) map.get("access_token"));
        authToken.setRefreshToken((String) map.get("refresh_token"));
        authToken.setJti((String)map.get("jti"));

        //3.將jti作為redis中的key,將jwt作為redis中的value進行資料的存放
        stringRedisTemplate.boundValueOps(authToken.getJti()).set(authToken.getAccessToken(),ttl, TimeUnit.SECONDS);
        return authToken;
    }

    private String getHttpBasic(String clientId, String clientSecret) {
        String value = clientId+":"+clientSecret;
        byte[] encode = Base64Utils.encode(value.getBytes());
        return "Basic "+new String(encode);
    }
}

AuthController

package com.changgou.oauth.controller;

import com.changgou.entity.Result;
import com.changgou.entity.StatusCode;
import com.changgou.oauth.service.AuthService;
import com.changgou.oauth.util.AuthToken;
import com.changgou.oauth.util.CookieUtil;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletResponse;

@Controller
@RequestMapping("/oauth")
public class AuthController {

    @Autowired
    private AuthService authService;

    @Value("${auth.clientId}")
    private String clientId;

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

    @Value("${auth.cookieDomain}")
    private String cookieDomain;

    @Value("${auth.cookieMaxAge}")
    private int cookieMaxAge;

    @RequestMapping("/toLogin")
    public String toLogin(){
        return "login";
    }


    @RequestMapping("/login")
    @ResponseBody
    public Result login(String username, String password, HttpServletResponse response){
        //校驗引數
        if (StringUtils.isEmpty(username)){
            throw new RuntimeException("請輸入使用者名稱");
        }
        if (StringUtils.isEmpty(password)){
            throw new RuntimeException("請輸入密碼");
        }
        //申請令牌 authtoken
        AuthToken authToken = authService.login(username, password, clientId, clientSecret);

        //將jti的值存入cookie中
        this.saveJtiToCookie(authToken.getJti(),response);

        //返回結果
        return new Result(true, StatusCode.OK,"登入成功",authToken.getJti());
    }

    //將令牌的斷標識jti存入到cookie中
    private void saveJtiToCookie(String jti, HttpServletResponse response) {
        CookieUtil.addCookie(response,cookieDomain,"/","uid",jti,cookieMaxAge,false);
    }
}

gateway_web

在這裡插入圖片描述


    <!--閘道器依賴-->
    <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>
        <!--redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
            <version>2.1.3.RELEASE</version>
        </dependency>
    </dependencies>

yml

spring:
  application:
    name: gateway-web
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]': # 匹配所有請求
            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:
            #- PrefixPath=/brand
            - StripPrefix=1
          #使用者微服務
        - id: changgou_user_route
          uri: lb://user
          predicates:
            - Path=/api/user/**,/api/address/**,/api/areas/**,/api/cities/**,/api/provinces/**
          filters:
            - StripPrefix=1
          #認證微服務
        - id: changgou_oauth_user
          uri: lb://user-auth
          predicates:
            - Path=/api/oauth/**
          filters:
            - StripPrefix=1
  redis:
    host: 192.168.200.128
server:
  port: 8001
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:6868/eureka
  instance:
    prefer-ip-address: true
management:
  endpoint:
    gateway:
      enabled: true
    web:
      exposure:
        include: true

filter中的
AuthFilter

package com.changgou.web.gateway.filter;

import com.changgou.web.gateway.service.AuthService;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
public class AuthFilter implements GlobalFilter, Ordered {

    @Autowired
    private AuthService authService;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();

        //1.判斷當前請求路徑是否為登入請求,如果是,則直接放行
        String path = request.getURI().getPath();
        if ("/api/oauth/login".equals(path) || !UrlFilter.hasAuthorize(path) ){
            //直接放行
            return chain.filter(exchange);
        }

        //2.從cookie中獲取jti的值,如果該值不存在,拒絕本次訪問
        String jti = authService.getJtiFromCookie(request);
        if (StringUtils.isEmpty(jti)){
            //拒絕訪問
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }

        //3.從redis中獲取jwt的值,如果該值不存在,拒絕本次訪問
        String jwt = authService.getJwtFromRedis(jti);
        if (StringUtils.isEmpty(jwt)){
            //拒絕訪問
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }

        //4.對當前的請求物件進行增強,讓它會攜帶令牌的資訊
        request.mutate().header("Authorization","Bearer "+jwt);
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

UrlFilter

package com.changgou.web.gateway.filter;

public class UrlFilter {

    //所有需要傳遞令牌的地址
    public static String filterPath="/api/wseckillorder,/api/seckill,/api/wxpay,/api/wxpay/**,/api/worder/**,/api/user/**,/api/address/**,/api/wcart/**,/api/cart/**,/api/categoryReport/**,/api/orderConfig/**,/api/order/**,/api/orderItem/**,/api/orderLog/**,/api/preferential/**,/api/returnCause/**,/api/returnOrder/**,/api/returnOrderItem/**";

    public static boolean hasAuthorize(String url){

        String[] split = filterPath.replace("**", "").split(",");

        for (String value : split) {

            if (url.startsWith(value)){
                return true; //代表當前的訪問地址是需要傳遞令牌的
            }
        }

        return false; //代表當前的訪問地址是不需要傳遞令牌的
    }
}

AuthService

package com.changgou.web.gateway.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpCookie;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Service;

@Service
public class AuthService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    //從cookie中獲取jti的值
    public String getJtiFromCookie(ServerHttpRequest request) {
        HttpCookie httpCookie = request.getCookies().getFirst("uid");
        if (httpCookie != null){
            String jti = httpCookie.getValue();
            return jti;
        }
        return null;
    }

    //從redis中獲取jwt
    public String getJwtFromRedis(String jti) {
        String jwt = stringRedisTemplate.boundValueOps(jti).get();
        return jwt;
    }
}

相關文章