使用 JSON Web Tokens


在〈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 資訊會長這個樣子:

使用 JSON Web Tokens

Access Token、Refresh Token 等,包含了 BASE64 編碼後的三個資訊,以「.」區隔開來,例如上圖中 access_tokeneyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9,是 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

使用 JSON Web Tokens

因此你也應該瞭解到,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 中找到以上的範例專案。