Apereo CAS OAuth 2.0 Refresh Token 授权流程(Refresh Token Grant)完全解析
2026/9/24 15:32:05 网站建设 项目流程
  • 后端
  • 认证鉴权
  • 单点登录

【免费下载链接】cas

Apereo CAS - Identity & Single Sign On for all earthlings and beyond.

项目地址:https://gitcode.com/gh_mirrors/ca/cas
点击查看免费下载

本文围绕 Apereo CAS 官方文档中关于OAuth 2.0 Refresh Token 授权流程(OAuth-ProtocolFlow-RefreshToken.md)展开,系统讲解如何在 CAS 中通过/oauth2.0/accessToken端点,用已颁发的 refresh token 换取新的 access token,并结合本仓库源码剖析其请求校验、令牌生成、令牌轮换(Token Rotation)与过期策略的完整实现链路。读完本文,你将掌握 Refresh Token Grant 的端点调用方式、全部核心参数、CAS 中的配置项与服务级覆盖规则,以及这套流程在源码中的真实执行路径。

一、Refresh Token 授权流程概述

OAuth 2.0 的 refresh token grant 类型用于在先前颁发的 access token 过期之后,用对应的 refresh token 重新获取一个新的 access token。它避免了用户在 access token 失效后重新走一遍完整的登录授权流程,是长会话(long-lived session)类应用的常见手段。

在 CAS 的 OAuth/OpenID Connect 实现中,该流程的契约定义如下:

端点(Endpoint)请求参数(Parameters)响应(Response)
/oauth2.0/accessTokengrant_type=refresh_token&client_id=<ID>
&client_secret=SECRET&refresh_token=REFRESH_TOKEN
新的 access token

从请求参数可以看出,该流程需要同时提交四种参数:

  • grant_type:固定为refresh_token,用于告诉 CAS 本次请求使用的是刷新令牌授权类型;
  • client_id:客户端在 CAS 服务注册中心登记的标识符;
  • client_secret:客户端的密钥,用于客户端认证;
  • refresh_token:先前由 CAS 颁发并保存下来的刷新令牌本体。

这些参数名在源码中均有对应的常量定义,见 OAuth20Constants.java:

String BASE_OAUTH20_URL = "/oauth2.0"; String GRANT_TYPE = "grant_type"; String CLIENT_ID = "client_id"; String CLIENT_SECRET = "client_secret"; String REFRESH_TOKEN = "refresh_token"; String ACCESS_TOKEN_URL = "accessToken"; String TOKEN_URL = "token";

注意BASE_OAUTH20_URLACCESS_TOKEN_URL拼接后正好构成文档表格中的/oauth2.0/accessToken端点地址。

二、端点与请求处理入口

Refresh Token Grant 的请求统一由Access Token 端点控制器OAuth20AccessTokenEndpointController接收处理,其声明位于 OAuth20AccessTokenEndpointController.java:

@PostMapping(path = { OAuth20Constants.BASE_OAUTH20_URL + '/' + OAuth20Constants.ACCESS_TOKEN_URL, OAuth20Constants.BASE_OAUTH20_URL + '/' + OAuth20Constants.TOKEN_URL}, produces = MediaType.APPLICATION_JSON_VALUE) public ModelAndView handleRequest(final HttpServletRequest request, final HttpServletResponse response) throws Exception { val context = new JEEContext(request, response); if (!verifyAccessTokenRequest(context)) { LOGGER.warn("Access token validation failed for request [{}]", context.getFullRequestURL()); return OAuth20Utils.writeError(response, OAuth20Constants.INVALID_GRANT); } val tokenRequestContext = examineAndExtractAccessTokenGrantRequest(request, response); logProtocolRequest(tokenRequestContext); val generatedTokenResult = getConfigurationContext().getAccessTokenGenerator().generate(tokenRequestContext); return generateAccessTokenResponse(tokenRequestContext, generatedTokenResult); }

该控制器同时映射了/oauth2.0/accessToken/oauth2.0/token两个路径,并且同时支持@PostMapping@GetMapping,即 POST 与 GET 两种 HTTP 方法均可发起令牌请求。整体处理链路可分为四步:

  1. verifyAccessTokenRequest(...):交给注册的 grant type 校验器链,验证请求合法性;
  2. examineAndExtractAccessTokenGrantRequest(...):通过可审计的提取器(extractor)解析出AccessTokenRequestContext
  3. getAccessTokenGenerator().generate(...):由令牌生成器实际产出新的 access token(及可选的 refresh token);
  4. generateAccessTokenResponse(...):将结果编码为 JSON 响应返回给客户端。

其中校验失败时,CAS 会直接返回invalid_grant错误码,这正是 refresh token 无效、过期或不属于该客户端时的典型错误响应。

三、Refresh Token 请求的校验逻辑

当 CAS 收到grant_type=refresh_token的请求后,负责校验的是OAuth20RefreshTokenGrantTypeTokenRequestValidator,其核心逻辑见 OAuth20RefreshTokenGrantTypeTokenRequestValidator.java,校验过程按以下顺序执行:

  1. 参数完整性检查:解析请求中的refresh_token参数与客户端标识,若 refresh token 缺失或client_id为空,直接返回校验失败;
  2. 刷新令牌存活性检查:通过ticketRegistry.getTicket(token, OAuth20RefreshToken.class)从票据注册中心(Ticket Registry)中查找对应的 refresh token 票据,若抛出InvalidTicketException(即令牌不存在或已过期),则校验失败并记录告警日志;
  3. 服务访问策略检查:依据client_id定位注册服务(OAuthRegisteredService),并执行RegisteredServiceAccessStrategyEnforcer的服务访问策略审计,未授权则抛出异常;
  4. 授权类型检查:调用isGrantTypeSupportedBy(registeredService, grantType),确认该服务定义中明确允许refresh_token授权类型,否则拒绝请求;
  5. 令牌归属检查:比对 refresh token 绑定的clientId与本次请求的client_id,二者不匹配时拒绝请求并记录警告日志。

从源码可见,CAS 对 Refresh Token Grant 的校验是相当严格的:刷新令牌必须真实存在于票据注册中心、未过期、且必须属于发起请求的客户端。这一归属校验直接防止了刷新令牌被跨客户端盗用。

四、请求提取器:识别并解析 Refresh Token Grant

在校验通过后,请求会被交给授权类型请求提取器处理。CAS 会根据grant_type参数值在多个提取器中路由,其中AccessTokenRefreshTokenGrantRequestExtractor专门负责refresh_token类型,见 AccessTokenRefreshTokenGrantRequestExtractor.java:

@Override public boolean supports(final WebContext context) { val grantType = getConfigurationContext().getObject().getRequestParameterResolver() .resolveRequestParameter(context, OAuth20Constants.GRANT_TYPE).orElse(StringUtils.EMPTY); return OAuth20Utils.isGrantType(grantType, getGrantType()); } @Override public OAuth20GrantTypes getGrantType() { return OAuth20GrantTypes.REFRESH_TOKEN; } @Override protected String getOAuthParameterName() { return OAuth20Constants.REFRESH_TOKEN; } @Override protected AccessTokenRequestContext extractInternal( final WebContext context, final AccessTokenRequestContext accessTokenRequestContext) { val registeredService = getOAuthRegisteredServiceBy(context); if (registeredService == null) { throw UnauthorizedServiceException.denied("Unable to locate service in registry"); } val shouldRenewRefreshToken = registeredService.isGenerateRefreshToken() && registeredService.isRenewRefreshToken(); return super.extractInternal(context, accessTokenRequestContext .withGenerateRefreshToken(shouldRenewRefreshToken) .withExpireOldRefreshToken(shouldRenewRefreshToken)); }

这段实现揭示了 Refresh Token Grant 的两个关键语义:

  • grant_type路由supports(...)方法通过比较请求中的grant_type参数与OAuth20GrantTypes.REFRESH_TOKEN来决定该提取器是否接管本次请求;
  • 令牌轮换(Token Rotation)extractInternal(...)中读取注册服务的generateRefreshTokenrenewRefreshToken两个开关,只有当两者同时为true时,CAS 才会在本次刷新过程中再生成一个新的 refresh token,并让旧 refresh token 过期withGenerateRefreshTokenwithExpireOldRefreshToken同时被置位)。若服务未开启轮换,则刷新后保留原 refresh token 继续使用。

此外,该提取器通过getRegisteredServiceIdentifierFromRequest(...)从请求中解析client_id/client_secret对,并据此从服务注册中心定位注册服务,找不到服务时直接抛出UnauthorizedServiceException

五、Refresh Token 的生成、追踪与轮换

5.1 令牌生成

新的 access token(以及需要轮换时的新 refresh token)由OAuth20DefaultTokenGenerator生成,见 OAuth20DefaultTokenGenerator.java:

val refreshToken = FunctionUtils.doIf(tokenRequestContext.isGenerateRefreshToken(), Unchecked.supplier(() -> generateRefreshToken(tokenRequestContext, accessToken.getId())), supplier -> null).get(); return new AccessAndRefreshTokens(addedAccessToken, refreshToken);

5.2 Access Token 追踪

CAS 的 refresh token 会记录通过它签发过的所有 access token,以便在刷新令牌被吊销时级联撤销同一授权下产生的访问令牌。该行为由配置项cas.authn.oauth.refreshToken.trackAccessTokens(默认true)控制,相关代码见 OAuth20DefaultTokenGenerator.java#L287-L294:

protected void updateRefreshToken(final AccessTokenRequestContext tokenRequestContext, final Ticket accessToken) { val trackAccessTokens = casProperties.getAuthn().getOauth().getRefreshToken().isTrackAccessTokens(); if (tokenRequestContext.isRefreshToken() && !tokenRequestContext.getToken().isStateless() && trackAccessTokens) { val refreshToken = (OAuth20RefreshToken) tokenRequestContext.getToken(); LOGGER.trace("Tracking access token [{}] linked to refresh token [{}]", accessToken.getId(), refreshToken.getId()); refreshToken.getAccessTokens().add(accessToken.getId()); ticketRegistry.updateTicket(refreshToken); } }

在票据模型层面,OAuth20DefaultRefreshToken持有一个accessTokens集合用于存放这些关联访问令牌的 ID(见 OAuth20DefaultRefreshToken.java),接口 OAuth20RefreshToken.java 对此也给出了明确的语义说明:撤销 refresh token 时应当同时使基于同一授权发放的 access token 失效。

5.3 旧令牌过期

当服务开启令牌轮换后,新令牌生成的同时会执行expireOldRefreshToken(...),将旧 refresh token 标记为过期并从注册中心删除,见 OAuth20DefaultTokenGenerator.java#L406-L411:

private void expireOldRefreshToken(final AccessTokenRequestContext tokenRequestContext) throws Exception { val oldRefreshToken = tokenRequestContext.getToken(); if (!oldRefreshToken.isStateless()) { LOGGER.debug("Expiring old refresh token [{}]", oldRefreshToken); oldRefreshToken.markTicketExpired(); ticketRegistry.deleteTicket(oldRefreshToken); } }

5.4 刷新令牌的票据前缀

refresh token 在 CAS 票据体系中的前缀为RT(见 OAuth20RefreshToken.java),OAuth20DefaultRefreshToken#getPrefix()返回该前缀(见 OAuth20DefaultRefreshToken.java)。在调试或排查票据注册中心中的数据时,以RT开头的票据即为 OAuth 刷新令牌。

六、Refresh Token 的过期策略与配置项

6.1 全局过期策略

CAS 为 refresh token 提供了全局默认的过期策略,其构建逻辑在 OAuth20RefreshTokenExpirationPolicyBuilder.java 中:

private ExpirationPolicy toTicketExpirationPolicy() { val rtProps = casProperties.getAuthn().getOauth().getRefreshToken(); val timeout = Beans.newDuration(rtProps.getTimeToKillInSeconds()).toSeconds(); return buildExpirationPolicyFor(timeout); }

其中timeToKillInSeconds表示超过该时长后 refresh token 即被视为过期的硬性超时(hard timeout)。

6.2 服务级覆盖

CAS 允许在单个注册服务(OAuthRegisteredService)级别覆盖全局过期策略。OAuthRegisteredService上提供了generateRefreshTokenrenewRefreshTokenjwtRefreshTokenrefreshTokenExpirationPolicy等字段(见 OAuthRegisteredService.java)。过期策略构建器会优先使用服务定义中配置的timeToKill,见 OAuth20RefreshTokenExpirationPolicyBuilder.java#L46-L56:

@Override public ExpirationPolicy buildTicketExpirationPolicyFor(final RegisteredServiceDefinition registeredService) { if (registeredService instanceof final OAuthRegisteredService service && service.getRefreshTokenExpirationPolicy() != null) { val policy = service.getRefreshTokenExpirationPolicy(); val timeToKill = policy.getTimeToKill(); if (StringUtils.isNotBlank(timeToKill)) { val timeToKillInSeconds = Beans.newDuration(timeToKill).toSeconds(); return buildExpirationPolicyFor(timeToKillInSeconds); } } return toTicketExpirationPolicy(); }

6.3 相关配置属性

Refresh Token 的全部配置属性集中在 OAuthRefreshTokenProperties.java 中,对应配置文件前缀为cas.authn.oauth.refresh-token.*

配置属性类型默认值说明
cas.authn.oauth.refresh-token.time-to-kill-in-secondsDuration(@DurationCapableP14D(14 天)超过该时长后 refresh token 视为过期的硬性超时
cas.authn.oauth.refresh-token.storage-nameStringoauthRefreshTokensCacheCAS 在底层票据注册中心中保存 OAuth refresh token 所用的存储对象名称
cas.authn.oauth.refresh-token.max-active-tokens-allowedlong0(不限制)单个应用最多可持有的活跃 refresh token 数量;超过上限时请求被拒绝且不再签发 access token
cas.authn.oauth.refresh-token.create-as-jwtbooleanfalse是否将 access token 生成为 JWT 形式
cas.authn.oauth.refresh-token.track-access-tokensbooleantrue是否记录由该 refresh token 签发的 access token,仅用于历史与审计目的

需要特别说明的是,max-active-tokens-allowed表示应用最多能够获得的活跃 refresh token 数量,一旦应用申请的令牌数超过该限制,请求会被拒绝且 access token 不会签发,这一点在 OAuthRefreshTokenProperties.java 的注释中有明确说明。

七、Public Client 场景下的刷新令牌认证

对于不定义 client secret 的公开客户端(public client),例如移动端应用,CAS 提供了专门的OAuth20RefreshTokenAuthenticator,见 OAuth20RefreshTokenAuthenticator.java。该类继承自OAuth20ClientIdClientSecretAuthenticator,但仅在请求满足以下条件时介入认证:

  • 请求中同时存在client_idgrant_type,且grant_typerefresh_token
  • 请求中包含refresh_token参数;
  • 注册服务不需要客户端认证!OAuth20Utils.doesServiceNeedAuthentication(registeredService))。

validateCredentials(...)中,CAS 会从票据注册中心取回该刷新令牌并依次检查:令牌是否存在、是否过期、refreshToken.getClientId()与请求的client_id是否一致。任一条件不满足都会抛出CredentialsException,从而拒绝本次刷新(见 OAuth20RefreshTokenAuthenticator.java#L73-L90)。

因此,在构造 refresh token 请求时,客户端认证方式取决于注册服务的配置:

  • 服务需要认证(如机密客户端):在请求体中携带client_secret
  • 服务为公开客户端:CAS 将依据client_id与刷新令牌本身完成认证,无需提供client_secret

八、完整调用示例与实战要点

8.1 标准刷新请求(机密客户端)

假设客户端注册的服务允许refresh_token授权类型,且已通过授权码流程获得过 refresh token,则刷新 access token 的 HTTP 请求示例如下:

curl -k -X POST 'https://cas.example.org/cas/oauth2.0/accessToken' \ -H 'Content-Type: application/x-www-form-urlencoded' \ -d 'grant_type=refresh_token' \ -d 'client_id=my-client-id' \ -d 'client_secret=my-client-secret' \ -d 'refresh_token=RT-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'

成功响应将返回新的 access token(JSON 形式,典型字段包含access_tokenexpires_in,若服务开启了轮换还会附带新的refresh_token)。

8.2 常见失败场景与排查方向

结合前文源码,可将 refresh token 请求失败归纳为以下几类原因:

失败表现可能原因排查方向
invalid_grantrefresh token 不存在或已过期检查票据注册中心中RT前缀票据;核对time-to-kill-in-seconds与服务的refreshTokenExpirationPolicy
invalid_grantclient_id与 refresh token 归属不一致确认刷新令牌由当前客户端签发;检查 OAuth20RefreshTokenGrantTypeTokenRequestValidator.java 中的归属比对逻辑
请求被拒绝服务定义未允许refresh_token授权类型在注册服务中显式启用refresh_tokengrant type
请求被拒绝应用持有的活跃刷新令牌数超过max-active-tokens-allowed清理旧令牌或上调上限
认证失败公开客户端未满足免认证条件检查服务是否需要客户端认证及client_id是否正确

8.3 服务端配置示意

application.yml/cas.properties中可按需覆盖全局刷新令牌行为:

# 全局刷新令牌有效期(默认 14 天) cas.authn.oauth.refresh-token.time-to-kill-in-seconds=P14D # 是否追踪由刷新令牌签发的访问令牌 cas.authn.oauth.refresh-token.track-access-tokens=true # 单个应用活跃刷新令牌上限 cas.authn.oauth.refresh-token.max-active-tokens-allowed=100

同时在服务注册定义(JSON/YAML)中,可为特定应用开启刷新令牌签发与轮换:

{ "@class": "org.apereo.cas.support.oauth.services.OAuthRegisteredService", "clientId": "my-client-id", "clientSecret": "my-client-secret", "serviceId": "https://app.example.org/.*", "generateRefreshToken": true, "renewRefreshToken": true, "supportedGrantTypes": ["refresh_token"] }

其中generateRefreshToken决定是否向该客户端签发 refresh token,renewRefreshToken决定刷新时是否执行令牌轮换(签发新 refresh token 并使旧令牌过期),supportedGrantTypes用于声明允许的授权类型。

九、小结

Refresh Token Grant 是 CAS OAuth/OpenID Connect 协议栈中用于维持长会话、避免频繁重新授权的关键授权类型。通过本文可以掌握:

  • 协议契约:向/oauth2.0/accessToken提交grant_type=refresh_tokenclient_idclient_secretrefresh_token四个参数即可换取新 access token;
  • 实现链路:请求依次经过 OAuth20AccessTokenEndpointController → OAuth20RefreshTokenGrantTypeTokenRequestValidator → AccessTokenRefreshTokenGrantRequestExtractor → OAuth20DefaultTokenGenerator;
  • 安全语义:刷新令牌必须存在、未过期且归属当前客户端;支持令牌轮换与访问令牌追踪,为吊销与审计提供支撑;
  • 配置能力:全局默认有效期 14 天,可被服务级refreshTokenExpirationPolicy覆盖;公开客户端(无 secret)场景有专门的认证器支持。

在此基础上,读者可以结合 CAS 文档目录中的其他 OAuth 协议流文档(如授权码流、客户端凭据流)构建完整的 OAuth 集成方案,并依据 OAuthRefreshTokenProperties.java 与 OAuthRegisteredService.java 按需定制刷新令牌行为。

  • 后端
  • 认证鉴权
  • 单点登录

【免费下载链接】cas

Apereo CAS - Identity & Single Sign On for all earthlings and beyond.

项目地址:https://gitcode.com/gh_mirrors/ca/cas
点击查看免费下载
上一篇:DIFT技术原理深度剖析:U-Net架构改造与扩散时间步特征提取机制
下一篇:颠覆式小说资源管理工具:Tomato-Novel-Downloader重构数字阅读体验

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询