基于JWT与React-Auth-Kit的AccessToken刷新机制问题咨询
JWT + React-Auth-Kit(TypeScript) 刷新令牌问题排查与优化
一、后端Express路由问题分析
当前代码
router.post('/refresh-token', async (req: Request, res: Response) => { const { refreshToken } = req.body; try { jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET!, async (err: any, decoded: any) => { if (err) { console.log(err); return res.status(403).send({ message: 'Invalid refresh token' }); } const userId = decoded.userId; const user = await User.findById(userId); if (!user) return res.status(403).send({ message: 'Invalid refresh token' }); const accessToken = await generateAccessToken(user); res.send({ accessToken }); }); } catch (err) { console.log(err); res.status(500).send({ message: 'Internal server error' }); } });
存在的问题
- 异步错误捕获失效:
jwt.verify的回调是异步逻辑,外层try/catch无法捕获回调内部(比如User.findById)抛出的错误,会导致未处理的Promise拒绝。 - 缺少RefreshToken有效性校验:仅验证签名和用户存在,未检查该refreshToken是否在用户的有效令牌列表中(比如用户登出时已注销的token,签名仍有效但应失效)。
- 错误状态码不精准:
jwt.verify的错误包含令牌过期、签名无效等不同场景,统一返回403无法区分,建议对过期错误返回401,无效签名返回403。
修正后的后端代码
import { promisify } from 'util'; const verifyRefreshToken = promisify(jwt.verify); router.post('/refresh-token', async (req: Request, res: Response) => { const { refreshToken } = req.body; try { // 使用Promise版本避免回调嵌套 const decoded = await verifyRefreshToken(refreshToken, process.env.REFRESH_TOKEN_SECRET!) as { userId: string }; const userId = decoded.userId; const user = await User.findById(userId); if (!user) { return res.status(403).json({ message: 'Invalid refresh token' }); } // 检查refreshToken是否在用户的有效令牌列表中(假设用户模型有refreshTokens字段) if (!user.refreshTokens.includes(refreshToken)) { return res.status(403).json({ message: 'Refresh token revoked' }); } // 生成新的AccessToken const accessToken = await generateAccessToken(user); // 可选:生成新的RefreshToken(令牌轮换机制,提升安全性) const newRefreshToken = await generateRefreshToken(user); // 更新用户的refreshTokens列表,替换旧token user.refreshTokens = user.refreshTokens.filter(t => t !== refreshToken).concat(newRefreshToken); await user.save(); res.json({ accessToken, refreshToken: newRefreshToken }); } catch (err: any) { console.log(err); // 区分JWT错误类型,返回精准状态码 if (err.name === 'TokenExpiredError') { return res.status(401).json({ message: 'Refresh token expired' }); } res.status(403).json({ message: 'Invalid refresh token' }); } });
二、React-Auth-Kit refreshApi 工作原理与修正
工作原理
React-Auth-Kit的createRefresh会自动监听AccessToken的生命周期:
- 根据
interval设置的时间间隔,定期检查AccessToken是否即将过期 - 当检测到AccessToken快过期时,调用
refreshApiCallback向后端请求新的AccessToken - 刷新成功后,自动更新存储中的AccessToken和过期时间;失败则触发认证失效逻辑
当前前端代码问题
const refreshApi = createRefresh({ interval: 15, // minutes refreshApiCallback: async ({ authToken, refreshToken, authTokenExpireAt, refreshTokenExpiresAt }) => { try { const res = await axios.post('.../refresh-token', { refreshToken }); return { isSuccess: true, newAuthToken: res.data.accessToken, newAuthTokenExpireIn: 15, }; } catch (err) { console.log(err); return { isSuccess: false, newAuthToken: '', }; } } });
- interval设置不合理:如果AccessToken有效期是15分钟,设置interval为15分钟会导致token过期后才触发刷新,应设置为比有效期短的时间(比如10分钟),确保在token过期前完成刷新。
- 未处理新RefreshToken:如果后端实现了令牌轮换,需要接收并返回新的refreshToken,更新前端存储。
- 失败处理不完整:刷新失败时,应主动清除用户认证状态(比如调用
signOut),避免用户停留在未授权状态。 - 请求URL为占位符:需替换为实际的后端接口地址(如
/api/auth/refresh-token)。
修正后的前端代码
import { createRefresh, useSignOut } from 'react-auth-kit'; import axios from 'axios'; const refreshApi = createRefresh({ // 设置为AccessToken有效期的2/3,比如15分钟有效期就设10分钟 interval: 10, refreshApiCallback: async ({ refreshToken }) => { try { const res = await axios.post('/api/auth/refresh-token', { refreshToken }); return { isSuccess: true, newAuthToken: res.data.accessToken, newAuthTokenExpireIn: 15, // 和后端AccessToken有效期一致 newRefreshToken: res.data.refreshToken, // 如果后端返回新的refreshToken newRefreshTokenExpiresIn: 7 * 24 * 60, // 新refreshToken的有效期(比如7天) }; } catch (err) { console.error('Refresh token failed:', err); // 刷新失败,执行登出 const signOut = useSignOut(); signOut(); return { isSuccess: false, newAuthToken: '', }; } } });
使用方式
需要将refreshApi传入AuthProvider的refresh属性,确保全局生效:
import { AuthProvider } from 'react-auth-kit'; function App() { return ( <AuthProvider authType={'cookie'} authName={'_auth'} refresh={refreshApi} // 传入refreshApi cookieDomain={window.location.hostname} cookieSecure={window.location.protocol === 'https:'} > {/* 你的应用组件 */} </AuthProvider> ); }
三、整体优化建议
后端优化
- 实现令牌轮换:每次刷新时生成新的RefreshToken,废弃旧token,降低令牌被盗用的风险。
- 存储RefreshToken哈希:不要在数据库中存储明文refreshToken,存储其哈希值,验证时对比哈希。
- 设置RefreshToken有效期:给refreshToken设置合理的有效期(比如7天),避免长期有效。
- 添加速率限制:对refresh-token接口添加速率限制,防止暴力攻击。
前端优化
- 请求拦截器:在axios中添加请求拦截器,自动在请求头中携带AccessToken,避免手动处理。
- 错误拦截器:拦截后端返回的401/403错误,自动触发登出或刷新逻辑。
- 持久化存储:确保refreshToken安全存储(比如使用HttpOnly Cookie,避免XSS攻击)。
内容的提问来源于stack exchange,提问作者Alexxino
相关产品推荐
相关产品推荐

