Перейти к основному содержимому

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

· 4 мин. чтения

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-компонент в нашей конфигурации:

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

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

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