在〈Client Credentials 核發流程(二)〉中談過,OAuth 2 本身並沒有規範 Access Token 應該是什麼樣子,如果想要增加 Access Token 的安全(像是避免被竄改),以及增加 Token 本身攜帶資訊的能力,可以使用 JSON Web Tokens(JWT),它對 Token 制定了規範,具有對 Token 簽署,資源伺服器可以直接確認 Token 等優點。
以〈Authorization Code 核發流程〉中的成果為例,如果想改用 JWT,可以在授權伺服器專案的 build.gradle 中加上:
implementation('org.springframework.security:spring-security-jwt')
接著,在組態設定上,必須定義 JwtAccessTokenConverter
,顧名思義,用它來轉換成 JWT,可以指定簽署金鑰,這邊採用對稱金鑰,並且在 AuthorizationServerEndpointsConfigurer
中指定 accessTokenConverter
:
package cc.openhome;
...略
@SpringBootApplication
@EnableAuthorizationServer
public class AuthSvrApplication {
...略
@Bean
public AuthorizationServerConfigurer authorizationServerConfigurer() {
return new AuthorizationServerConfigurerAdapter() {
...略
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.accessTokenConverter(accessTokenConverter())
.authenticationManager(webSecurityConfigurerAdapter.authenticationManagerBean())
.userDetailsService(webSecurityConfigurerAdapter.userDetailsServiceBean());
}
};
}
...略
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("CATERPILLAR_KEY");
return converter;
}
}
這麼一來,授權伺服器核發的 Token 資訊會長這個樣子:
Access Token、Refresh Token 等,包含了 BASE64 編碼後的三個資訊,以「.
」區隔開來,例如上圖中 access_token
的 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
,是 JWT 標頭(Header)資訊經由 BASE64 編碼後的結果:
{
"typ": "JWT",
"alg": "HS256"
}
這部份表明了類型以及簽署演算方式,因為都是 HS256,因此 refresh_token
中開頭看到的也是 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
。
access_token
中「.
」隔開的第二部份是eyJhdWQiOlsicmVzb3VyY2UiXSwidXNlcl9uYW1lIjoiY2F0ZXJwaWxsYXIiLCJzY29wZSI6WyJhY2NvdW50IiwibWVzc2FnZSIsImVtYWlsIl0sImV4cCI6MTU0ODA3ODA4OSwiYXV0aG9yaXRpZXMiOlsiUk9MRV9NRU1CRVIiXSwianRpIjoiMzMwYzY4MGEtNTU4Ny00MTlmLWI4MTEtZDAxOGM4NTFiNWUxIiwiY2xpZW50X2lkIjoiYXV0aGNvZGVjbGllbnQifQ
是 JWT 中資訊承載(Payload)的部份 BASE64 編碼後的結果:
{
"aud": [
"resource"
],
"user_name": "caterpillar",
"scope": [
"account",
"message",
"email"
],
"exp": 1548040078,
"authorities": [
"ROLE_MEMBER"
],
"jti": "330c680a-5587-419f-b811-d018c851b5e1",
"client_id": "authcodeclient",
"iat": 1548036478
}
(jti
是 JWT 的識別 ID,每次核發都會不同,iat
是 JWT 核發的時間戳記。)
第三個部份 hx7bXNH043mAwww0X7oWaYR84QpCx28AQJ2ldmLVyj4
,則是將前兩個 BASE64 的結果,用「.
」連起來,再使用金鑰簽署,因此收到 access_token
的資源伺服器,可以將前兩個部份取出,以同樣金鑰進行簽署,然後與第三個部份比對,來確認有無被竄改。
有工具或網站可以協助將 JWT 中的資訊取出,例如 www.jsonwebtoken.io
:
因此你也應該瞭解到,JWT 預設只是對資訊承載部份施加簽署,並沒有加密,JWT 中資訊承載的部份只是單純的 BASE64 編碼,雖然 JWT 中資訊承載(Payload)的部份可以自定內容,切記不要放入敏感資訊。
配合以上的授權伺服器,資源伺服器也要能處理 JWT,因此加入相關設定,包括 build.gradle 中要加入:
implementation('org.springframework.security:spring-security-jwt')
以及設定金鑰、轉換器(包含 JwtTokenStore
中,TokenStore
是由來管理 Token 的核發、儲存、更新之用):
package cc.openhome;
...略
@SpringBootApplication
@EnableResourceServer
@RestController
public class ResSvrApplication {
...略
@Bean
public ResourceServerConfigurer resourceServerConfigurer() {
return new ResourceServerConfigurer() {
...略
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.tokenStore(tokenStore())
.resourceId("resource");
}
};
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("CATERPILLAR_KEY");
return converter;
}
}
因為 JWT 本身的承載部份,已經包含了身份、角色等資訊,資源伺服器不需要與授權伺服器確認了,application.properties 中的 check_token
等設定就可以移除了。
你可以在 JWT 中找到以上的範例專案。