Spring Security OAuth2.0系列文章:
前面兩篇文章詳細講解了如何基於spring boot + oath2.0搭建認證中心和資源中心,本篇文章將會講解整合jwt以及將客戶端資訊和授權碼資訊儲存到資料庫。
一、 JWT
1. JWT簡介
JSON Web Token(JWT)是一個開放的行業標準(RFC 7519),它定義了一種簡介的、自包含的協議格式,用於在通訊雙方傳遞json物件,傳遞的資訊經過數字簽名可以被驗證和信任。JWT可以使用HMAC演算法或使用RSA的公鑰/私鑰對來簽名,防止被篡改。
標準: https://tools.ietf.org/html/rfc7519
JWT令牌的優點:
1)jwt基於json,非常方便解析。
2)可以在令牌中自定義豐富的內容,易擴充套件。
3)通過非對稱加密演算法及數字簽名技術,JWT防止篡改,安全性高。
4)資源服務使用JWT可不依賴認證服務即可完成授權。
缺點:
1)JWT令牌較長,佔儲存空間比較大,這意味著會耗費一定的頻寬資源
2)JWT簽名和驗籤都要耗費處理器資源
2. JWT令牌結構
JWT令牌由三部分組成,每部分中間使用點(.)分隔,比如:xxxxx.yyyyy.zzzzz
2.1 Header
頭部包括令牌的型別(即JWT)及使用的雜湊演算法(如HMAC SHA256或RSA)一個例子如下:
下邊是Header部分的內容
{
"alg": "HS256",
"typ": "JWT"
}
將上邊的內容使用Base64Url編碼,得到一個字串就是JWT令牌的第一部分。
2.2 Payload
第二部分是負載,內容也是一個json物件,它是存放有效資訊的地方,它可以存放jwt提供的現成欄位,比如:iss(簽發者),exp(過期時間戳), sub(面向的使用者)等,也可自定義欄位。此部分不建議存放敏感資訊,因為此部分可以解碼還原原始內容。最後將第二部分負載使用Base64Url編碼,得到一個字串就是JWT令牌的第二部分。
一個例子:
{
"sub": "1234567890",
"name": "456",
"admin": true
}
2.3 Signature
第三部分是簽名,此部分用於防止jwt內容被篡改。
這個部分使用base64url將前兩部分進行編碼,編碼後使用點(.)連線組成字串,最後使用header中宣告簽名演算法進行簽名。
一個例子:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
- base64UrlEncode(header):jwt令牌的第一部分。
- base64UrlEncode(payload):jwt令牌的第二部分。
- secret:簽名所使用的金鑰。
二、配置JWT
1.認證服務配置JWT
TokenConfig類的修改
@Configuration
public class TokenConfig {
private static final String SIGNING_KEY = "auth123";
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter(){
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);//對稱祕鑰,資源伺服器使用該祕鑰來驗證
return jwtAccessTokenConverter;
}
}
AuthorizationServerTokenServices設定Token增強類
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Bean
public AuthorizationServerTokenServices tokenServices(){
DefaultTokenServices services = new DefaultTokenServices();
services.setClientDetailsService(clientDetailsService);
services.setSupportRefreshToken(true);
services.setTokenStore(tokenStore);
services.setAccessTokenValiditySeconds(7200);
services.setRefreshTokenValiditySeconds(259200);
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Collections.singletonList(jwtAccessTokenConverter));
services.setTokenEnhancer(tokenEnhancerChain);
return services;
}
然後就可以測試了:
得到響應結果:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiJ6aGFuZ3NhbiIsInNjb3BlIjpbIlJPTEVfQURNSU4iLCJST0xFX1VTRVIiLCJST0xFX0FQSSJdLCJleHAiOjE2MTAzNzI5MzUsImF1dGhvcml0aWVzIjpbInAxIiwicDIiXSwianRpIjoiOWQzMzRmZGMtOTcwZC00YmJkLWI2MmMtZDU4MDZkNTgzM2YwIiwiY2xpZW50X2lkIjoiYzEifQ.gZraRNeX-o_jKiH7XQgg3TlUQBpxUcXa2-qR_Treu8U",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiJ6aGFuZ3NhbiIsInNjb3BlIjpbIlJPTEVfQURNSU4iLCJST0xFX1VTRVIiLCJST0xFX0FQSSJdLCJhdGkiOiI5ZDMzNGZkYy05NzBkLTRiYmQtYjYyYy1kNTgwNmQ1ODMzZjAiLCJleHAiOjE2MTA2MjQ5MzUsImF1dGhvcml0aWVzIjpbInAxIiwicDIiXSwianRpIjoiN2U1NzE0NTgtNmU2Zi00YjlmLTkxODQtOWUzZmVmZmQ1YTNjIiwiY2xpZW50X2lkIjoiYzEifQ.wyiS-z-xhBPZSODXZHQVDJCQ6dcmeJjAwBPWe2GhT94",
"expires_in": 7199,
"scope": "ROLE_ADMIN ROLE_USER ROLE_API",
"jti": "9d334fdc-970d-4bbd-b62c-d5806d5833f0"
}
會發現accessToken長了很多,這是因為token是jwt字串,分為三部分,第二部分payload攜帶了很多資訊,開啟jwt.io網站,將上面的accessToken貼上去,可以看到Base64解碼後的資訊:
2.資源服務配置
第一步,將認證服務中的TokenConfig直接拷貝到資源服務中
第二步,修改ResouceServerConfig 類
@Autowired
private TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources
.resourceId(RESOURCE_ID)
// .tokenServices(resourceServerTokenServices)//令牌服務
.tokenStore(tokenStore)
.stateless(true);
}
即可完成資源服務整合jwt的功能。
3.介面測試
POST請求 http://127.0.0.1:30000/oauth/token?client_id=c1&client_secret=secret&grant_type=password&username=zhangsan&password=123 獲取令牌,獲取到accessToken之後攜帶token GET 請求 http://127.0.0.1:30001/r1 ,得到響應結果:
訪問資源r1
即可證明成功。
三、客戶端資訊儲存到資料庫
認證服務客戶端資訊還是儲存在記憶體中,現在將其改造放到資料庫中
1. 新建表
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
`client_id` varchar(255) NOT NULL COMMENT '客戶端標識',
`resource_ids` varchar(255) DEFAULT NULL COMMENT '接入資源列表',
`client_secret` varchar(255) DEFAULT NULL COMMENT '客戶端祕鑰',
`scope` varchar(255) DEFAULT NULL,
`authorized_grant_types` varchar(255) DEFAULT NULL,
`web_server_redirect_uri` varchar(255) DEFAULT NULL,
`authorities` varchar(255) DEFAULT NULL,
`access_token_validity` int(11) DEFAULT NULL,
`refresh_token_validity` int(11) DEFAULT NULL,
`additional_information` longtext,
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`archived` tinyint(4) DEFAULT NULL,
`trusted` tinyint(4) DEFAULT NULL,
`autoapprove` varchar(255) DEFAULT NULL,
PRIMARY KEY (`client_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='接入客戶端資訊';
/*Data for the table `oauth_client_details` */
insert into `oauth_client_details`(`client_id`,`resource_ids`,`client_secret`,`scope`,`authorized_grant_types`,`web_server_redirect_uri`,`authorities`,`access_token_validity`,`refresh_token_validity`,`additional_information`,`create_time`,`archived`,`trusted`,`autoapprove`) values
('c1','res1','$2a$10$X2xVwW.7cOEh2niPqHYAne9EnjRJFj7QI4TqfmnDou9fT/45sCFEm','ROLE_ADMIN,ROLE_USER,ROLE_API','client_credentials,password,authorization_code,implicit,refresh_token','https://www.baidu.com',NULL,7200,259200,NULL,'2021-01-11 09:09:53',0,0,'false'),
('c2','res2','$2a$10$X2xVwW.7cOEh2niPqHYAne9EnjRJFj7QI4TqfmnDou9fT/45sCFEm','ROLE_API','client_credentials,password,authorization_code,implicit,refresh_token','https://www.baidu.com',NULL,31536000,2592000,NULL,'2021-01-11 09:09:56',0,0,'false');
/*Table structure for table `oauth_code` */
DROP TABLE IF EXISTS `oauth_code`;
CREATE TABLE `oauth_code` (
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`code` varchar(255) DEFAULT NULL,
`authentication` blob,
KEY `code_index` (`code`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;
上述SQL新建了兩張表oauth_client_details
以及oauth_code
分別用於儲存客戶端資訊以及授權碼資訊,由於使用了jwt token(jwt token本身就儲存了資料),所以不再儲存資料庫,兩張表均為spring oauth2.0內建表,不需要寫SQL,內建框架自動識別表。
2.修改配置
對應上述兩張表,分別修改Bean物件的建立為jdbc型別的:
@Bean
public AuthorizationCodeServices authorizationCodeServices(DataSource dataSource){
return new JdbcAuthorizationCodeServices(dataSource);
}
@Bean
public ClientDetailsService clientDetailsService(DataSource dataSource) {
JdbcClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
clientDetailsService.setPasswordEncoder(passwordEncoder);
return clientDetailsService;
}
之後修改客戶端配置物件:
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetailsService);
// clients.inMemory()
// .withClient("c1")
// .secret(new BCryptPasswordEncoder().encode("secret"))//$2a$10$0uhIO.ADUFv7OQ/kuwsC1.o3JYvnevt5y3qX/ji0AUXs4KYGio3q6
// .resourceIds("r1")
// .authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")
// .scopes("all")
// .autoApprove(false)
// .redirectUris("https://www.baidu.com");
}
完成修改。
3.介面測試
GET請求:http://127.0.0.1:30000/oauth/authorize?client_id=c1&response_type=code&scope=ROLE_API&redirect_uri=https://www.baidu.com 獲取授權碼後,觀察表oauth_code,裡面應當已經有了授權碼資料。
四、原始碼地址
原始碼地址:https://gitee.com/kdyzm/spring-security-oauth-study/tree/v4.0.0
我的部落格地址:https://blog.kdyzm.cn/