1. 概述
在这个快速教程中,我们将展示如何使用基于Spring Security的OAuth授权服务器来撤销用户的令牌。
当用户登出时,他们的令牌并不会立即从令牌存储中移除,而是在其自然过期前仍有效。
因此,撤销令牌意味着从令牌存储中删除该令牌。我们将关注框架中的标准令牌实现,而不是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. 默认TokenServices Bean
处理所有令牌的类是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. 撤销刷新令牌
刷新令牌可用于获取新的访问令牌。每当访问令牌被撤销时,与之一起接收到的刷新令牌也将被标记为无效。
如果我们还想要撤销刷新令牌本身,我们可以使用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;
}
为了测试撤销后刷新令牌是否仍然有效,我们将编写以下测试,首先获取访问令牌,然后刷新它,接着删除刷新令牌,再尝试刷新它。
我们会发现,在撤销后,我们将收到响应错误:“无效刷新令牌”:
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项目中找到。