十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

一个 Token 就够了,JWT 续签为什么要搞 Access Token + Refresh Token 双 Token?

一个 Token 就够了,JWT 续签为什么要搞 Access Token + Refresh Token 双 Token? JWT 做登录认证有一点很多人不理解登录成功发一个 Token前端每次请求带上后端验签过期了重新登录。感觉一个 Token 就能解决所有问题了。那为什么 OAuth 2.0 规范里非搞了Access Token和Refresh Token两个因为单 Token 方案有一个绕不开的点那就是 Token 的有效期。单 Token 的问题Token 用于身份认证必须给它设一个过期时间。这个过期时间怎么设就是问题所在。设短了用户体验不好了设 30 分钟用户正在填一个复杂的表单填了 25 分钟点提交的时候 Token 过期了直接跳转到登录页表单数据全没了。谁都得想骂人。设长了安全风险高了设 7 天如果这个 Token 被泄露了抓包、XSS 攻击、用户在公共电脑上没退出登录——攻击者拿着这个 Token 可以用 7 天。在这 7 天里你没有任何办法让它失效因为 JWT 是无状态的服务端不存 Session没地方去标记这个 Token 作废了。也可以搞个黑名单Token 被盗就加入黑名单。这样也行但这会使得每次请求都要查一遍黑名单JWT 的无状态、不用查库的优势就没了又干回到了Session的模式只不过换了个存储的地方。单 Token 方案的根本矛盾过期时间越短越安全但用户体验越差过期时间越长体验越好但安全风险越大。没法同时满足两头。双 Token 如何解决思路很直接既然一个 Token 兼顾不了安全和体验那就拆成两个一个管安全一个管体验。Access Token过期时间很短通常 15-30 分钟。前端每次请求带它去访问业务接口。因为有效期短即使被泄露了攻击者能利用的窗口也很小。Refresh Token过期时间很长通常 7-30 天。它只有一个用途在 Access Token 过期之后用它去换一个新的 Access Token。它不能直接访问业务接口。整个流程是这样的用户体验上只要 Refresh Token 没过期用户永远不会被踢出去。哪怕 Access Token 15 分钟就过期一次前端自动刷新用户根本感觉不到。安全性上真正在网络上高频传输的是 Access Token它即使被截获也只有 15-30 分钟的有效期。Refresh Token 只在刷新的时候传输一次暴露面小得多。为什么 Refresh Token 更安全Refresh Token 有效期那么长它被偷了不是一样危险确实但 Refresh Token 有几个特点让它被盗的概率远低于 Access Token传输频率低Access Token 每次请求都要带上一天可能传输几百上千次。Refresh Token 只在 Access Token 过期时才传输一次一天可能就传几次。传输次数越少被中间人截获的概率越低。存储方式可以不同Access Token 通常放在内存或者localStorage里方便前端随时取用。Refresh Token 可以存在HttpOnly Cookie里JavaScript访问不到XSS 攻击偷不走它。可以做更严格的校验Refresh Token 请求刷新接口时服务端可以额外校验设备指纹、IP 地址是否一致。如果发现 Refresh Token 在一个陌生 IP 上使用直接拒绝并让用户重新登录。这种重校验对 Access Token 做的话成本太高每个请求都校验设备指纹影响性能但 Refresh Token 只在刷新时才用一天就几次完全扛得住。可以做 Refresh Token 轮换每次用 Refresh Token 换新的 Access Token 时同时下发一个新的 Refresh Token旧的立即作废。这样即使 Refresh Token 被偷了攻击者用了一次之后正常用户下一次刷新就会因为旧 Token 已经失效而触发异常服务端可以立刻发现并强制用户重新登录。后端实现的核心逻辑不贴完整的代码了说清楚关键点。登录接口同时生成两个 Tokenpublic LoginResponse login(String username, String password) { // 验证用户名密码... String accessToken jwtUtil.generateToken(userId, Duration.ofMinutes(30)); String refreshToken UUID.randomUUID().toString(); // Refresh Token 存 Redis关联用户信息 redis.opsForValue().set( refresh: refreshToken, userId, 7, TimeUnit.DAYS ); returnnew LoginResponse(accessToken, refreshToken); }注意 Refresh Token 不一定是 JWT。很多人以为两个都是 JWT其实 Refresh Token 用一个随机字符串就行真正的状态存在服务端的 Redis 里。这样你可以随时在服务端让它失效——用户点了退出登录直接删掉 Redis 里的 Refresh Token 立刻生效。如果 Refresh Token 也用 JWT 且不存服务端那又回到了无法主动让它失效的老问题。刷新接口public TokenResponse refresh(String refreshToken) { // 从 Redis 查 Refresh Token 对应的用户 String userId redis.opsForValue().get(refresh: refreshToken); if (userId null) { thrownew AuthException(Refresh Token 已过期请重新登录); } // 生成新的 Access Token String newAccessToken jwtUtil.generateToken(userId, Duration.ofMinutes(30)); // Refresh Token 轮换旧的删掉发一个新的 redis.delete(refresh: refreshToken); String newRefreshToken UUID.randomUUID().toString(); redis.opsForValue().set( refresh: newRefreshToken, userId, 7, TimeUnit.DAYS ); returnnew TokenResponse(newAccessToken, newRefreshToken); }每次刷新都换一个新的 Refresh Token旧的立刻删掉。这是 Refresh Token RotationOAuth 2.0 安全最佳实践里推荐的做法。前端实现前端要做的核心是拦截 401 响应自动刷新然后重放失败的请求。用 Axios 拦截器来实现let isRefreshing false; let pendingRequests []; axios.interceptors.response.use( response response, async error { const originalRequest error.config; if (error.response?.status 401 !originalRequest._retry) { if (isRefreshing) { // 已经在刷新了把请求排队 returnnewPromise(resolve { pendingRequests.push(token { originalRequest.headers.Authorization Bearer token; resolve(axios(originalRequest)); }); }); } originalRequest._retry true; isRefreshing true; try { const { data } await axios.post(/auth/refresh, { refreshToken: getRefreshToken() }); setAccessToken(data.accessToken); setRefreshToken(data.refreshToken); // 把排队的请求全部用新 Token 重发 pendingRequests.forEach(cb cb(data.accessToken)); pendingRequests []; originalRequest.headers.Authorization Bearer data.accessToken; return axios(originalRequest); } catch (e) { // Refresh Token 也过期了跳登录 clearTokens(); window.location.href /login; returnPromise.reject(e); } finally { isRefreshing false; } } returnPromise.reject(error); } );这段代码有个关键细节isRefreshing标志位和pendingRequests队列。Access Token 过期的瞬间页面上可能同时有好几个请求都返回了 401。如果每个 401 都去调一次刷新接口就会并发刷新前一个刷新拿到的 Refresh Token 刚换完就被后一个刷新请求用旧的 Token 去调直接失败。所以要保证只有第一个 401 触发刷新其他的排队等着等新 Token 拿到了统一重发。什么时候不需要双 Token双 Token 不是所有场景都值得搞。内部管理后台用户就那几十个运营人员安全要求没那么极端Token 过期了重新登录也不是什么大事。单 Token 设个 2 小时过期完全够用。短期活动页面H5 活动页可能就活一周搞双 Token 的工程量大于它的收益。已经用了 Session 的项目如果你的系统本来就是有状态的Session 存在 Redis 里那 Token 只是 Session ID 的另一种表现形式。Session 机制天然支持服务端主动失效双 Token 解决的问题对你来说不存在。双 Token 真正有价值的场景是无状态的 JWT 认证 用户量大 安全性要求高 用户体验不能打折扣。移动端 App、SaaS 产品、开放平台 API 这些是典型场景。
返回列表