Spring Security OAuth2.0認證授權三:使用JWT令牌

狂盜一枝梅發表於2021-01-11

Spring Security OAuth2.0系列文章:

前面兩篇文章詳細講解了如何基於spring boot + oath2.0搭建認證中心和資源中心,本篇文章將會講解整合jwt以及將客戶端資訊和授權碼資訊儲存到資料庫。

一、 JWT

1. 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令牌較長,佔儲存空間比較大,這意味著會耗費一定的頻寬資源

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

然後就可以測試了:

POST請求介面:http://127.0.0.1:30000/oauth/token?client_id=c1&client_secret=secret&grant_type=password&username=zhangsan&password=123

得到響應結果:

{
"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解碼後的資訊:

2021-01-11_201526.png

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/

相關文章