You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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的生命周期:

  1. 根据interval设置的时间间隔,定期检查AccessToken是否即将过期
  2. 当检测到AccessToken快过期时,调用refreshApiCallback向后端请求新的AccessToken
  3. 刷新成功后,自动更新存储中的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 16:07:13