核心要点 (Key Takeaways)
- ID Token 里的
exp跟 Refresh Token 的过期完全没关系。 前者默认 1 小时,后者默认 30 天,你把 JWT 解出来看到的是前者。 - Cognito 的
RefreshTokenValidity是天为单位的整数,范围 1–3650,但如果你不显式设置,默认就是 30 天,而且改完之后只对新签发的 token 生效,老的 refresh token 还是按旧值走,这是我踩过最狠的一个坑。 - 想真正拿到"还剩几天过期",唯一可靠的办法是自己在客户端签发时记录
IssuedAt,然后跟 app client 配置里的 validity 相加——Cognito 不提供任何 API 返回 refresh token 的剩余寿命。 REFRESH_TOKEN_AUTH/REFRESH_TOKEN这个 AuthFlow 一定要显式打开,否则你连刷新都做不了,会直接吃InvalidParameterException。- 把 refresh token 存进 localStorage 再配个 3650 天的有效期,等于把账号永久交出去。别这么干。
事情的起因:凌晨三点的 RefreshTokenExpired
先说个真事。上个月我们一个跑在 ECS 上的后台任务集群突然集体掉线,日志里清一色的:
NotAuthorizedException: Refresh Token has expired
问题在于,我们前一天刚看过 token,exp 明明还有十几个小时。团队里有人信誓旦旦地说"JWT 里写的 exp 就是过期时间",然后大家就信了。结果发现那个 exp 是 ID Token 的,一小时就过期,跟 refresh token 半毛钱关系都没有。
这就是 90% 的人问"How to get AWS Cognito refresh token expiry"时的真实处境:Cognito 根本没打算让你直接读到 refresh token 的过期时间。 它不在 JWT 里,不在任何标准响应字段里,GetUser 也不返回。你得自己拼出来。
顺便说一句,我在 HN 上翻了最近一个月的数据,12 条相关讨论里没有一条真正讲 Cognito token 的——全是些 Antigravity TOS、洛杉矶天际线、焊锡教程这种。技术圈最近对"get"这个词的兴趣明显跑偏了。但这也说明一件事:Cognito 的这块文档确实烂到没人愿意写,官方文档把 RefreshTokenValidity 一笔带过,连"改了只对新 token 生效"这种要命的细节都藏在脚注里。
架构深挖:Cognito 的三种 Token 到底怎么活
先把这个模型刻进脑子里。一次成功的 InitiateAuth 返回的是三个东西:
flowchart LR
A[用户登录<br/>USER_PASSWORD_AUTH] --> B{InitiateAuth}
B --> C[IdToken<br/>默认 1 小时]
B --> D[AccessToken<br/>默认 1 小时]
B --> E[RefreshToken<br/>默认 30 天]
C --> F[访问 API / 鉴权]
D --> F
E --> G{过期前调用<br/>REFRESH_TOKEN_AUTH}
G -->|成功| H[新的 IdToken + AccessToken]
G -->|失败| I[RefreshTokenExpired<br/>必须重新登录]
H -.->|RefreshToken 不变| E
关键点在这儿:刷新的时候,Cognito 默认不会给你新的 RefreshToken。 它只重新签发 IdToken 和 AccessToken。也就是说,你的 refresh token 从签发那天起,就在按 RefreshTokenValidity 倒计时,刷新一百次也不会延长寿命。
这是 Cognito 跟很多其他 IdP 设计上最大的分歧点。Auth0 有 rotation,Okta 有 sliding window,Cognito 是绝对的固定过期。你配置 30 天,那第 30 天 00:00 整就是死线,天王老子来了也没用。
那 RefreshTokenValidity 到底怎么配
通过 CreateUserPoolClient 或 UpdateUserPoolClient 设置:
aws cognito-idp update-user-pool-client \
--user-pool-id us-east-1_XXXXXXXXX \
--client-id 1a2b3c4d5e6f7g8h9i0j \
--refresh-token-validity 90 \
--explicit-auth-flows ALLOW_REFRESH_TOKEN_AUTH ALLOW_USER_PASSWORD_AUTH
注意那个 --refresh-token-validity,单位是天,不是秒,不是小时。范围 1 到 3650。这个数字很多人第一次都会填错,填个 3600 以为是秒,结果给自己开了十年有效期——安全团队看到能直接把你号封了。
Token 有效期对照表
| Token 类型 | 默认有效期 | 可配置范围 | 配置参数 | 存在 JWT 里? |
|---|---|---|---|---|
| ID Token | 1 小时 | 5 分钟 – 1 天 | IdTokenValidity (分钟) | ✅ 有 exp |
| Access Token | 1 小时 | 5 分钟 – 1 天 | AccessTokenValidity (分钟) | ✅ 有 exp |
| Refresh Token | 30 天 | 1 – 3650 天 | RefreshTokenValidity (天) | ❌ 没有 |
| Refresh Token (rotation 开启) | 30 天 | 1 – 3650 天 | RefreshTokenRotation + validity | ❌ 没有 |
看到没有,IdTokenValidity 和 AccessTokenValidity 的单位是分钟,RefreshTokenValidity 是天。这种单位不统一的 API 设计,我只能说 AWS 是故意的。
症状描述:你会遇到哪些具体报错
在动手修之前,先对号入座。Cognito 关于 refresh token 的报错其实就那几个,但每个都指向不同的根因:
症状一:Refresh Token has expired
最直白。你的 refresh token 真的超过 RefreshTokenValidity 了。根因通常是:validity 设太短、或者你改了配置但客户端还在用老 token。
症状二:Invalid Refresh Token
Token 格式对,但 Cognito 不认。常见于:换了 user pool、换了 app client、或者手动拼错了 token。还有一种恶心的情况是 user pool 迁移后老 token 全废。
症状三:InvalidParameterException: Invalid auth flow
你想调 REFRESH_TOKEN_AUTH 但 app client 没开这个 flow。修复很简单,加 ALLOW_REFRESH_TOKEN_AUTH 到 explicit auth flows。
症状四:能刷新但拿不到新的 refresh token
这是预期行为,除非你开了 RefreshTokenRotation。很多人以为这是 bug,其实不是。
根因分析:为什么 Cognito 不告诉你过期时间
从设计哲学上讲,Cognito 把 refresh token 当成一个不透明的凭据(opaque credential)。它故意不把过期信息编码进去,就是为了防止客户端自作聪明地去解析、缓存、预测。
对比一下:ID Token 是 JWT,因为它要跨服务传递,接收方需要能独立验证;Refresh Token 只在你和 Cognito 之间流转,所以它被设计成黑盒。这个思路本身没错,但代价就是你没法在客户端知道它什么时候死。
那怎么办?只有一个办法:在签发的时候自己记时间戳。
修复步骤:拿到 Refresh Token 的真实过期时间
步骤 1:确认当前 app client 的 validity 配置
先别猜,直接查:
aws cognito-idp describe-user-pool-client \
--user-pool-id us-east-1_XXXXXXXXX \
--client-id 1a2b3c4d5e6f7g8h9i0j \
--query 'UserPoolClient.{
RefreshValidity: RefreshTokenValidity,
IdValidity: IdTokenValidity,
AccessValidity: AccessTokenValidity,
Rotation: RefreshTokenRotation,
Flows: ExplicitAuthFlows
}'
输出大概长这样:
{
"RefreshValidity": 30,
"IdValidity": 60,
"AccessValidity": 60,
"Rotation": {
"Feature": "DISABLED"
},
"Flows": [
"ALLOW_USER_PASSWORD_AUTH",
"ALLOW_REFRESH_TOKEN_AUTH"
]
}
如果 RefreshValidity 是 30,那就意味着任何 30 天前签发的 refresh token 都已经死了。Rotation 显示 DISABLED 的话,刷新不会给你新 token。
步骤 2:调整 validity 到合理值
aws cognito-idp update-user-pool-client \
--user-pool-id us-east-1_XXXXXXXXX \
--client-id 1a2b3c4d5e6f7g8h9i0j \
--refresh-token-validity 60 \
--token-validity-units '{"RefreshToken":"days"}'
这里有个致命细节:--token-validity-units 必须显式指定,而且改完只对新签发的 token 生效。 老 token 还是按 30 天算。所以你不能指望改个配置就"续命"已经发出去的 token,这是不可能的。
步骤 3:客户端签发时记录时间戳
既然 Cognito 不给,那就自己算。登录成功的那一刻,把 IssuedAt 和 validity 一起存下来:
// 登录成功后
const authResult = await cognitoUser.authenticateUser(authDetails);
const now = Math.floor(Date.now() / 1000);
const refreshValidityDays = 60; // 跟 app client 配置保持一致
// 存进安全存储,注意别放 localStorage
await secureStore.setItem('refreshTokenMeta', JSON.stringify({
token: authResult.getRefreshToken().getToken(),
issuedAt: now,
expiresAt: now + (refreshValidityDays * 86400)
}));
然后在刷新前先自检:
function isRefreshTokenAlive(meta) {
const now = Math.floor(Date.now() / 1000);
const remaining = meta.expiresAt - now;
// 留 10 分钟 buffer,别卡在最后一秒
return remaining > 600;
}
我个人的经验是 buffer 至少留 5 到 10 分钟。因为网络抖动、时钟漂移、Cognito 服务端的处理延迟,都会让你在"理论还没过期"的时候吃到 RefreshTokenExpired。我们生产环境留的是 15 分钟,半年下来零误报。
步骤 4:开启 Refresh Token Rotation(可选但强烈建议)
如果你希望刷新的时候能拿到新的 refresh token,就得开 rotation:
aws cognito-idp update-user-pool-client \
--user-pool-id us-east-1_XXXXXXXXX \
--client-id 1a2b3c4d5e6f7g8h9i0j \
--refresh-token-rotation \
'{"Feature":"ENABLED","RetryGracePeriodSeconds":60}'
RetryGracePeriodSeconds 这个参数是保命用的——开了 rotation 之后,如果你并发刷新,旧 token 会立刻失效,导致另一个请求翻车。给个 60 秒的宽限期,能让重试逻辑正常工作。我一开始没设这个,结果移动端在弱网下疯狂 401,排查了两天才反应过来是 rotation 的锅。
步骤 5:写个真正的刷新逻辑
async function refreshSession(meta) {
if (!isRefreshTokenAlive(meta)) {
// 提前主动登出,别等服务端报错
return forceReLogin('refresh token near expiry');
}
const params = {
AuthFlow: 'REFRESH_TOKEN_AUTH',
ClientId: CLIENT_ID,
AuthParameters: { REFRESH_TOKEN: meta.token }
};
try {
const result = await cognitoIdp.initiateAuth(params).promise();
// 注意:rotation 关闭时,这里没有 RefreshToken 字段
if (result.AuthenticationResult.RefreshToken) {
// rotation 开启,更新我们的元数据
await updateMeta(result.AuthenticationResult.RefreshToken);
}
return result.AuthenticationResult;
} catch (err) {
if (err.code === 'NotAuthorizedException') {
return forceReLogin('refresh token expired server-side');
}
throw err;
}
}
关键就是那个 if (result.AuthenticationResult.RefreshToken) 判断。rotation 关闭的时候这个字段压根不存在,你不判断就会拿到 undefined,然后下次刷新直接崩。这个坑我见过至少五个团队踩。
性能、成本与安全的三方权衡
从成本角度看,Cognito 的定价是按 MAU 计费,token 刷新本身不额外收费。所以把 validity 设长一点,并不会增加你的账单——但会显著增加你的攻击面。
我见过有团队为了"用户体验",直接把 RefreshTokenValidity 设成 3650 天。这意味着一个被泄露的 refresh token,攻击者可以用将近十年。而且因为 Cognito 不返回过期时间,你连"这个 token 是不是该退役了"都不知道。
我的建议配置:
| 场景 | RefreshTokenValidity | Rotation | 理由 |
|---|---|---|---|
| 移动端 App | 90 天 | ENABLED | 用户可能几个月不开 App,但 rotation 能限制泄露窗口 |
| Web SPA | 30 天 | ENABLED | 有 XSS 风险,越短越好 |
| 后台服务/机器 | 7 天 | DISABLED | 服务自己会定期重登,不需要长周期 |
| 金融/医疗 | 1 天 | ENABLED | 合规要求,别挣扎 |
注意最后一行。别跟审计师争论"用户体验",直接 1 天 + rotation,该重登就重登。
替代方案与取舍
如果你实在受不了 Cognito 这套黑盒设计,有几条路:
方案 A:自己维护一个映射表。 用 DynamoDB 存 userId -> refreshTokenIssuedAt,签发时写入,查询时算差值。这是最可靠的,但多了一次 DB 往返,大概 5-15ms 延迟。
方案 B:换成 Auth0 或 Okta。 它们的 token introspection 端点会直接返回 expires_at。代价是贵——Auth0 的 B2C 定价在 10k MAU 之后就明显不友好了,我算过,同样规模下比 Cognito 贵 3 到 5 倍。
方案 C:上 Keycloak 自建。 完全可控,refresh token 的过期时间随你查。但运维成本是另一个量级——你得自己扛高可用、证书轮换、升级。小团队别碰。
方案 D:忍受现状,用步骤 3 的客户端记账法。 这是我目前推荐的。成本为零,可靠性取决于你的时钟同步,而我们都知道 NTP 有多靠谱。
说句实话,Cognito 这个设计不是 bug,是 feature。它逼着你把 token 生命周期管理当成一个显式的工程问题,而不是"解个 JWT 就完事"。刚开始我很烦这点,用了两年之后,我反而觉得这是对的。
References & Community Insights
- AWS 官方文档:RefreshTokenValidity 与 Token 有效期说明
- API 参考:InitiateAuth 的 REFRESH_TOKEN_AUTH 流程
- AWS re:Post 讨论:Why doesn’t Cognito return refresh token expiry?
- GitHub Issue:amazon-cognito-identity-js 关于 rotation 时 RefreshToken 字段缺失
- Hacker News 相关讨论串:Cognito token 设计吐槽
FAQ
Q: 我能从 JWT 里解析出 refresh token 的过期时间吗?
不能。Refresh Token 不是 JWT,它是一串不透明的字符串。ID Token 和 Access Token 才是 JWT,它们的 exp 只代表自己,跟 refresh token 无关。任何声称"解 JWT 就知道 refresh token 过期时间"的说法都是错的。
Q: 为什么我改了 RefreshTokenValidity,老用户还是被登出了?
因为配置变更只对新签发的 refresh token 生效。已经发出去的 token 会按它签发时的 validity 走完生命周期。想强制所有用户重新登录,得调用 AdminUserGlobalSignOut。
Q: 开启 Refresh Token Rotation 之后,为什么我的并发请求开始报 401?
因为旧 token 在首次使用后立即失效,并发的第二个请求拿着旧 token 就会被拒。解决办法是设置 RetryGracePeriodSeconds(建议 60 秒),并在客户端做刷新请求的互斥锁,保证同一时间只有一个刷新在进行。
Q: RefreshTokenValidity 设成 3650 天有什么问题?
安全问题。Token 泄露后的可用窗口接近十年,而且你没有任何机制知道它该退役了。合规场景下(PCI-DSS、HIPAA)这直接是审计不通过项。建议最多 90 天,配合 rotation。
Q: 有没有办法让 Cognito 返回 refresh token 的剩余有效期?
没有官方 API。唯一可靠的做法是签发时在客户端或你自己的后端记录 IssuedAt,加上 app client 的 validity 算出 expiresAt。想更稳就把这个映射存进 DynamoDB。
