- 后端
- 认证鉴权
- 单点登录
【免费下载链接】cas
Apereo CAS - Identity & Single Sign On for all earthlings and beyond.
本文围绕 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/accessToken | grant_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_URL与ACCESS_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 方法均可发起令牌请求。整体处理链路可分为四步:
verifyAccessTokenRequest(...):交给注册的 grant type 校验器链,验证请求合法性;examineAndExtractAccessTokenGrantRequest(...):通过可审计的提取器(extractor)解析出AccessTokenRequestContext;getAccessTokenGenerator().generate(...):由令牌生成器实际产出新的 access token(及可选的 refresh token);generateAccessTokenResponse(...):将结果编码为 JSON 响应返回给客户端。
其中校验失败时,CAS 会直接返回invalid_grant错误码,这正是 refresh token 无效、过期或不属于该客户端时的典型错误响应。
三、Refresh Token 请求的校验逻辑
当 CAS 收到grant_type=refresh_token的请求后,负责校验的是OAuth20RefreshTokenGrantTypeTokenRequestValidator,其核心逻辑见 OAuth20RefreshTokenGrantTypeTokenRequestValidator.java,校验过程按以下顺序执行:
- 参数完整性检查:解析请求中的
refresh_token参数与客户端标识,若 refresh token 缺失或client_id为空,直接返回校验失败; - 刷新令牌存活性检查:通过
ticketRegistry.getTicket(token, OAuth20RefreshToken.class)从票据注册中心(Ticket Registry)中查找对应的 refresh token 票据,若抛出InvalidTicketException(即令牌不存在或已过期),则校验失败并记录告警日志; - 服务访问策略检查:依据
client_id定位注册服务(OAuthRegisteredService),并执行RegisteredServiceAccessStrategyEnforcer的服务访问策略审计,未授权则抛出异常; - 授权类型检查:调用
isGrantTypeSupportedBy(registeredService, grantType),确认该服务定义中明确允许refresh_token授权类型,否则拒绝请求; - 令牌归属检查:比对 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(...)中读取注册服务的generateRefreshToken与renewRefreshToken两个开关,只有当两者同时为true时,CAS 才会在本次刷新过程中再生成一个新的 refresh token,并让旧 refresh token 过期(withGenerateRefreshToken与withExpireOldRefreshToken同时被置位)。若服务未开启轮换,则刷新后保留原 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上提供了generateRefreshToken、renewRefreshToken、jwtRefreshToken与refreshTokenExpirationPolicy等字段(见 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-seconds | Duration(@DurationCapable) | P14D(14 天) | 超过该时长后 refresh token 视为过期的硬性超时 |
cas.authn.oauth.refresh-token.storage-name | String | oauthRefreshTokensCache | CAS 在底层票据注册中心中保存 OAuth refresh token 所用的存储对象名称 |
cas.authn.oauth.refresh-token.max-active-tokens-allowed | long | 0(不限制) | 单个应用最多可持有的活跃 refresh token 数量;超过上限时请求被拒绝且不再签发 access token |
cas.authn.oauth.refresh-token.create-as-jwt | boolean | false | 是否将 access token 生成为 JWT 形式 |
cas.authn.oauth.refresh-token.track-access-tokens | boolean | true | 是否记录由该 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_id与grant_type,且grant_type为refresh_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_token、expires_in,若服务开启了轮换还会附带新的refresh_token)。
8.2 常见失败场景与排查方向
结合前文源码,可将 refresh token 请求失败归纳为以下几类原因:
| 失败表现 | 可能原因 | 排查方向 |
|---|---|---|
invalid_grant | refresh token 不存在或已过期 | 检查票据注册中心中RT前缀票据;核对time-to-kill-in-seconds与服务的refreshTokenExpirationPolicy |
invalid_grant | client_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_token与client_id、client_secret、refresh_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.
相关推荐
Apereo CAS OAuth 隐式授权流(Implicit Flow / Token Response Type)实战指南
Apereo CAS OAuth 隐式授权流(Implicit Flow / Token Response Type)实战指南 本指南以 Apereo CAS
后端认证鉴权单点登录Apereo CAS OAuth 2.0 Token Exchange 协议流程实战指南:Impersonation 与 Delegation 的令牌交换
Apereo CAS OAuth 2.0 Token Exchange 协议流程实战指南:Impersonation 与 Delegation 的令牌交换 导读
后端认证鉴权单点登录编程方式获取 DiceBear 头像风格全部可选参数:OptionsDescriptor 完全指南
编程方式获取 DiceBear 头像风格全部可选参数:OptionsDescriptor 完全指南 每种 DiceBear 头像风格都因为其组件(compone
UI组件后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考