Spring Security OAuth2 – Simple Token Revocation (using the Spring Security OAuth legacy stack) – Spring Security OAuth2 – Simple Token Revocation (使用Spring Security OAuth遗留栈)

最后修改: 2016年 11月 23日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this quick tutorial, we’ll illustrate how we can revoke tokens granted by an OAuth Authorization Server implemented with Spring Security.

在这个快速教程中,我们将说明如何撤销由OAuth授权服务器实现的Spring Security授予的令牌。

When a user logs out, their token is not immediately removed from the token store; instead, it remains valid until it expires on its own.

当用户注销时,他们的令牌不会立即从令牌存储中删除;相反,它仍然有效,直到它自己过期。

And so, revocation of a token will mean removing that token from the token store. We’ll cover the standard token implementation in the framework, not JWT tokens.

因此,撤销一个令牌将意味着从令牌存储中删除该令牌。我们将涵盖框架中的标准令牌实现,而不是JWT令牌。

Note: this article is using the Spring OAuth legacy project.

注意:本文使用的是Spring OAuth遗留项目

2. The TokenStore

2.TokenStore

First, let’s set up the token store; we’ll use a JdbcTokenStore, along with the accompanying data source:

首先,我们来设置令牌存储;我们将使用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. The DefaultTokenServices Bean

3.DefaultTokenServices Bean

The class that handles all tokens is the DefaultTokenServices – and has to be defined as a bean in our configuration:

处理所有令牌的类是DefaultTokenServices – 必须在我们的配置中定义为一个bean。

@Bean
@Primary
public DefaultTokenServices tokenServices() {
    DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
    defaultTokenServices.setTokenStore(tokenStore());
    defaultTokenServices.setSupportRefreshToken(true);
    return defaultTokenServices;
}

4. Displaying the List of Tokens

4.显示令牌列表

For admin purposes, let’s also set up a way to view the currently valid tokens.

为了管理的目的,我们还设置了一个查看当前有效令牌的方法。

We’ll access the TokenStore in a controller, and retrieve the currently stored tokens for a specified client id:

我们将在一个控制器中访问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. Revoking an Access Token

5.撤销访问令牌

In order to invalidate a token, we’ll make use of the revokeToken() API from the ConsumerTokenServices interface:

为了使令牌失效,我们将利用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;
}

Of course this is a very sensitive operation so we should either only use it internally, or we should take great care to expose it with the proper security in place.

当然,这是一个非常敏感的操作,所以我们应该只在内部使用它,或者我们应该非常小心地在适当的安全情况下暴露它。

6. The Front-End

6.前端

For the front-end of our example, we’ll display the list of valid tokens, the token currently used by the logged in user making the revocation request, and a field where the user can enter the token they wish to revoke:

对于我们例子的前端,我们将显示有效令牌的列表、提出撤销请求的登录用户当前使用的令牌,以及一个用户可以输入他们希望撤销的令牌的字段。

$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="";
    }
}

If a user attempts to use a revoked token again, they will receive an ‘invalid token’ error with status code 401.

如果用户试图再次使用被撤销的令牌,他们将收到一个状态代码为401的 “无效令牌 “错误。

7. Revoking the Refresh Token

7.撤销刷新令牌

The refresh token can be used to obtain a new access token. Whenever an access token is revoked, the refresh token that was received with it is invalidated.

刷新令牌可用于获得新的访问令牌。每当一个访问令牌被撤销时,与它一起收到的刷新令牌就会失效。

If we want to invalidate the refresh token itself also, we can use the method removeRefreshToken() of class JdbcTokenStore, which will remove the refresh token from the store:

如果我们想让刷新令牌本身失效,我们可以使用JdbcTokenStore类的removeRefreshToken()方法,这将从存储中删除刷新令牌。

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

In order to test that the refresh token is no longer valid after being revoked, we’ll write the following test, in which we obtain an access token, refresh it, then remove the refresh token, and attempt to refresh it again.

为了测试刷新令牌在被撤销后不再有效,我们将编写以下测试,在测试中我们获得一个访问令牌,刷新它,然后删除刷新令牌,并尝试再次刷新它。

We will see that after revocation, we will receive the response error: “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. Conclusion

8.结论

In this tutorial, we have demonstrated how to revoke an OAuth access token and an Oauth refresh token.

在本教程中,我们已经演示了如何撤销OAuth访问令牌和Oauth刷新令牌。

The implementation of this tutorial can be found in the GitHub project.

本教程的实现可以在GitHub项目中找到。