1. 概述
在本快速教程中,我們將演示如何撤銷由 OAuth 授權服務器 實現的令牌。
當用户註銷時,其令牌不會立即從令牌存儲中刪除,而是會等待其自行過期。
因此,撤銷令牌意味着從令牌存儲中刪除該令牌。我們將涵蓋框架中的標準令牌實現,而不是 JWT 令牌。
注意:本文中使用的是 Spring OAuth 遺留項目。
2. TokenStore
首先,我們設置 TokenStore;我們將使用 JdbcTokenStore,以及隨之而來的數據源:@Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource());
}
@Bean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName(env.getProperty("jdbc.driverClassName"));
dataSource.setUrl(env.getProperty("jdbc.url"));
dataSource.setUsername(env.getProperty("jdbc.user"));
dataSource.setPassword(env.getProperty("jdbc.pass"));
return dataSource;
}
3. 默認 Token 服務 Bean
處理所有 Token 的類是 DefaultTokenServices – 並且必須在我們的配置中定義為 Bean:@Bean
@Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
return defaultTokenServices;
}
4. 顯示令牌列表
為了管理目的,我們還將設置一種查看當前有效令牌的方法。我們將訪問TokenStore在控制器中,並檢索指定客户端 ID 的存儲令牌:
@Resource(name="tokenStore")
TokenStore tokenStore;
@RequestMapping(method = RequestMethod.GET, value = "/tokens")
@ResponseBody
public List<String> getTokens() {
List<String> tokenValues = new ArrayList<String>();
Collection<OAuth2AccessToken> tokens = tokenStore.findTokensByClientId("sampleClientId");
if (tokens!=null){
for (OAuth2AccessToken token:tokens){
tokenValues.add(token.getValue());
}
}
return tokenValues;
}
5. 取消訪問令牌
為了無效化令牌,我們將使用 ConsumerTokenServices 接口中的 revokeToken() API:@Resource(name="tokenServices")
ConsumerTokenServices tokenServices;
@RequestMapping(method = RequestMethod.POST, value = "/tokens/revoke/{tokenId:.*}")
@ResponseBody
public String revokeToken(@PathVariable String tokenId) {
tokenServices.revokeToken(tokenId);
return tokenId;}
當然,這是一個非常敏感的操作,因此我們應該只在內部使用它,或者採取適當的安全措施來暴露它。
6. 前端
對於我們示例的前端,我們將顯示有效令牌的列表、當前由已登錄用户使用的令牌,以及用户可以輸入要撤銷的令牌的字段:
$scope.revokeToken =
$resource("http://localhost:8082/spring-security-oauth-resource/tokens/revoke/:tokenId",
{tokenId:'@tokenId'});
$scope.tokens = $resource("http://localhost:8082/spring-security-oauth-resource/tokens");
$scope.getTokens = function(){
$scope.tokenList = $scope.tokens.query();
}
$scope.revokeAccessToken = function(){
if ($scope.tokenToRevoke && $scope.tokenToRevoke.length !=0){
$scope.revokeToken.save({tokenId:$scope.tokenToRevoke});
$rootScope.message="Token:"+$scope.tokenToRevoke+" was revoked!";
$scope.tokenToRevoke="";
}
}
如果用户嘗試使用已撤銷的令牌,他們將收到“無效令牌”錯誤,狀態碼為401。
7. 取消刷新令牌
刷新令牌可用於獲取新的訪問令牌。每當訪問令牌被撤銷時,與它一起收到的刷新令牌將被失效。
如果我們要使刷新令牌本身也失效,可以使用 removeRefreshToken() 方法 JdbcTokenStore 類,這將從存儲中刪除刷新令牌:
@RequestMapping(method = RequestMethod.POST, value = "/tokens/revokeRefreshToken/{tokenId:.*}")
@ResponseBody
public String revokeRefreshToken(@PathVariable String tokenId) {
if (tokenStore instanceof JdbcTokenStore){
((JdbcTokenStore) tokenStore).removeRefreshToken(tokenId);
}
return tokenId;
}
為了測試刷新令牌在被撤銷後不再有效,我們將編寫以下測試,其中我們獲取一個訪問令牌,刷新它,然後撤銷刷新令牌,並再次嘗試刷新它。
我們將看到在撤銷後,我們會收到錯誤響應:“invalid refresh token”:
public class TokenRevocationLiveTest {
private String refreshToken;
private String obtainAccessToken(String clientId, String username, String password) {
Map<String, String> params = new HashMap<String, String>();
params.put("grant_type", "password");
params.put("client_id", clientId);
params.put("username", username);
params.put("password", password);
Response response = RestAssured.given().auth().
preemptive().basic(clientId,"secret").and().with().params(params).
when().post("http://localhost:8081/spring-security-oauth-server/oauth/token");
refreshToken = response.jsonPath().getString("refresh_token");
return response.jsonPath().getString("access_token");
}
private String obtainRefreshToken(String clientId) {
Map<String, String> params = new HashMap<String, String>();
params.put("grant_type", "refresh_token");
params.put("client_id", clientId);
params.put("refresh_token", refreshToken);
Response response = RestAssured.given().auth()
.preemptive().basic(clientId,"secret").and().with().params(params)
.when().post("http://localhost:8081/spring-security-oauth-server/oauth/token");
return response.jsonPath().getString("access_token");
}
private void authorizeClient(String clientId) {
Map<String, String> params = new HashMap<String, String>();
params.put("response_type", "code");
params.put("client_id", clientId);
params.put("scope", "read,write");
Response response = RestAssured.given().auth().preemptive()
.basic(clientId,"secret").and().with().params(params).
when().post("http://localhost:8081/spring-security-oauth-server/oauth/authorize");
}
@Test
public void givenUser_whenRevokeRefreshToken_thenRefreshTokenInvalidError() {
String accessToken1 = obtainAccessToken("fooClientIdPassword", "john", "123");
String accessToken2 = obtainAccessToken("fooClientIdPassword", "tom", "111");
authorizeClient("fooClientIdPassword");
String accessToken3 = obtainRefreshToken("fooClientIdPassword");
authorizeClient("fooClientIdPassword");
Response refreshTokenResponse = RestAssured.given().
header("Authorization", "Bearer " + accessToken3)
.get("http://localhost:8082/spring-security-oauth-resource/tokens");
assertEquals(200, refreshTokenResponse.getStatusCode());
Response revokeRefreshTokenResponse = RestAssured.given()
.header("Authorization", "Bearer " + accessToken1)
.post("http://localhost:8082/spring-security-oauth-resource/tokens/revokeRefreshToken/"+refreshToken);
assertEquals(200, revokeRefreshTokenResponse.getStatusCode());
String accessToken4 = obtainRefreshToken("fooClientIdPassword");
authorizeClient("fooClientIdPassword");
Response refreshTokenResponse2 = RestAssured.given()
.header("Authorization", "Bearer " + accessToken4)
.get("http://localhost:8082/spring-security-oauth-resource/tokens");
assertEquals(401, refreshTokenResponse2.getStatusCode());
}
}
8. 結論
在本教程中,我們演示瞭如何撤銷 OAuth 訪問令牌和 OAuth 刷新令牌。