Cannot convert access token to JSON
授權服務頒發token(未進行公私鑰加密)后,攜帶此token請求資源服務,提示此錯誤。
使用token可以在線解析,跟蹤代碼后問題出現JwtHelper
類decodeAndVerify方法,內容如下:
...
public static Jwt decodeAndVerify(String token, SignatureVerifier verifier) {
Jwt jwt = decode(token);
jwt.verifySignature(verifier);
return jwt;
}
...
其中jwt.verifySignature(verifier)中verifier為空,在下一層JwtImpl
類中,方法如下:
...
public void verifySignature(SignatureVerifier verifier) {
verifier.verify(this.signingInput(), this.crypto);
}
...
所以在資源服務中,必須對JwtAccessTokenConverter
初始化setVerifier。
...
/**
* Description: 為簽名驗證和解析提供轉換器<br>
* Details: 看起來 {@link org.springframework.security.jwt.crypto.sign.RsaVerifier} 已經被標記為過時了, 究其原因, 似乎 Spring 已經發布了一個新的產品 Spring Authorization Server, 有空再研究.
*
* @see <a href="https://github.com/spring-projects/spring-security/wiki/OAuth-2.0-Migration-Guide">OAuth 2.0 Migration Guide</a>
* @see <a href="https://spring.io/blog/2020/04/15/announcing-the-spring-authorization-server">Announcing the Spring Authorization Server</a>
* @see JwtAccessTokenConverter
*/
@SuppressWarnings("deprecation")
private JwtAccessTokenConverter jwtAccessTokenConverter() {
final JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setVerifier(new org.springframework.security.jwt.crypto.sign.RsaVerifier(retrievePublicKey()));
return jwtAccessTokenConverter;
}
/**
* Description: 獲取公鑰 (Verifier Key)<br>
* Details: 啟動時調用
*
* @return java.lang.String
* @author LiKe
* @date 2020-07-22 11:45:40
*/
private String retrievePublicKey() {
final ClassPathResource classPathResource = new ClassPathResource(AUTHORIZATION_SERVER_PUBLIC_KEY_FILENAME);
try (
// ~ 先從本地取讀取名為 authorization-server.pub 的公鑰文件, 獲取公鑰
final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(classPathResource.getInputStream()))
) {
log.debug("{} :: 從本地獲取公鑰 ...", RESOURCE_ID);
return bufferedReader.lines().collect(Collectors.joining("\n"));
} catch (IOException e) {
// ~ 如果本地沒有, 則嘗試通過授權服務器的 /oauth/token_key 端點獲取公鑰
log.debug("{} :: 從本地獲取公鑰失敗: {}, 嘗試從授權服務器 /oauth/token_key 端點獲取 ...", RESOURCE_ID, e.getMessage());
final RestTemplate restTemplate = new RestTemplate();
final String responseValue = restTemplate.getForObject(AUTHORIZATION_SERVER_TOKEN_KEY_ENDPOINT_URL, String.class);
log.debug("{} :: 授權服務器返回原始公鑰信息: {}", RESOURCE_ID, responseValue);
return JSON.parseObject(JSON.parseObject(responseValue).getString("data")).getString("value");
}
}
...
最終定位出現問題是因為沒有正確初始化signingKey
與verifierKey
。
先說說 verifierKey 和 verifier, 默認值是一個 6 位的隨機字符串 (new RandomValueStringGenerator().generate()), 和它 “配套” 的 verifier 是持有這個 verifierKey 的 MacSigner (用 HMACSHA256 算法驗證 Signature) / RsaVerifier (用 RSA 公鑰驗證 Signature), 在 JwtAccessTokenConverter 的 decode 方法中作為簽名校驗器被傳入 JwtHelper 的 decodeAndVerify(@NotNull String token, org.springframework.security.jwt.crypto.sign.SignatureVerifier verifier)
下面例子為使用原始MacSigner HMACSHA256 算法
例子:
...
private JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("***");
try {
converter.afterPropertiesSet();
} catch (Exception e) {
e.printStackTrace();
}
return converter;
}
...
最后問題完美解決。
而在 JwtHelper 的目標方法中, 首先把 token 的三個部分 (以 . 分隔的) 拆分出來, Base64.urlDecode 解碼. 再用我們傳入的 verifier 將 “Header.Payload” 編碼 (如果是 RSA, 就是公鑰.) 並與拆分出來的 Signature 部分比對 (Reference: org.springframework.security.jwt.crypto.sign.RsaVerifier#verify).
對應的, signer 和 signingKey 作為簽名 “組件” 存在, (可以看到在默認情況下, JwtAccessTokenConverter 對 JWT 的 Signature 采用的是對稱加密, signingKey 和 verifierKey 一致) 在 JwtHelper 的 encode(@NotNull CharSequence content, @NotNull org.springframework.security.jwt.crypto.sign.Signer signer) 方法中, 被用於將 “Header.Payload” 加密 (如果是 RSA, 就是私鑰) (Reference: org.springframework.security.jwt.crypto.sign.RsaSigner#sign).
所以算法本質上不是對 JWT 整體進行加解密, 而是對其中的 Signature 部分
參考
Java JwtAccessTokenConverter.setVerifierKey方法代碼示例 - 純淨天空 (vimsky.com)