获取token(/oauth/token)

本文详细解析了Spring Security OAuth2中TokenEndpoint类的工作原理,包括如何处理POST请求生成OAuth2AccessToken,验证客户端信息,处理不同的授权类型,以及刷新令牌和授权码请求的特殊处理。同时介绍了AbstractTokenGranter和WeafTokenServices类在生成访问令牌过程中的作用。

1、入口方法(spring-security-oauth2-2.0.14.RELEASE-sources.jar)

@FrameworkEndpoint
public class TokenEndpoint extends AbstractEndpoint {

    @RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
    public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException{
        if (!(principal instanceof Authentication)) {
            throw new InsufficientAuthenticationException("There is no client authentication. Try adding an appropriate authentication filter.");
        }

        String clientId = getClientId(principal);
        ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);

        TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);

        if (clientId != null && !clientId.equals("")) {
            // Only validate the client details if a client authenticated during this
            // request.
            if (!clientId.equals(tokenRequest.getClientId())) {
                // double check to make sure that the client ID in the token request is the same as that in the
                // authenticated client
                throw new InvalidClientException("Given client ID does not match authenticated client");
            }
        }
        if (authenticatedClient != null) {
            oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
        }
        if (!StringUtils.hasText(tokenRequest.getGrantType())) {
            throw new InvalidRequestException("Missing grant type");
        }
        if (tokenRequest.getGrantType().equals("implicit")) {
            throw new InvalidGrantException("Implicit grant type not supported from token endpoint");
        }

        if (isAuthCodeRequest(parameters)) {
            // The scope was requested or determined during the authorization step
            if (!tokenRequest.getScope().isEmpty()) {
                logger.debug("Clearing scope of incoming token request");
                tokenRequest.setScope(Collections.<String> emptySet());
            }
        }

        if (isRefreshTokenRequest(parameters)) {
            // A refresh token has its own default scopes, so we should ignore any added by the factory here.
            tokenRequest.setScope(OAuth2Utils.parseParameterList(parameters.get(OAuth2Utils.SCOPE)));
        }

        OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
        if (token == null) {
            throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
        }

        return getResponse(token);
    }

    /**
	 * @param principal the currently authentication principal
	 * @return a client id if there is one in the principal
	 */
    protected String getClientId(Principal principal) {
        Authentication client = (Authentication) principal;
        if (!client.isAuthenticated()) {
            throw new InsufficientAuthenticationException("The client is not authenticated.");
        }
        String clientId = client.getName();
        if (client instanceof OAuth2Authentication) {
            // Might be a client and user combined authentication
            clientId = ((OAuth2Authentication) client).getOAuth2Request().getClientId();
        }
        return clientId;
    }

}

public interface Principal{...}

public interface Authentication extends Principal, Serializable {...}

public abstract class AbstractAuthenticationToken implements Authentication,
		CredentialsContainer {
    public String getName() {
        if (this.getPrincipal() instanceof UserDetails) {
            return ((UserDetails) this.getPrincipal()).getUsername();
        }

        if (getPrincipal() instanceof Principal) {
            return ((Principal) getPrincipal()).getName();
        }

        return (this.getPrincipal() == null) ? "" : this.getPrincipal().toString();
    }
}

 

2、

public abstract class AbstractTokenGranter implements TokenGranter {
	public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {

        if (!this.grantType.equals(grantType)) {
            return null;
        }
		
        String clientId = tokenRequest.getClientId();
        ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
        validateGrantType(grantType, client);
		
        logger.debug("Getting access token for: " + clientId);

        return getAccessToken(client, tokenRequest);

	}

    protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
        return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
    }
}

3、

public interface AuthorizationServerTokenServices {
    OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException;
}
public class DefaultTokenServices implements AuthorizationServerTokenServices, ResourceServerTokenServices, ConsumerTokenServices, InitializingBean {

}

4、

public class WeafTokenServices extends DefaultTokenServices {
    @Transactional
    public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
        OAuth2AccessToken existingAccessToken = this.tokenStore.getAccessToken(authentication);
        OAuth2RefreshToken refreshToken = null;
        if(existingAccessToken != null) {
            String clientId = authentication.getOAuth2Request().getClientId();
            Boolean kickUser = null;
            for (WeafSecurityProperties.SecurityClient securityClient : weafSecurityProperties.getClients()) {
                if (securityClient.getClientId().equals(clientId)) {
                    kickUser = securityClient.getKickUser();
                    break;
                }
            }
            //当前客户端支持踢人模式,则直接将该人员的远token全部删除
            if (kickUser) {
                if(existingAccessToken.getRefreshToken() != null) {
                    refreshToken = existingAccessToken.getRefreshToken();
                    this.tokenStore.removeRefreshToken(refreshToken);
                }
                this.tokenStore.removeAccessToken(existingAccessToken);
            } else {
                //该客户端不是踢人模式,所以如果原token存在,则获取原token,如果远token过期则重新生成.
                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;
                }
            }
        }

        if(refreshToken == null) {
            refreshToken = this.createRefreshToken(authentication);
        } else if(refreshToken instanceof ExpiringOAuth2RefreshToken) {
            ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken)refreshToken;
            if(System.currentTimeMillis() > expiring.getExpiration().getTime()) {
                refreshToken = this.createRefreshToken(authentication);
            }
        }
        OAuth2AccessToken accessToken = this.createAccessToken(authentication, refreshToken);
        this.tokenStore.storeAccessToken(accessToken, authentication);
        refreshToken = accessToken.getRefreshToken();
        if(refreshToken != null) {
            this.tokenStore.storeRefreshToken(refreshToken, authentication);
        }

        return accessToken;
    }

    private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
        DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());
        int validitySeconds = this.getAccessTokenValiditySeconds(authentication.getOAuth2Request());
        if(validitySeconds > 0) {
            token.setExpiration(new Date(System.currentTimeMillis() + (long)validitySeconds * 1000L));
        }

        token.setRefreshToken(refreshToken);
        token.setScope(authentication.getOAuth2Request().getScope());
        if (token.getAdditionalInformation() == null) {
            Map<String, Object> additionInformation = new HashMap<>();
            additionInformation.put(WeafConstants.Security.CLIENT_ID, authentication.getOAuth2Request().getClientId());
            token.setAdditionalInformation(Collections.unmodifiableMap(additionInformation));
        } else {
            Map<String, Object> tempMap = new HashMap<>();
            tempMap.putAll(token.getAdditionalInformation());
            tempMap.put(ACCESS_TOKEN_ADDITION_CLIENT_ID, authentication.getOAuth2Request().getClientId());
            token.setAdditionalInformation(Collections.unmodifiableMap(tempMap));
        }
        return (OAuth2AccessToken)(this.accessTokenEnhancer != null?this.accessTokenEnhancer.enhance(token, authentication):token);
    }

    private OAuth2RefreshToken createRefreshToken(OAuth2Authentication authentication) {
        if(!this.isSupportRefreshToken(authentication.getOAuth2Request())) {
            return null;
        } else {
            int validitySeconds = this.getRefreshTokenValiditySeconds(authentication.getOAuth2Request());
            String value = UUID.randomUUID().toString();
            return (OAuth2RefreshToken)(validitySeconds > 0?new DefaultExpiringOAuth2RefreshToken(value, new Date(System.currentTimeMillis() + (long)validitySeconds * 1000L)):new DefaultOAuth2RefreshToken(value));
        }
    }

}

 

 

<think>我们比较抖音开放平台中两个获取访问令牌的接口: 1. client_credentials方式:`/oauth/client_token/`(客户端模式) 2. authorization_code方式:`/oauth/access_token/`(授权码模式) 根据引用内容[^1][^2][^4][^5]和抖音开放平台文档,分析如下: ### 1. 接口用途对比 | 特性 | `/oauth/client_token/` (客户端模式) | `/oauth/access_token/` (授权码模式) | |---------------------|---------------------------------------------|---------------------------------------------| | **适用场景** | 服务端调用,无需用户参与 | 需要用户授权登录 | | **令牌类型** | 应用级access_token(代表应用) | 用户级access_token(代表用户授权) | | **是否需要用户登录** | 否 | 是 | | **获取方式** | 用`client_key`和`client_secret`直接换取 | 用临时授权码`code`换取(需前端跳转授权) | | **令牌权限** | 只能调用部分应用级接口(如创建直播任务[^2]) | 可调用用户相关接口(如获取用户信息[^4]) | | **时效性** | 2小时(与用户级相同) | 2小时(需用refresh_token刷新) | ### 2. 代码实现对比 #### (1) 客户端模式(client_credentials)[^5] ```php function getClientToken($client_key, $client_secret) { $url = "https://open.douyin.com/oauth/client_token/"; $params = [ 'client_key' => $client_key, 'client_secret' => $client_secret, 'grant_type' => 'client_credentials' ]; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url . '?' . http_build_query($params)); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); $data = json_decode($response, true); if (isset($data['data']['access_token'])) { return $data['data']['access_token']; // 应用级token } throw new Exception("获取失败: " . ($data['message'] ?? '未知错误')); } ``` #### (2) 授权码模式(authorization_code)[^1][^4] ```php function getUserToken($client_key, $client_secret, $code, $redirect_uri) { $url = "https://open.douyin.com/oauth/access_token/"; $postData = [ 'client_key' => $client_key, 'client_secret' => $client_secret, 'code' => $code, 'grant_type' => 'authorization_code', 'redirect_uri' => $redirect_uri ]; // ...(同上文获取用户级token的代码) // 返回包含access_token和refresh_token的数组 } ``` ### 3. 适用场景总结 | **选择标准** | 推荐方式 | |----------------------------------------|------------------------------| | 需要操作用户数据(如发视频、读粉丝) | 授权码模式(用户级token)[^4] | | 仅需调用应用级接口(如管理直播任务[^2]) | 客户端模式(应用级token)[^5] | | 开发后台定时任务 | 客户端模式(无需用户交互) | | 开发用户登录功能 | 授权码模式(需跳转授权) | ### 4. 安全注意事项 - **客户端模式**:`client_secret`必须严格保密(仅限服务器使用)[^5] - **授权码模式**:`redirect_uri`需使用HTTPS且完全匹配,防止劫持[^4] - 两种令牌均需缓存并定期刷新(用户级用refresh_token,应用级定时重新获取) > ✅ **结论**:没有绝对“更好”,根据业务需求选择: > - 用户相关功能 → 授权码模式 > - 应用管理功能 → 客户端模式 §§相关问题§§ 1. 抖音开放平台中应用级access_token能调用哪些具体接口? 2. 如何用refresh_token刷新用户级access_token? 3. 抖音用户授权页面的scope参数应该如何选择?</think>### 抖音API访问令牌获取方法对比分析 抖音开放平台提供两种获取访问令牌的接口,主要区别如下: | 特性 | `/oauth/client_token/` (客户端模式) | `/oauth/access_token/` (授权码模式) | |--------------------|---------------------------------------------|---------------------------------------------| | **认证方式** | 应用级认证 (`client_credentials`) | 用户级认证 (`authorization_code`) | | **令牌类型** | 应用级access_token (代表应用) | 用户级access_token (代表用户授权) | | **是否需要用户** | 否 | 是 (需用户授权) | | **获取参数** | client_key + client_secret [^5] | client_key + client_secret + code [^1] | | **适用场景** | 服务端定时任务、直播管理[^2] | 用户登录、获取用户数据[^4] | | **令牌权限** | 仅限应用级接口 (如创建直播[^2]) | 可访问用户数据接口 (如user_info) | | **时效性** | 2小时 (需定时刷新) | 2小时 (需用refresh_token刷新) | | **安全要求** | 需严格保护client_secret | 需验证redirect_uri和state参数 | ### 选择建议 #### ✅ 使用 `/oauth/client_token/` 当: ```php // 应用级令牌获取示例 function getClientToken($client_key, $client_secret) { $url = "https://open.douyin.com/oauth/client_token/"; $params = [ 'client_key' => $client_key, 'client_secret' => $client_secret, 'grant_type' => 'client_credentials' ]; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url . '?' . http_build_query($params)); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); $data = json_decode($response, true); if (isset($data['data']['access_token'])) { return $data['data']['access_token']; // 应用级token } throw new Exception("获取失败: " . ($data['message'] ?? '未知错误')); } ``` 适用场景: 1. 自动化后台任务(如定时发布视频) 2. 直播管理接口调用[^2] 3. 不需要用户授权的服务端操作 #### ✅ 使用 `/oauth/access_token/` 当: ```php // 用户级令牌获取示例 (需前端授权流程) function getUserToken($client_key, $client_secret, $code, $redirect_uri) { $url = "https://open.douyin.com/oauth/access_token/"; $postData = [ 'client_key' => $client_key, 'client_secret' => $client_secret, 'code' => $code, 'grant_type' => 'authorization_code', 'redirect_uri' => $redirect_uri ]; // ... (同原始回答中的实现) // 返回包含access_token和refresh_token的数组 } ``` 适用场景: 1. 用户登录功能实现[^4] 2. 获取用户个人数据(如粉丝列表) 3. 操作用户资源(如发布视频) ### 关键区别总结 1. **权限范围不同** - 客户端模式:只能访问**应用级资源**(如直播管理) - 授权码模式:可访问**用户级资源**(如用户视频) 2. **安全机制不同** - 客户端模式依赖`client_secret`保护[^5] - 授权码模式需验证`redirect_uri`和`state`参数[^4] 3. **用户体验不同** - 客户端模式无需用户交互 - 授权码模式需用户授权同意 > 📌 **最佳实践**:根据业务需求组合使用两种模式。例如: > - 用`client_token`管理直播[^2] > - 用`access_token`获取用户信息[^1] > - 使用JWT管理令牌生命周期[^3]
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值