Spring Security OAuth2 - простой отзыв токена

Spring Security OAuth2 - простой отзыв токена

1. обзор

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

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

Таким образом, отзыв токена будет означать удаление этого токена из хранилища токенов.

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

2. 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. БинDefaultTokenServices

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

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

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

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

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

@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. Отзыв токена доступа

Чтобы сделать токен недействительным, мы воспользуемся APIrevokeToken() из интерфейса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.

Реализацию этого руководства можно найти вthe GitHub project - это проект на основе Maven, поэтому его должно быть легко импортировать и запускать как есть.