【Spring Security】 DefaultTokenServices

Metadata

title: 【Spring Security】 DefaultTokenServices
date: 2023-02-05 15:44
tags:
  - 行动阶段/完成
  - 主题场景/组件
  - 笔记空间/KnowladgeSpace/ProgramSpace/ModuleSpace
  - 细化主题/Module/SpringSecurity
categories:
  - SpringSecurity
keywords:
  - SpringSecurity
description: 【Spring Security】 DefaultTokenServices

【Spring Security】 DefaultTokenServices

DefaultTokenServices 是默认的令牌服务类,其实现了以下四个接口:

public class DefaultTokenServices implements AuthorizationServerTokenServices, ResourceServerTokenServices,
        ConsumerTokenServices, InitializingBean

这些接口的主要作用如下:

  • AuthorizationServerTokenServices:授权服务器的令牌服务类
  • ResourceServerTokenServices:资源服务器的令牌服务类
  • ConsumerTokenServices:注销令牌服务类
  • InitializingBean :Bean 后置处理

该类的属性说明如下:

    // 刷新令牌过期时间 默认30天
    private int refreshTokenValiditySeconds = 60 * 60 * 24 * 30; // default 30 days.
    // 访问令牌过期时间 默认12个小时
    private int accessTokenValiditySeconds = 60 * 60 * 12; // default 12 hours.
    // 是否支持刷新令牌
    private boolean supportRefreshToken = false;
    // 是否重用刷新令牌
    private boolean reuseRefreshToken = true;
    // 令牌存储
    private TokenStore tokenStore;
    // 客户端服务类
    private ClientDetailsService clientDetailsService;
    // 令牌增强
    private TokenEnhancer accessTokenEnhancer;
    // 认证管理器
    private AuthenticationManager authenticationManager;

1. 实现 InitializingBean 接口

实现 InitializingBean,主要是对令牌存储进行检查,必须设置相应的存储类。

public void afterPropertiesSet() throws Exception {
        Assert.notNull(tokenStore, "tokenStore must be set");
    }

2. 实现 AuthorizationServerTokenServices 接口

实现其 createAccessToken 方法,用于创建令牌对象:

    @Transactional
    public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
        // 1. 首先判断当前用户是否已存在访问令牌
        OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
        OAuth2RefreshToken refreshToken = null;
        // 2. 如果已存在,判断是否过期,已过期则直接删除,没过期则重新存储并返回之前的令牌。
        if (existingAccessToken != null) {
            if (existingAccessToken.isExpired()) {
                if (existingAccessToken.getRefreshToken() != null) {
                    refreshToken = existingAccessToken.getRefreshToken();
                    // The token store could remove the refresh token when the
                    // access token is removed, but we want to
                    // be sure...
                    tokenStore.removeRefreshToken(refreshToken);
                }
                tokenStore.removeAccessToken(existingAccessToken);
            }
            else {
                // Re-store the access token in case the authentication has changed
                tokenStore.storeAccessToken(existingAccessToken, authentication);
                return existingAccessToken;
            }
        }

        // 3. 没有刷新令牌则创建刷新令牌 
        if (refreshToken == null) {
            refreshToken = createRefreshToken(authentication);
        }
        // 4. 如果刷新令牌过期了,创建新的刷新令牌
        else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
            ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
            if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
                refreshToken = createRefreshToken(authentication);
            }
        }
        // 5 . 创建访问令牌对象
        OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
        tokenStore.storeAccessToken(accessToken, authentication);
        // 6. 有刷新令牌则存贮,并返回令牌对象
        refreshToken = accessToken.getRefreshToken();
        if (refreshToken != null) {
            tokenStore.storeRefreshToken(refreshToken, authentication);
        }
        return accessToken;
    }

实现其 refreshAccessToken 方法,用于刷新访问令牌:

    @Transactional(noRollbackFor={InvalidTokenException.class, InvalidGrantException.class})
    public OAuth2AccessToken refreshAccessToken(String refreshTokenValue, TokenRequest tokenRequest)
            throws AuthenticationException {
        // 1. 不支持刷新 当前令牌,则抛出异常
        if (!supportRefreshToken) {
            throw new InvalidGrantException("Invalid refresh token: " + refreshTokenValue);
        }
        // 2. 获取存储中的刷新令牌对象
        OAuth2RefreshToken refreshToken = tokenStore.readRefreshToken(refreshTokenValue);
        if (refreshToken == null) {
            throw new InvalidGrantException("Invalid refresh token: " + refreshTokenValue);
        }	
        // 3. 通过刷新令牌获取认证信息
        OAuth2Authentication authentication = tokenStore.readAuthenticationForRefreshToken(refreshToken);
        // 4. 如果设置了认证管理器,则会重新对进行认证
        if (this.authenticationManager != null && !authentication.isClientOnly()) {
            // The client has already been authenticated, but the user authentication might be old now, so give it a
            // chance to re-authenticate.
            Authentication user = new PreAuthenticatedAuthenticationToken(authentication.getUserAuthentication(), "", authentication.getAuthorities());
            user = authenticationManager.authenticate(user);
            Object details = authentication.getDetails();
            authentication = new OAuth2Authentication(authentication.getOAuth2Request(), user);
            authentication.setDetails(details);
        }
        String clientId = authentication.getOAuth2Request().getClientId();
        if (clientId == null || !clientId.equals(tokenRequest.getClientId())) {
            throw new InvalidGrantException("Wrong client for this refresh token: " + refreshTokenValue);
        }

        // clear out any access tokens already associated with the refresh
        // token.
        tokenStore.removeAccessTokenUsingRefreshToken(refreshToken);

        if (isExpired(refreshToken)) {
            tokenStore.removeRefreshToken(refreshToken);
            throw new InvalidTokenException("Invalid refresh token (expired): " + refreshToken);
        }

        authentication = createRefreshedAuthentication(authentication, tokenRequest);

        if (!reuseRefreshToken) {
            tokenStore.removeRefreshToken(refreshToken);
            refreshToken = createRefreshToken(authentication);
        }
        // 5. 创建新的访问令牌并返回,如果没设置重用,则会创建新的刷新令牌并返回
        OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
        tokenStore.storeAccessToken(accessToken, authentication);
        if (!reuseRefreshToken) {
            tokenStore.storeRefreshToken(accessToken.getRefreshToken(), authentication);
        }
        return accessToken;
    }

实现其 getAccessToken 方法,用于根据认证对象获取访问令牌:

public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) {
        return tokenStore.getAccessToken(authentication);
    }

3. 实现 ResourceServerTokenServices 接口

实现其 loadAuthentication 方法,用于获取认证对象:

    public OAuth2Authentication loadAuthentication(String accessTokenValue) throws AuthenticationException,
            InvalidTokenException {
        // 1. 获取访问令牌对象
        OAuth2AccessToken accessToken = tokenStore.readAccessToken(accessTokenValue);
        if (accessToken == null) {
            throw new InvalidTokenException("Invalid access token: " + accessTokenValue);
        }
        // 2. 过期则直接删除,并抛出异常
        else if (accessToken.isExpired()) {
            tokenStore.removeAccessToken(accessToken);
            throw new InvalidTokenException("Access token expired: " + accessTokenValue);
        }
        // 3. 获取认证信息
        OAuth2Authentication result = tokenStore.readAuthentication(accessToken);
        if (result == null) {
            // in case of race condition
            throw new InvalidTokenException("Invalid access token: " + accessTokenValue);
        }
        // 4. 重新查询客户端信息,没有改客户端信息,则抛出异常
        if (clientDetailsService != null) {
            String clientId = result.getOAuth2Request().getClientId();
            try {
                clientDetailsService.loadClientByClientId(clientId);
            }
            catch (ClientRegistrationException e) {
                throw new InvalidTokenException("Client not valid: " + clientId, e);
            }
        }
        return result;
    }

实现其 readAccessToken 方法,用于获取访问令牌对象:

public OAuth2AccessToken readAccessToken(String accessToken) {
        return tokenStore.readAccessToken(accessToken);
    }

4. 实现 ConsumerTokenServices 接口

实现其 revokeToken 方法,用于注销:

    public boolean revokeToken(String tokenValue) {
        // 获取访问令牌对象,不存在注销失败
        OAuth2AccessToken accessToken = tokenStore.readAccessToken(tokenValue);
        if (accessToken == null) {
            return false;
        }
        // 注销刷新令牌
        if (accessToken.getRefreshToken() != null) {
            tokenStore.removeRefreshToken(accessToken.getRefreshToken());
        }
        // 删除访问令牌对象
        tokenStore.removeAccessToken(accessToken);
        return true;
    }

应用案例

在了解了 DefaultTokenServices 的源码之后,我们就可是对令牌进行更近一步的自定义。

案例 1. 返回自定义 Token ID

源码分析

在申请令牌的时候,返回的令牌,其实只是一个令牌值(也可以理解为 ID ),还有其他令牌信息是存贮在持久化组件中的。

比如之前使用 Redis 存储,其信息展示如下:

首先我们看下令牌对象 DefaultOAuth2AccessToken 源码:

    // token值 
    private String value;
    // 什么时候过期
    private Date expiration;
    // 类型 默认Bearer 
    private String tokenType = BEARER_TYPE.toLowerCase();
    // 刷新令牌
    private OAuth2RefreshToken refreshToken;
    // 范围
    private Set<String> scope;
    // 自定义信息
    private Map<String, Object> additionalInformation = Collections.emptyMap();

在 DefaultTokenServices 中创建令牌对象的源码如下:

    private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
        // 1. 使用UUID工具类 生成value值
        DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());
        // 2. 获取客户端设置的过期时间
        int validitySeconds = getAccessTokenValiditySeconds(authentication.getOAuth2Request());
        if (validitySeconds > 0) {
            token.setExpiration(new Date(System.currentTimeMillis() + (validitySeconds * 1000L)));
        }
        // 3. 设置刷新令牌 、 范围
        token.setRefreshToken(refreshToken);
        token.setScope(authentication.getOAuth2Request().getScope());
        // 4. 令牌增强
        return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;
    }

在上面代码可以看到,使用的是 UUID 生成的随机字符串,那么我们修改这个代码,就可以使用自定义的生成方式了。

自定义令牌值实现方案

自定义 MyDefaultTokenServices,将 MyDefaultTokenServices 中的代码都复制过来,添加一个生成 value 值的方法,然后将 createRefreshToken、createAccessToken 使用此方法生成令牌值。

    private OAuth2RefreshToken createRefreshToken(OAuth2Authentication authentication) {
        if (!isSupportRefreshToken(authentication.getOAuth2Request())) {
            return null;
        }
        int validitySeconds = getRefreshTokenValiditySeconds(authentication.getOAuth2Request());
        // 自定义令牌值生成策略
        String value = createTokenValue(authentication);
        if (validitySeconds > 0) {
            return new DefaultExpiringOAuth2RefreshToken(value, new Date(System.currentTimeMillis()
                    + (validitySeconds * 1000L)));
        }
        return new DefaultOAuth2RefreshToken(value);
    }

    private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {

        DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(createTokenValue(authentication));
        int validitySeconds = getAccessTokenValiditySeconds(authentication.getOAuth2Request());
        if (validitySeconds > 0) {
            token.setExpiration(new Date(System.currentTimeMillis() + (validitySeconds * 1000L)));
        }
        token.setRefreshToken(refreshToken);
        token.setScope(authentication.getOAuth2Request().getScope());

        return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;
    }

    /**
     * 自定义令牌值生成策略
     */
    private String createTokenValue(OAuth2Authentication authentication) {
        String clientId = authentication.getOAuth2Request().getClientId();
        return UUID.randomUUID() + "_" + clientId;
    }

配置一个 MyDefaultTokenServices Bean 对象:

    @Bean
    @Primary
    public MyDefaultTokenServices defaultTokenServices(RedisConnectionFactory connectionFactory) {
        MyDefaultTokenServices resourceServerTokenServices = new MyDefaultTokenServices();
        resourceServerTokenServices.setTokenStore(tokenStore(connectionFactory));
        return resourceServerTokenServices;
    }

在授权服务器配置类中,端点添加自定义的 DefaultTokenServices:

    // 端点配置
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        // 省略其他。。。
        endpoints.pathMapping("/oauth/token","/custom/token");
        // 添加自定义Token Services
        endpoints.tokenServices(myDefaultTokenServices);
    }

测试:可以看到采用自定义的方式生成的 token 值