1. 程式人生 > >SpringOauth2.0原始碼分析之儲存(五)

SpringOauth2.0原始碼分析之儲存(五)

1.概述

前面幾個章節所述內容如下:

本章節主要敘說Token的儲存情況。預設的情況下,SpringOauth2.0 提供4種方式儲存。第一種是提供了基於mysql的儲存,第二種是基於redis的儲存。第三種基於jvm的儲存,第四種基於Jwt的儲存方式。這裡我們主要分析的是mysql的持久化和redis的持久化。首先分析下儲存的實現類。然後分析下不同儲存方式下,表元資料的資訊。

2.Token儲存的實現

2.1 token儲存的介面詳解

token的儲存是通過TokenStore這個介面實現的,下面我們分析下TokenStore的方法引數。

public interface TokenStore {
//讀取指定的使用者身份認證
	OAuth2Authentication readAuthentication(OAuth2AccessToken token);
// 根據token讀取指定的使用者身份認證
	OAuth2Authentication readAuthentication(String token);
// 儲存token資訊和使用者認證資訊
	void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication);
// 根據tokenValue讀取token資訊
	OAuth2AccessToken readAccessToken(String tokenValue);
// 移除token資訊
	void removeAccessToken(OAuth2AccessToken token);
// 儲存重新整理token資訊
	void storeRefreshToken(OAuth2RefreshToken refreshToken, OAuth2Authentication authentication);
// 讀取重新整理token資訊
	OAuth2RefreshToken readRefreshToken(String tokenValue);
// 讀取Token詳細資訊
	OAuth2AccessToken getAccessToken(OAuth2Authentication authentication);
// 通過客戶端和使用者名稱查詢當前授權的所有token資訊
	Collection<OAuth2AccessToken> findTokensByClientIdAndUserName(String clientId, String userName);
// 查詢當前客戶端下的所有用認證的token資訊
	Collection<OAuth2AccessToken> findTokensByClientId(String clientId);
}

通過上一章節的分析,我們可以知道,當前儲存的主鍵:token_id的生成規則是根據的:

	private static final String CLIENT_ID = "client_id";
	private static final String SCOPE = "scope";
	private static final String USERNAME = "username";

傳輸的這三個值做MD5生成的。所以,同一個客戶端下,可以存在多個使用者的token的資訊。

2.2 TokenStore介面的實現詳解

在這裡插入圖片描述 通過介面的實現可以得出,其主要的有四種方式來儲存Token。

  1. RedisTokenStore 通過Redis的方式進行儲存
  2. JdbcTokenStore 通過Jdbc序列化的方式進行儲存
  3. InMemoryTokenStore 直接將當前的Token資訊儲存在JVM中。
  4. JwtTokenStroe 通過Jwt的方式進行儲存

上面的四種方式進行Token的持久化儲存。其中InMemoryTokenStore是將當前的token資訊儲存到jvm中,重啟服務後當前token資訊將不復存在。所以,只能在測試開發時候使用。

3.Token儲存的元資料資訊

3.1 mysql中token儲存的元資料詳解
CREATE SCHEMA IF NOT EXISTS `oauth2` DEFAULT CHARACTER SET utf8 ;
USE `oauth2` ;

-- -----------------------------------------------------
-- Table `oauth2`.`clientdetails`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `oauth2`.`clientdetails` (
  `appId` VARCHAR(128) NOT NULL,
  `resourceIds` VARCHAR(256) NULL DEFAULT NULL,
  `appSecret` VARCHAR(256) NULL DEFAULT NULL,
  `scope` VARCHAR(256) NULL DEFAULT NULL,
  `grantTypes` VARCHAR(256) NULL DEFAULT NULL,
  `redirectUrl` VARCHAR(256) NULL DEFAULT NULL,
  `authorities` VARCHAR(256) NULL DEFAULT NULL,
  `access_token_validity` INT(11) NULL DEFAULT NULL,
  `refresh_token_validity` INT(11) NULL DEFAULT NULL,
  `additionalInformation` VARCHAR(4096) NULL DEFAULT NULL,
  `autoApproveScopes` VARCHAR(256) NULL DEFAULT NULL,
  PRIMARY KEY (`appId`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;


-- -----------------------------------------------------
-- Table `oatuh2`.`oauth_access_token`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `oauth2`.`oauth_access_token` (
  `token_id` VARCHAR(256) NULL DEFAULT NULL,
  `token` BLOB NULL DEFAULT NULL,
  `authentication_id` VARCHAR(128) NOT NULL,
  `user_name` VARCHAR(256) NULL DEFAULT NULL,
  `client_id` VARCHAR(256) NULL DEFAULT NULL,
  `authentication` BLOB NULL DEFAULT NULL,
  `refresh_token` VARCHAR(256) NULL DEFAULT NULL,
  PRIMARY KEY (`authentication_id`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;


-- -----------------------------------------------------
-- Table `oatuh2`.`oauth_approvals`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `oauth2`.`oauth_approvals` (
  `userId` VARCHAR(256) NULL DEFAULT NULL,
  `clientId` VARCHAR(256) NULL DEFAULT NULL,
  `scope` VARCHAR(256) NULL DEFAULT NULL,
  `status` VARCHAR(10) NULL DEFAULT NULL,
  `expiresAt` DATETIME NULL DEFAULT NULL,
  `lastModifiedAt` DATETIME NULL DEFAULT NULL)
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;


-- -----------------------------------------------------
-- Table `oatuh2`.`oauth_client_details`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `oauth2`.`oauth_client_details` (
  `client_id` VARCHAR(128) NOT NULL,
  `resource_ids` VARCHAR(256) NULL DEFAULT NULL,
  `client_secret` VARCHAR(256) NULL DEFAULT NULL,
  `scope` VARCHAR(256) NULL DEFAULT NULL,
  `authorized_grant_types` VARCHAR(256) NULL DEFAULT NULL,
  `web_server_redirect_uri` VARCHAR(256) NULL DEFAULT NULL,
  `authorities` VARCHAR(256) NULL DEFAULT NULL,
  `access_token_validity` INT(11) NULL DEFAULT NULL,
  `refresh_token_validity` INT(11) NULL DEFAULT NULL,
  `additional_information` VARCHAR(4096) NULL DEFAULT NULL,
  `autoapprove` VARCHAR(256) NULL DEFAULT NULL,
  PRIMARY KEY (`client_id`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;


-- -----------------------------------------------------
-- Table `oatuh2`.`oauth_client_token`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `oauth2`.`oauth_client_token` (
  `token_id` VARCHAR(256) NULL DEFAULT NULL,
  `token` BLOB NULL DEFAULT NULL,
  `authentication_id` VARCHAR(128) NOT NULL,
  `user_name` VARCHAR(256) NULL DEFAULT NULL,
  `client_id` VARCHAR(256) NULL DEFAULT NULL,
  PRIMARY KEY (`authentication_id`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;


-- -----------------------------------------------------
-- Table `oatuh2`.`oauth_code`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `oauth2`.`oauth_code` (
  `code` VARCHAR(256) NULL DEFAULT NULL,
  `authentication` BLOB NULL DEFAULT NULL)
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;


-- -----------------------------------------------------
-- Table `oatuh2`.`oauth_refresh_token`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `oauth2`.`oauth_refresh_token` (
  `token_id` VARCHAR(256) NULL DEFAULT NULL,
  `token` BLOB NULL DEFAULT NULL,
  `authentication` BLOB NULL DEFAULT NULL)
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;
--------------------- 

表字段和元資料說明:

表名欄位名欄位說明
oauth_client_detailsclient_id主鍵,必須唯一,不能為空.用於唯一標識每一個客戶端(client);在註冊時必須填寫(也可由服務端自動生成).對於不同的grant_type,該欄位都是必須的.在實際應用中的另一個名稱叫appKey,與client_id是同一個概念.
resource_ids客戶端所能訪問的資源id集合,多個資源時用逗號(,)分隔,如:"unity-resource,mobile-resource".該欄位的值必須來源於與security.xml中標籤‹oauth2:resource-server的屬性resource-id值一致.在security.xml配置有幾個‹oauth2:resource-server標籤,則該欄位可以使用幾個該值.在實際應用中,我們一般將資源進行分類,並分別配置對應的‹oauth2:resource-server,如訂單資源配置一個‹oauth2:resource-server,使用者資源又配置一個‹oauth2:resource-server.當註冊客戶端時,根據實際需要可選擇資源id,也可根據不同的註冊流程,賦予對應的資源id.
client_secret用於指定客戶端(client)的訪問密匙;在註冊時必須填寫(也可由服務端自動生成).對於不同的grant_type,該欄位都是必須的.在實際應用中的另一個名稱叫appSecret,與client_secret是同一個概念.
scope指定客戶端申請的許可權範圍,可選值包括read,write,trust;若有多個許可權範圍用逗號(,)分隔,如:"read,write".scope的值與security.xml中配置的‹intercept-urlaccess屬性有關係.如‹intercept-url的配置為
‹intercept-url pattern="/m/**"access="ROLE_MOBILE,SCOPE_READ"/>
則說明訪問該URL時的客戶端必須有read許可權範圍.write的配置值為SCOPE_WRITE,trust的配置值為SCOPE_TRUST.在實際應該中,該值一般由服務端指定,常用的值為read,write.
authorized_grant_types指定客戶端支援的grant_type,可選值包括authorization_code,password,refresh_token,implicit,client_credentials,若支援多個grant_type用逗號(,)分隔,如:"authorization_code,password".在實際應用中,當註冊時,該欄位是一般由伺服器端指定的,而不是由申請者去選擇的,最常用的grant_type組合有:"authorization_code,refresh_token"(針對通過瀏覽器訪問的客戶端);"password,refresh_token"(針對移動裝置的客戶端).implicitclient_credentials在實際中很少使用.
web_server_redirect_uri客戶端的重定向URI,可為空,當grant_type為authorization_codeimplicit時,在Oauth的流程中會使用並檢查與註冊時填寫的redirect_uri是否一致.下面分別說明:
  • 當grant_type=authorization_code時,第一步從spring-oauth-server獲取'code'時客戶端發起請求時必須有redirect_uri引數,該引數的值必須與web_server_redirect_uri的值一致.第二步用'code'換取'access_token'時客戶也必須傳遞相同的redirect_uri.在實際應用中,web_server_redirect_uri在註冊時是必須填寫的,一般用來處理伺服器返回的code,驗證state是否合法與通過code去換取access_token值.在spring-oauth-client專案中,可具體參考AuthorizationCodeController.java中的authorizationCodeCallback方法.
  • 當grant_type=implicit時通過redirect_uri的hash值來傳遞access_token值.如:
    http:
    然後客戶端通過JS等從hash值中取到access_token值.
authorities指定客戶端所擁有的Spring Security的許可權值,可選,若有多個許可權值,用逗號(,)分隔,如:"ROLE_UNITY,ROLE_USER".對於是否要設定該欄位的值,要根據不同的grant_type來判斷,若客戶端在Oauth流程中需要使用者的使用者名稱(username)與密碼(password)的(authorization_code,password),則該欄位可以不需要設定值,因為服務端將根據使用者在服務端所擁有的許可權來判斷是否有許可權訪問對應的API.但如果客戶端在Oauth流程中不需要使用者資訊的(implicit,client_credentials),則該欄位必須要設定對應的許可權值,因為服務端將根據該欄位值的許可權來判斷是否有許可權訪問對應的API.

(請在spring-oauth-client專案中來測試不同grant_type時authorities的變化)

access_token_validity設定客戶端的access_token的有效時間值(單位:秒),可選,若不設定值則使用預設的有效時間值(60*60*12,12小時).在服務端獲取的access_token JSON資料中的expires_in欄位的值即為當前access_token的有效時間值.在專案中,可具體參考DefaultTokenServices.java中屬性accessTokenValiditySeconds.在實際應用中,該值一般是由服務端處理的,不需要客戶端自定義.
refresh_token_validity設定客戶端的refresh_token的有效時間值(單位:秒),可選,若不設定值則使用預設的有效時間值(60*60*24*30,30天).若客戶端的grant_type不包括refresh_token,則不用關心該欄位在專案中,可具體參考DefaultTokenServices.java中屬性refreshTokenValiditySeconds.在實際應用中,該值一般是由服務端處理的,不需要客戶端自定義.
additional_information這是一個預留的欄位,在Oauth的流程中沒有實際的使用,可選,但若設定值,必須是JSON格式的資料,如:
{"country":"CN","country_code":"086"}
按照spring-security-oauth專案中對該欄位的描述Additional information for this client,not need by the vanilla OAuth protocol but might be useful,for example,for storing descriptive information.(詳見ClientDetails.javagetAdditionalInformation()方法的註釋)在實際應用中,可以用該欄位來儲存關於客戶端的一些其他資訊,如客戶端的國家,地區,註冊時的IP地址等等.
create_time資料的建立時間,精確到秒,由資料庫在插入資料時取當前系統時間自動生成(擴充套件欄位)
archived用於標識客戶端是否已存檔(即實現邏輯刪除),預設值為'0'(即未存檔).對該欄位的具體使用請參考CustomJdbcClientDetailsService.java,在該類中,擴充套件了在查詢client_details的SQL加上archived=0條件(擴充套件欄位)
trusted設定客戶端是否為受信任的,預設為'0'(即不受信任的,1為受信任的).該欄位只適用於grant_type="authorization_code"的情況,當用戶登入成功後,若該值為0,則會跳轉到讓使用者Approve的頁面讓使用者同意授權,若該欄位為1,則在登入後不需要再讓使用者Approve同意授權(因為是受信任的).對該欄位的具體使用請參考OauthUserApprovalHandler.java.(擴充套件欄位)
autoapprove設定使用者是否自動Approval操作,預設值為'false',可選值包括'true','false','read','write'.該欄位只適用於grant_type="authorization_code"的情況,當用戶登入成功後,若該值為'true'或支援的scope值,則會跳過使用者Approve的頁面,直接授權.該欄位與trusted有類似的功能,是spring-security-oauth2的2.0版本後新增的新屬性.

在專案中,主要操作oauth_client_details表的類是JdbcClientDetailsService.java,更多的細節請參考該類.也可以根據實際的需要,去擴充套件或修改該類的實現.

oauth_client_tokencreate_time資料的建立時間,精確到秒,由資料庫在插入資料時取當前系統時間自動生成(擴充套件欄位)
token_id從伺服器端獲取到的access_token的值.
token這是一個二進位制的欄位,儲存的資料是OAuth2AccessToken.java物件序列化後的二進位制資料.
authentication_id該欄位具有唯一性,是根據當前的username(如果有),client_id與scope通過MD5加密生成的.具體實現請參考DefaultClientKeyGenerator.java類.
user_name登入時的使用者名稱
client_id

該表用於在客戶端系統中儲存從服務端獲取的token資料,在spring-oauth-server專案中未使用到.對oauth_client_token表的主要操作在JdbcClientTokenServices.java類中,更多的細節請參考該類.

oauth_access_tokencreate_time資料的建立時間,精確到秒,由資料庫在插入資料時取當前系統時間自動生成(擴充套件欄位)
token_id該欄位的值是將access_token的值通過MD5加密後儲存的.
token儲存將OAuth2AccessToken.java物件序列化後的二進位制資料,是真實的AccessToken的資料值.
authentication_id該欄位具有唯一性,其值是根據當前的username(如果有),client_id與scope通過MD5加密生成的.具體實現請參考DefaultAuthenticationKeyGenerator.java類.
user_name登入時的使用者名稱,若客戶端沒有使用者名稱(如grant_type="client_credentials"),則該值等於client_id
client_id
authentication儲存將OAuth2Authentication.java物件序列化後的二進位制資料.
refresh_token該欄位的值是將refresh_token的值通過MD5加密後儲存的.

在專案中,主要操作oauth_access_token表的物件是JdbcTokenStore.java.更多的細節請參考該類.

oauth_refresh_tokencreate_time資料的建立時間,精確到秒,由資料庫在插入資料時取當前系統時間自動生成(擴充套件欄位)
token_id該欄位的值是將refresh_token的值通過MD5加密後儲存的.
token儲存將OAuth2RefreshToken.java物件序列化後的二進位制資料.
authentication儲存將OAuth2Authentication.java物件序列化後的二進位制資料.

在專案中,主要操作oauth_refresh_token表的物件是JdbcTokenStore.java.(與操作oauth_access_token表的物件一樣);更多的細節請參考該類.如果客戶端的grant_type不支援refresh_token,則不會使用該表.

oauth_codecreate_time資料的建立時間,精確到秒,由資料庫在插入資料時取當前系統時間自動生成(擴充套件欄位)
code儲存服務端系統生成的code的值(未加密).
authentication儲存將AuthorizationRequestHolder.java物件序列化後的二進位制資料.

在專案中,主要操作oauth_code表的物件是JdbcAuthorizationCodeServices.java.更多的細節請參考該類.只有當grant_type為"authorization_code"時,該表中才會有資料產生;其他的grant_type沒有使用該表.

3.2 redis中token儲存的元資料詳解

資料儲存在redis中,並不像儲存在mysql中那樣可以做關聯查詢,並且根據redis中的資料結構。SpringOauth2.0 在redis中的儲存結構如下:

  • auth_to_access OAuth2Authentication相關資訊加密後的值,value為string結構 這個主要是通過OAuth2Authentication來獲取OAuth2AccessToken
  • auth:token value為string結構 這個主要用來獲取token的OAuth2Authentication,用來獲取相應的許可權資訊
  • client_id_to_access:clientId value為list結構 這個主要是儲存了每個clientId申請的OAuth2AccessToken的集合 方便用來審計和應急處理跟clientId相關的token
  • access:token value為string 這個主要是通過token值來獲取OAuth2AccessToken
  • uname_to_access:clientId:userId, value的結構是list 儲存OAuth2AccessToken的集合 主要是為了通過clientId,userId來獲取OAuth2AccessToken集合,方便用來獲取及revoke approval

結語

通過上面的分析和元資料描述,基本瞭解了SpringOauth2.0分別在mysql和redis中的持久化資料結構。個人理解這裡框架本身直接將java物件通過java本身的序列化的方式進行儲存,這樣需要更多的儲存空間以及頻寬。同時反序列化需要花費更多的cpu效能。可能覺得需要改進的地方