統一鑑權認證是一個基礎服務。它幾乎在所有企業內部都需要,企業內部只要有兩個以上系統存在,就有必要實現一套統一的授權系統,否則使用者使用非常地麻煩,需要在不同系統之間來回登入切換。特別是在微服務大行其道的今天,這個統一授權認證服務更是一個基礎和關鍵入口。實現的方案有很多種,但都大同小異。
本文主要介紹授權認證服務架構方案設計及實現,這個實踐也是本人在企業內部成功實現的經驗總結。從舊有系統Cas認證方式到升級Oauth2.0認證,怎麼保持兩套認證體(Cas和Oauth2)互認整個過程遇到不少問題,以及針對問題解決。將從如下幾個方面進行描述。文章比較長,各位看官花點耐心呀!
目錄結構
1、概念介紹
1.1、什麼是認證
1.2、什麼是授權
1.3、什麼是鑑權
1.4、什麼是許可權控制
1.5、三者關係
2、Http認證方案
2.1 認證流程圖
2.2 認證步驟解析
2.3 優缺點對比
2.4 使用場景
3、Session-Cookie認證方案
3.1 認證流程圖
3.2 認證步驟解析
3.3 優缺點對比
3.4 使用場景
3.5 程式碼實現
4、Token認證方案
4.1 Token認證原理
4.2 重新整理Token
4.3 Token與Session-Cookie區別
5、OAuth2認證方案
5.1 OAuth2定義
5.2 OAuth2角色
5.3 OAuth2認證流程
6、JWT認證方案
6.1 JWT定義
6.2 JWT組成
6.3 JWT使用
6.4 JWT認證流程
6.5 JWT優缺點
7、集團統一授權認證架構方案
5.1 方案設計
5.2 關鍵問題
5.3 企業內部實踐
5.4 關鍵程式碼
8、總結
1、概念介紹
1.1、什麼是認證
認證(Identification)是指根據宣告者所特有的識別資訊,確認宣告者的身份。
白話文的意思就是:你需要用身份證證明你自己是你自己。
比如我們常見的認證技術:
1.2、什麼是授權
授權(Authorization):在資訊保安領域是指資源所有者委派執行者,賦予執行者指定範圍的資源操作許可權,以便對資源的相關操作。
在現實生活領域例如:銀行卡(由銀行派發)、門禁卡(由物業管理處派發)、鑰匙(由房東派發),這些都是現實生活中授權的實現方式。
在網際網路領域例如:web 伺服器的 session 機制、web 瀏覽器的 cookie 機制、頒發授權令牌(token)等都是一個授權的機制。
1.3、什麼是鑑權
鑑權(Authentication)在資訊保安領域是指對於一個宣告者所宣告的身份權利,對其所宣告的真實性進行鑑別確認的過程。
若從授權出發,則會更加容易理解鑑權。授權和鑑權是兩個上下游相匹配的關係,先授權,後鑑權。
在現實生活領域:門禁卡需要通過門禁卡識別器,銀行卡需要通過銀行卡識別器;
在網際網路領域:校驗 session/cookie/token 的合法性和有效性
鑑權是一個承上啟下的一個環節,上游它接受授權的輸出,校驗其真實性後,然後獲取許可權(permission),這個將會為下一步的許可權控制做好準備。
1.4、什麼是許可權控制
許可權控制(Access/Permission Control)將可執行的操作定義為許可權列表,然後判斷操作是否允許/禁止
對於許可權控制,可以分為兩部分進行理解:一個是許可權,另一個是控制。許可權是抽象的邏輯概念,而控制是具體的實現方式。
在現實生活領域中:以門禁卡的許可權實現為例,一個門禁卡,擁有開公司所有的門的許可權;一個門禁卡,擁有管理員角色的許可權,因而可以開公司所有的門。
在網際網路領域:通過 web 後端服務,來控制介面存取,允許或拒絕存取請求。
1.5 認證、授權、鑑權和許可權控制的關係
看到這裡,我們應該明白了認證、授權、鑑權和許可權控制這四個環節是一個前後依次發生、上下游的關係,如下圖所示:
需要說明的是,這四個環節在有些時候會同時發生。例如在下面的幾個場景:
2、Http認證方案
在 HTTP 中,基本認證方案(Basic Access Authentication)是允許使用者端(通常指的就是網頁瀏覽器)在請求時,通過使用者提供使用者名稱和密碼的方式,實現對使用者身份的驗證。
因為幾乎所有的線上網站都不會走該認證方案,所以該方案大家瞭解即可
2.1 認證流程圖
2.2 認證步驟解析
(1)使用者端(如瀏覽器):向伺服器請求一個受限的列表資料或資源,例如欄位如下
GET /list/ HTTP/1.1
Host: www.baidu.com
Authorization: Basic aHR0cHdhdGNoOmY=
(2)伺服器:使用者端你好,這個資源在安全區 baidu.com裡,是受限資源,需要基本認證;
並且向用戶端返回 401 狀態碼(Unauthorized 未被授權的)以及附帶提供了一個認證域www-Authenticate: Basic realm=」baidu.com」要求進行身份驗證;
其中Basic就是驗證的模式,而realm="baidu.com"說明使用者端需要輸入這個安全域的使用者名稱和密碼,而不是其他域的
HTTP/1.1 401 Unauthorized
www-Authenticate: Basic realm= "baidu.com"
(3)使用者端:伺服器,我已經攜帶了使用者名稱和密碼給你了,你看一下;(注:如使用者端是瀏覽器,那麼此時會自動彈出一個彈窗,讓使用者輸入使用者名稱和密碼);
輸入完使用者名稱和密碼後,則使用者端將使用者名稱及密碼以 Base64 加密方式傳送給伺服器
傳送的格式如下 (其中 Basic 內容為:使用者名稱:密碼 的 ase64 形式):
GET /list/ HTTP/1.1
Authorization: Basic Ksid2FuZzp3YW5n==
(4)伺服器:使用者端你好,我已經校驗了Authorization欄位你的使用者名稱和密碼,是正確的,這是你要的資源。
成功:HTTP/1.1 200 OK
失敗:HTTP/1.1 403 Forbidden
2.3 優缺點對比
2.3.1 優點
實現簡單,基本所有流行的瀏覽器都支援
2.3.2 缺點
(1)不安全:
(2)無法主動登出:
由於 HTTP 協定沒有提供機制清除瀏覽器中的 Basic 認證資訊,除非分頁或瀏覽器關閉、或使用者清除歷史記錄。
2.4 使用場景
內部網路,或者對安全要求不是很高的網路。
3、Session-Cookie認證方案
Session-Cookie認證是利用伺服器端的Session(對談)和瀏覽器(使用者端)的 Cookie 來實現的前後端通訊認證模式。
在理解這句話之前我們先簡單瞭解下什麼是 Cookie以及什麼是 Session?
3.1 什麼是 Cookie
眾所周知,HTTP 是無狀態的協定(對於事務處理沒有記憶能力,每次使用者端和伺服器端對談完成時,伺服器端不會儲存任何對談資訊);
所以為了讓伺服器區分不同的使用者端,就必須主動的去維護一個狀態,這個狀態用於告知伺服器端前後兩個請求是否來自同一瀏覽器。而這個狀態可以通過Cookie去實現。
特點:
3.2 什麼是 Session
Session 的抽象概念是對談,是無狀態協定通訊過程中,為了實現中斷/繼續操作,將使用者和伺服器之間的互動進行的一種抽象;
具體來說,是伺服器生成的一種 Session 結構,可以通過多種方式儲存,如記憶體、資料庫、檔案等,大型網站一般有專門的 Session 伺服器叢集來儲存使用者對談;
原理流程:
特點:
與 Cookie 的差異:
看到這裡可能就有人想到了,Session-Cookie是不是就是把Session儲存在了使用者端的Cookie中呢?是的,的確是這樣的,我們接著往下看
3.3 Session-Cookie 的認證流程圖
3.4 Session-Cookie 認證步驟解析
注:可以使用簽名對sid進行加密處理,伺服器端會根據對應的secret金鑰進行解密 (非必須步驟)
3.5 Session-Cookie 優缺點對比
優點
缺點
3.6 使用場景
4、Token認證方案
現在我們已經得知,Session-Cookie的一些缺點,以及 Session 的維護給伺服器端造成很大困擾,我們必須找地方存放它,又要考慮分散式的問題,甚至要單獨為了它啟用一套 Redis 叢集。那有沒有更好的辦法?
那Token就應運而生了
4.1 Token認證原理
Token是一個令牌,使用者端存取伺服器時,驗證通過後伺服器端會為其簽發一張令牌,之後使用者端就可以攜帶令牌存取伺服器,伺服器端只需要驗證令牌的有效性即可。
一句話概括;存取資源介面(API)時所需要的資源憑證
一般 Token 的組成:
uid(使用者唯一的身份標識) +time(當前時間的時間戳) +sign(簽名,Token的前幾位以雜湊演演算法壓縮成的一定長度的十六進位制字串)
Token 的認證流程圖:
Token 認證步驟解析:
Token 的優點:
Token 的缺點:
4.2 重新整理 Token
業務介面用來鑑權的 Token,我們稱之為Access Token。
為了安全,我們的Access Token有效期一般設定較短,以避免被盜用。但過短的有效期會造成Access Token經常過期,過期後怎麼辦呢?
一種辦法是:重新整理 Access Token,讓使用者重新登入獲取新 Token,會很麻煩;
另外一種辦法是:再來一個 Token,一個專門生成 Access Token 的 Token,我們稱為Refresh Token;
Refresh Token 的認證流程圖:
Refresh Token 認證步驟解析:
4.3 Token 和 Session-Cookie 的區別
Session-Cookie和Token有很多類似的地方,但是Token更像是Session-Cookie的升級改良版。
如果你的使用者資料可能需要和第三方共用,或者允許第三方呼叫API介面,用Token 。如果永遠只是自己的網站,自己的App,用什麼就無所謂了。
5、OAuth2.0認證方案
5.1 OAuth2.0定義
OAuth 2.0 是一個開放授權標準,它允許使用者讓第三方應用基於令牌Token的授權,在無需暴露使用者密碼的情況下使第三應用能獲取對使用者資料的有限存取許可權 。
OAuth 2.0定義了四種授權許可型別:
5.2 OAuth2.0角色
(A)資源擁有者(RO)
(B)使用者端(Client)
(C)資源伺服器(RS)
(D)授證伺服器(AS)。
5.3 OAuth2.0認證流程
5.3.1、OAuth 2.0流程圖
關鍵步驟:
(A)使用者開啟使用者端以後,使用者端要求使用者給予授權。
(B)使用者同意給予使用者端授權。
(C)使用者端使用上一步獲得的授權,向認證伺服器申請令牌。
(D)授權認證伺服器對使用者端進行認證以後,確認無誤,同意發放令牌。
(E)使用者端使用令牌,向資源伺服器申請獲取資源。
(F)資源伺服器確認令牌無誤,同意向用戶端開放資源。
5.3.2、授權碼模式
授權碼模式(authorization code)是功能最完整、流程最嚴密的授權模式。
它的特點就是通過使用者端的後臺伺服器,與「服務提供商」的授權認證中心進行互動
關鍵步驟:
(A)使用者存取使用者端,後者將前者導向認證伺服器。
(B)使用者選擇是否給予使用者端授權。
(C)假設使用者給予授權,認證伺服器將使用者導向使用者端事先指定的"重定向URI"(redirection URI),同時附上一個授權碼。
(D)使用者端收到授權碼,附上早先的"重定向URI",向認證伺服器申請令牌。這一步是在使用者端的後臺的伺服器上完成的,對使用者不可見。
(E)認證伺服器核對了授權碼和重定向URI,確認無誤後,向用戶端傳送存取令牌(access token)和更新令牌(refresh token)。
說明備註:
第1步驟中,使用者端申請認證的URI,包含以下引數:
response_type:表示授權型別,必選項,此處的值固定為"code"
client_id:表示使用者端的ID,必選項
redirect_uri:表示重定向URI,可選項
scope:表示申請的許可權範圍,可選項
state:表示使用者端的當前狀態,可以指定任意值,認證伺服器會原封不動地返回這個值。
5.3.2、隱式許可模式
隱式許可模式(implicit grant type)不通過第三方應用程式的伺服器,直接在瀏覽器中向認證伺服器申請令牌,跳過了"授權碼"這個步驟,因此得名。
所有步驟在瀏覽器中完成,令牌對存取者是可見的,且使用者端不需要認證。
關鍵步驟:
(A)使用者端將使用者導向認證伺服器。
(B)使用者決定是否給於使用者端授權。
(C)假設使用者給予授權,認證伺服器將使用者導向使用者端指定的"重定向URI",並在URI的Hash部分包含了存取令牌。
(D)瀏覽器向資源伺服器發出請求,其中不包括上一步收到的Hash值。
(E)資源伺服器返回一個網頁,其中包含的程式碼可以獲取Hash值中的令牌。
(F)瀏覽器執行上一步獲得的指令碼,提取出令牌。
(G)瀏覽器將令牌發給使用者端。
說明備註:
A步驟中,使用者端發出的HTTP請求,包含以下引數:
response_type:表示授權型別,此處的值固定為"token",必選項。
client_id:表示使用者端的ID,必選項。
redirect_uri:表示重定向的URI,可選項。
scope:表示許可權範圍,可選項。
state:表示使用者端的當前狀態,可以指定任意值,認證伺服器會原封不動地返回這個值。
5.3.3、密碼憑證模式
密碼憑證模式(Resource Owner Password Credentials Grant)中,使用者向用戶端提供自己的使用者名稱和密碼。
使用者端使用這些資訊,向"服務提供商"索要授權。
在這種模式中,使用者必須把自己的密碼給使用者端,但是使用者端不得儲存密碼。
這通常用在使用者對使用者端高度信任的情況下,並且只有授權認證中心在其他授權模式無法執行的情況下,才能考慮使用這種模式。
關鍵步驟:
(A)使用者端將使用者導向認證伺服器。
(B)使用者決定是否給於使用者端授權。
(C)假設使用者給予授權,認證伺服器將使用者導向使用者端指定的"重定向URI",並在URI的Hash部分包含了存取令牌。
(D)瀏覽器向資源伺服器發出請求,其中不包括上一步收到的Hash值。
(E)資源伺服器返回一個網頁,其中包含的程式碼可以獲取Hash值中的令牌。
(F)瀏覽器執行上一步獲得的指令碼,提取出令牌。
(G)瀏覽器將令牌發給使用者端。
說明備註:
B步驟中,使用者端發出的HTTP請求,包含以下引數:
grant_type:表示授權型別,此處的值固定為"password",必選項。
username:表示使用者名稱,必選項。
password:表示使用者的密碼,必選項。
scope:表示許可權範圍,可選項。
5.3.4、密碼憑證模式
使用者端憑證模式(Client Credentials Grant)指使用者端以自己的名義,而不是以使用者的名義,向授權認證中心進行認證。嚴格地說,使用者端憑證模式並不屬於OAuth框架所要解決的問題。在這種模式中,使用者直接向用戶端註冊,使用者端以自己的名義要求"服務提供商"提供服務,其實不存在授權問題。
關鍵步驟:
(A)使用者端向認證伺服器進行身份認證,並要求一個存取令牌。
(B)認證伺服器確認無誤後,向用戶端提供存取令牌。
備註說明:
A步驟中,使用者端發出的HTTP請求,包含以下引數:
granttype:表示授權型別,此處的值固定為"clientcredentials",必選項。
scope:表示許可權範圍,可選項。
6、JWT Token驗證
我們知道了Token的使用方式以及組成,我們不難發現,伺服器端驗證使用者端傳送過來的 Token 時,還需要查詢資料庫獲取使用者基本資訊,然後驗證 Token 是否有效;
這樣每次請求驗證都要查詢資料庫,增加了查庫帶來的延遲等效能消耗;
那麼這時候業界常用的JWT就應運而生了!!!
6.1 JWT定義
JWT是Auth0提出的通過對 JSON 進行加密簽名來實現授權驗證的方案;
就是登入成功後將相關使用者資訊組成 JSON 物件,然後對這個物件進行某種方式的加密,返回給使用者端;使用者端在下次請求時帶上這個 Token;伺服器端再收到請求時校驗 token 合法性,其實也就是在校驗請求的合法性。
6.2 JWT 的組成
JWT 由三部分組成:Header 頭部、Payload 負載和Signature 簽名
它是一個很長的字串,中間用點(.)分隔成三個部分。列如 :
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Header 頭部:
在 Header 中通常包含了兩部分:
{
"alg": "HS256",
"typ": "JWT"
}
Payload 負載:
它包含一些宣告 Claim (實體的描述,通常是一個 User 資訊,還包括一些其他的後設資料) ,用來存放實際需要傳遞的資料,JWT 規定了7個官方欄位:
除了官方欄位,你還可以在這個部分定義私有欄位,下面就是一個例子。
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
Signature 簽名
Signature 部分是對前兩部分的簽名,防止資料篡改。
首先,需要指定一個金鑰(secret)。這個金鑰只有伺服器才知道,不能洩露給使用者。然後,使用 Header 裡面指定的簽名演演算法(預設是 HMAC SHA256),按照下面的公式產生簽名。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
JWT 加密、解密標例
6.3 JWT使用
使用者端收到伺服器返回的 JWT,可以儲存在 Cookie 裡面,也可以儲存在 localStorage。
此後,使用者端每次與伺服器通訊,都要帶上這個 JWT。你可以把它放在 Cookie 裡面自動傳送,但是這樣不能跨域,所以更好的做法是放在 HTTP 請求的頭資訊Authorization欄位裡面。
Authorization: Bearer <token>
6.4 JWT認證流程
其實 JWT 的認證流程與 Token 的認證流程差不多,只是不需要再單獨去查詢資料庫查詢使用者使用者;簡要概括如下:
6.5 JWT優缺點
優點:
缺點:
7、集團統一授權認證架構方案
首先感謝各位看官!文章很長,為了普及認證基本原理和知識不得花重手筆詳細描述清楚,能一路看到這裡,恭喜!應該對認證體系有了系統性的瞭解和認識。
通過上面各種方案分析整理對比,我們已經有了一個清晰結構認知。下面我們來設計一套適用於集團內部的統一的授權認證方案。
我們集團內部有很多個業務系統包括:OA系統、商旅系統、財務系統、培訓系統、BPM、ERP、MES、MLP、MOM、MSCS、WCC、MTCS、TIMS、SRM、WMS、PLM、......
行銷系統(這裡面又包含60多個子系統,如:設計軟體、客服400系統、客情調查問卷、訂單系統,賬號系統、活動、促銷、會員、CRM、電商引流、行銷補貼、學習培訓、傳單系統等以及各種獨立的業務中臺和資料中臺),這些系統都要打通統一授權體系,實現單點登入。所以整體實施方案是比較複雜的。
當時在設計授權方案是遇到不小挑戰,基於已有業務存在兩個重要困難點:
1、集團內部已經有非常多個子系統,算下來大大小小有130多個子系統;
2、集團職能體系和製造體系已經有一套相對早期的基於CAS單點登入系統,而且很難改得動;主要難辦的是隻要涉及到財務和生產製造系統,保持系統指標穩定壓倒其他一切;行銷體系的各個系統相對比較新,採用Oauth2協定認證,實現行銷體系內部統一認證。但還是不能滿足業務的要求,兩套認證體系各自為政,這是認人不可接受的。最終的目標是:集團內部所有系統一次登入必須互通互聯。
7.1 方案設計
OAuth2內部整體概要
Oauth2認證方案
兩套體系互認
7.2 關鍵問題
授權認證中心主要提供四個端點:認證端點、令牌頒發端點、令牌校驗端點和登出端點。
集團內部Cas單點與行銷系統Oauth2.0認證內部打通隧道建立互信機制。
1、使用者登入職能或製造系統在CAS認證通過,發起一次行銷系統的Oauth2認證服務訊息通知,並完成一次授信;
2、使用者登入行銷系統在oauth2認證服務鑑權通過,發起一次CAS通訊通過使用者關鍵資訊進行TGT交換,並做一次token和tgt繫結動作。
3、使用者在任意一個體系登出,兩邊系統都會互發一次訊息通知。
通過上次機制設計實現了兩套獨立認證體系互通互信。不用修改原有舊系統登入邏輯,也是最少代價方案實現全集團系統單點登入 。
7.3 核心程式碼實現
@RestController
@RequestMapping("/oauth")
@Module("令牌授權")
public class AccessTokenController implements AccessTokenRemoteService {
@Resource
private AccessTokenService accessTokenService;
@RestApi(name = "授權碼模式授權",no = "Auth02",idx = 1)
@PostMapping(value = "/token/authCode")
public Response<AccessToken> authByAuthCode(@RequestBody AuthCodeAuthentication authentication){
return accessTokenService.authByAuthCode(authentication);
}
@RestApi(name = "密碼模式授權",no = "Auth03",idx = 2)
@PostMapping(value = "/token/password")
public Response<AccessToken> authByPassword(@RequestBody PasswordAuthentication authentication) {
return accessTokenService.authByPassword(authentication);
}
@RestApi(name = "使用者端憑證模式授權",no = "Auth04",idx = 3)
@PostMapping(value = "/token/clientCredentials")
public Response<AccessToken> authByRefreshToken(@RequestBody ClientCredentialsAuthentication authentication) {
return accessTokenService.authByClientCredentials(authentication);
}
@RestApi(name = "重新整理令牌授權",no = "Auth05",idx = 4)
@PostMapping(value = "/token/refreshToken")
public Response<AccessToken> authByRefreshToken(@RequestBody RefreshTokenAuthentication authentication) {
return accessTokenService.authByRefreshToken(authentication);
}
@Service
public class AccessTokenService {
@Resource
private RedisTokenStore redisTokenStore;
@Resource
private ClientDetailsRepository clientDetailsRepository;
@Resource
private LoginUserRepository loginUserRepository;
@Value("${miop.auth.defaultClientId:}")
public String defaultClientId;
@Resource
private RedisTemplate redisTemplate;
/**
* 獲取登入使用者
* @param accessToken
* @return
*/
public Response<ShiroUser> getUserByToken(String accessToken) {
OAuth2Authentication auth2Authentication = redisTokenStore.readAuthentication(accessToken);
if(auth2Authentication == null){
return Response.failure("令牌已失效");
}
UserCO user = (UserCO)auth2Authentication.getUserAuthentication().getPrincipal();
if(user == null){
return Response.failure("令牌已失效");
}
SpringContextUtil.getBean(AccessTokenService.class).renewalAccessToken(accessToken);
ShiroUser shiroUser = BeanToolkit.instance().copy(user,ShiroUser.class);
shiroUser.setAccessToken(accessToken);
return Response.of(shiroUser);
}
/**
* 一分鐘內只更新一次token的過期時間
* @param accessToken
* @return
*/
@Cacheable(value = "renewalAccessToken",key = "#accessToken")
public char renewalAccessToken(String accessToken){
redisTokenStore.renewalAccessToken(accessToken);
return '1';
}
/**
* 登出使用者
* @param accessToken
* @return
*/
public Response logout(String accessToken) {
OAuth2Authentication auth2Authentication = redisTokenStore.readAuthentication(accessToken);
if(auth2Authentication != null){
OAuth2AccessToken oAuth2AccessToken = redisTokenStore.getAccessToken(auth2Authentication);
if(oAuth2AccessToken != null){
redisTokenStore.removeRefreshToken(oAuth2AccessToken.getRefreshToken().getValue());
redisTokenStore.removeAccessToken(accessToken);
}
}
loginUserRepository.logout(accessToken);
return Response.success();
}
public Response<String> createJwt(String accessToken) {
Response<ShiroUser> shiroUser = getUserByToken(accessToken);
Map claims = (Map) JSON.toJSON(shiroUser);
String jwt = JwtUtils.createJwt(claims,20);
return Response.of(jwt);
}
/**
* 使用者密碼授權
* @param authentication
* @return
*/
public Response<AccessToken> authByPassword(PasswordAuthentication authentication){
AuthLog authLog = createAuthLog(authentication);
try {
AccessToken accessToken = auth(authentication,GrantType.PASSWORD);
authLog.setStatus(Status.NORMAL.getValue());
ShiroUser user = accessToken.getUser();
if(user != null){
authLog.setUserName(user.getName());
}
return Response.of(accessToken);
}catch (Exception e){
authLog.setStatus(Status.UN_NORMAL.getValue());
authLog.setMsg(e.getMessage());
throw e;
}finally {
redisTemplate.opsForList().rightPush(LogConstants.LOGIN_LOG_REDIS_QUEUE, authLog);
}
}
public Response<AccessToken> authByAuthCode(AuthCodeAuthentication authentication){
AccessToken accessToken = auth(authentication,GrantType.AUTHORIZATION_CODE);
return Response.of(accessToken);
}
public Response<AccessToken> authByRefreshToken(RefreshTokenAuthentication authentication){
AccessToken accessToken = auth(authentication,GrantType.REFRESH_TOKEN);
return Response.of(accessToken);
}
public Response<AccessToken> authByClientCredentials(ClientCredentialsAuthentication authentication){
AccessToken accessToken = auth(authentication,GrantType.CLIENT_CREDENTIALS);
return Response.of(accessToken);
}
private AccessToken auth(BaseAuthentication authentication,GrantType grantType){
ClientDetails clientDetails = clientDetailsRepository.selectByIdWithCache(authentication.getClientId());
if(!StringUtil.equals(clientDetails.getClientSecret(),authentication.getClientSecret())){
throw new AuthException("無效的 client credentials:"+authentication.getClientSecret());
}
if(!clientDetails.getAuthorizedGrantTypes().contains(grantType.getValue())){
throw new AuthException("該clientId不允許"+grantType.getValue()+"授權方式");
}
for (String scope : authentication.getScope().split(",")) {
if (!clientDetails.getScope().contains(scope)) {
throw new AuthException("不合法的scope:"+scope);
}
}
TokenRequest tokenRequest = new TokenRequest((Map)JSON.toJSON(authentication), authentication.getClientId(),
Arrays.asList(authentication.getScope().split(",")), grantType.getValue());
OAuth2AccessToken oAuth2AccessToken = AuthorizationServer.endpoints.getTokenGranter().grant(grantType.getValue(),tokenRequest);
AccessToken accessToken = getAccessToken(oAuth2AccessToken);
return accessToken;
}
/**
* 轉成自定義的令牌物件
* @param oAuth2AccessToken
* @return
*/
private AccessToken getAccessToken(OAuth2AccessToken oAuth2AccessToken) {
DefaultOAuth2AccessToken defaultOAuth2AccessToken = (DefaultOAuth2AccessToken)oAuth2AccessToken;
DefaultExpiringOAuth2RefreshToken refreshToken = (DefaultExpiringOAuth2RefreshToken)defaultOAuth2AccessToken.getRefreshToken();
AccessToken accessToken = new AccessToken();
accessToken.setTokenType(defaultOAuth2AccessToken.getTokenType());
accessToken.setAccessToken(oAuth2AccessToken.getValue());
accessToken.setAccessTokenExpiresIn(oAuth2AccessToken.getExpiresIn());
if(oAuth2AccessToken.getRefreshToken() != null){
accessToken.setRefreshToken(defaultOAuth2AccessToken.getRefreshToken().getValue());
accessToken.setRefreshTokenExpiresIn((int)((refreshToken.getExpiration().getTime() - System.currentTimeMillis())/1000));
}
OAuth2Authentication auth2Authentication = redisTokenStore.readAuthentication(oAuth2AccessToken.getValue());
if(auth2Authentication != null){
Object principal = auth2Authentication.getUserAuthentication().getPrincipal();
ShiroUser shiroUser = BeanToolkit.instance().copy(principal,ShiroUser.class);
shiroUser.setAccessToken(oAuth2AccessToken.getValue());
accessToken.setUser(shiroUser);
Map claims = (Map) JSON.toJSON(shiroUser);
String jwt = JwtUtils.createJwt(claims,20);
accessToken.setJwt(jwt);
}
return accessToken;
}
private AuthLog createAuthLog(PasswordAuthentication authentication) {
AuthLog authLog = new AuthLog();
authLog.setClientId(authentication.getClientId());
authLog.setGrantType(GrantType.PASSWORD.getValue());
authLog.setAccessTime(LocalDateTime.now());
authLog.setIp(RequestUtil.getIpAddress());
authLog.setTraceId(Trace.traceId.get());
authLog.setAccount(authentication.getAccount());
authLog.setOrgNo(authentication.getOrgNo());
return authLog;
}
8、總結
1、通過本文,我們可以全現系統學習認證體系的原理和設計方案;
2、所有公司只有兩個以上系統存在就有必要實現授證登入體系,單獨從認證服務本身來看是比較簡單的,但是跟多個系統整合的時候還是遇到的問題坑,特別是大企業裡系統錯綜複雜,動則上個百個系統集團單點登入,要成功實施上也不是那麼簡單的事,希望通過本文我們能學到一點啟發。
3、輕巧方案,用最小的代價,最快方式去實現。認證授權還是有一些細節的坑,比如跨域問題、安全攻防的問題本文還沒有提到,等有空再補了!有需要的同學持續關注吧,我會持續完善和修改,暫時先偷一下懶。
4、本文主要是工作中總結的資料方案,也有參照到一些網上的資料(時間太久也不知道原作者是誰,若有涉及侵權問題請聯絡本人)。