Рубрики
Без рубрики

Spring Security OAuth2 – Простой отзыв токена (с использованием устаревшего стека Spring Security OAuth)

Быстрое практическое введение в отзыв токенов с помощью Spring Security OAuth2.

Автор оригинала: Loredana Crusoveanu.

1. Обзор

В этом кратком руководстве мы проиллюстрируем, как мы можем отозвать токены, предоставленные Сервер авторизации OAuth реализован с помощью Spring Security .

Когда пользователь выходит из системы, его токен не удаляется сразу из хранилища токенов; вместо этого он остается действительным до тех пор, пока срок его действия не истечет сам по себе.

Таким образом, отзыв токена будет означать удаление этого токена из хранилища токенов. Мы рассмотрим стандартную реализацию токенов в фреймворке, а не токены JWT.

Примечание : в этой статье используется устаревший проект Spring OAuth .

2. Хранилище Токенов

Во-первых, давайте настроим хранилище токенов; мы будем использовать 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. Компонент DefaultTokenServices

Класс, который обрабатывает все токены, является DefaultTokenServices – и должен быть определен как компонент в нашей конфигурации:

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

4. Отображение списка токенов

Для целей администрирования давайте также настроим способ просмотра текущих допустимых токенов.

Мы получим доступ к хранилищу токенов в контроллере и получим сохраненные в данный момент токены для указанного идентификатора клиента:

@Resource(name="tokenStore")
TokenStore tokenStore;

@RequestMapping(method = RequestMethod.GET, value = "/tokens")
@ResponseBody
public List getTokens() {
    List tokenValues = new ArrayList();
    Collection tokens = tokenStore.findTokensByClientId("sampleClientId"); 
    if (tokens!=null){
        for (OAuth2AccessToken token:tokens){
            tokenValues.add(token.getValue());
        }
    }
    return tokenValues;
}

5. Отзыв маркера доступа

Чтобы аннулировать токен, мы будем использовать revokeToken() API из интерфейса ConsumerTokenServices :

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

Чтобы проверить, что маркер обновления больше не действителен после отзыва, мы напишем следующий тест, в котором мы получим маркер доступа, обновим его, затем удалим маркер обновления и попытаемся обновить его снова.

Мы увидим, что после отзыва мы получим ошибку ответа: “недопустимый токен обновления”:

public class TokenRevocationLiveTest {
    private String refreshToken;

    private String obtainAccessToken(String clientId, String username, String password) {
        Map params = new HashMap();
        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 params = new HashMap();
        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 params = new HashMap();
        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.

Реализацию этого учебника можно найти в проекте GitHub .