嗨,掘金的小伙伴们!👋 欢迎回到我们的 AI Fullstack 项目实战系列!今天我们要攻克一个在全栈开发中非常经典且硬核的功能——双 Token 无感刷新(Refresh Token)。
相信大家在使用 App 时都有过这样的体验:登录一次后,好像很久都不用再登录,但偶尔过个十天半个月又突然让你重新登录。这背后其实就是 Access Token(短命)和 Refresh Token(长命)这对好基友在默默工作。
今天我们就结合代码,把这个功能的后端和前端实现彻底讲透!坐稳了,老司机发车啦!🚗💨
🧐 为什么要搞两个 Token?
简单来说,是为了安全和体验的平衡:
- Access Token (AT): 有效期短(比如 15 分钟)。用于日常请求,就算被黑客截获,也就只能用 15 分钟,损失可控。🛡️
- Refresh Token (RT): 有效期长(比如 7 天)。只用于在 AT 过期时,找服务器“换”一个新的 AT。因为它只在刷新时传输,暴露风险小。🔄
🔙 后端:发令枪与弹药库
我们先看看后端 NestJS 是怎么处理这个逻辑的。
1. 刷新 Token 的入口 (refreshToken)
当前端拿着 refresh_token 来请求时,后端要做的是:验票 -> 换新票。
async refreshToken(rt: string) {
try {
// 1️⃣ 验明正身:验证传来的 refresh_token 是否有效
const payload = await this.jwtService.verifyAsync(rt,{
secret:process.env.TOKEN_SECRET
});
console.log(payload,"??????");
// 2️⃣ 再次颁发:如果验证通过,用 payload 里的信息(id, name)生成新的一对 Token
const token = await this.generateTokens(payload.sub,payload.name);
return token;
} catch(e) {
console.log(e);
// 3️⃣ 铁面无私:如果 RT 也过期了或者伪造的,直接抛出 401,让用户重新登录
throw new UnauthorizedException('Refresh Token已失效,请重新登录');
}
}
2. 颁发 Token 的核心 (generateTokens)
这里有个很棒的 OOP 实践:复杂度剥离。我们将生成 Token 的逻辑单独抽离成一个 private 方法。
// OOP private 方法,专门负责生产 Token
private async generateTokens(id: string,name: string) {
// 🔫 发令枪装填弹药:准备 Payload
const payload = {
sub: id, // JWT 标准字段,通常放用户ID
name
};
// ⚡ 并发执行:同时生成两个 Token,效率 Max!
const [at, rt] = await Promise.all([
// 🎫 Access Token:15分钟,短小精悍
this.jwtService.signAsync(payload, {
secret: process.env.TOKEN_SECRET,
expiresIn: '15m'
}),
// 🎟️ Refresh Token:7天,超长待机
this.jwtService.signAsync(payload, {
secret: process.env.TOKEN_SECRET,
expiresIn: '7d'
})
])
// 打包带走
return {
access_token: at,
refresh_token: rt
}
}
🔜 前端:Axios 拦截器大作战
后端准备好了,前端怎么配合呢?这可是个细活,涉及到并发控制和请求重试。
1. 为什么不直接用 axios?
我们创建了一个 axios 实例。这样做的好处是,我们可以给这个实例单独配置 baseURL 和拦截器,而不会污染全局的 axios 对象。
const instance = axios.create({
// 🏭 所有的请求都会自动加上这个前缀,方便管理
baseURL: 'http://localhost:5173/api'
});
2. 请求拦截:自动携带令牌
每次发请求前,我们都要从 Store 里把 Token 拿出来,塞进请求头里。
instance.interceptors.request.use(config => {
// 🛒 从 Zustand Store 中获取 Access Token
// 注意:这里用 getState() 而不是 hook,因为这里是普通函数环境
const token = useUserStore.getState().accessToken;
if(token){
// 🎫 夹带私货:告诉服务器 "我是有身份的人"
config.headers.Authorization = `Bearer ${token}`;
}
return config;
})
3. 响应拦截:并发请求的“红绿灯” 🚦
这是最精彩的部分!当 Access Token 过期时,可能会有多个请求同时失败(401)。我们不能让它们都去刷新 Token,那样服务器会疯掉的。我们需要一个锁和一个队列。
// 🔒 锁:标记是否正在刷新 Token
let isRefreshing = false;
// 🚌 候车室:存储因为 Token 过期而暂停的请求
let requestsQueue: any[] = [];
失败处理逻辑
当请求失败(特别是 401 错误)时,我们进入 response 拦截器的 error 回调:
}, async (err) => {
// config 中存储的是请求失败返回的请求对象,包含请求数据
// response 中存储的是返回的错误信息❌
const { config, response } = err;
// 🛑 核心判断:是 401 错误,且这个请求之前没重试过
// _retry 是我们自定义的标记,标记是否已经重新请求过了,防止死循环
if(response.status == 401 && !config._retry) {
// 场景 A:已经有别的请求在刷新 Token 了
if(isRefreshing) {
// ⏳ 排队:返回一个 Promise,把请求暂存到队列里
// 等新 Token 来了,再 resolve 这个 Promise
return new Promise((resolve) => {
requestsQueue.push((token:string) => {
// 替换成新的 Token
config.headers.Authorization = `Bearer ${token}`;
// 🚀 重新发射!
resolve(instance(config));
});
})
}
// 场景 B:我是第一个发现 Token 过期的
config._retry = true; // 🚩 立个 flag,表示我重试过了
isRefreshing = true; // 🔒 落锁,别人都得排队
try {
// 1️⃣ 获取本地存的 Refresh Token
const { refreshToken } = useUserStore.getState();
if(refreshToken) {
// 2️⃣ 发起刷新请求(无感刷新)
const { access_token, refresh_token } = await instance.post('/auth/refresh',{
refresh_token: refreshToken
})
// 3️⃣ 更新本地 Store
useUserStore.setState({
accessToken: access_token,
refreshToken: refresh_token,
isLogin: true,
});
// 4️⃣ 🎉 喜大普奔:通知队列里的兄弟们,新 Token 来了!
requestsQueue.forEach((callback) => callback(access_token));
requestsQueue = []; // 清空队列
// 5️⃣ 别忘了自己:重试当前这个失败的请求
config.headers.Authorization = `Bearer ${access_token}`;
return instance(config);
}
} catch(err) {
// 😱 悲剧了:Refresh Token 也过期了,或者刷新失败
// 只能强制登出,跳转登录页
useUserStore.getState().logout();
window.location.href = '/login';
return Promise.reject(err);
} finally {
// 🔓 解锁:无论成功失败,刷新过程结束
isRefreshing = false;
}
}
return Promise.reject(err);
})
💡 重点总结
- 双 Token 机制:Access Token 负责日常业务,Refresh Token 负责续命。
- NestJS 后端:
verifyAsync验证旧 RT,generateTokens颁发新双 Token。 - Axios 拦截器:
- Request: 自动注入 Token。
- Response: 捕获 401,利用
isRefreshing锁和requestsQueue队列处理并发刷新,实现用户无感知的体验。
写代码就像讲故事,逻辑通了,代码自然就顺畅了。希望这篇硬核又轻松的教程能帮你彻底搞定 Token 刷新!如果不明白,欢迎在评论区提问哦!👇
Happy Coding! 💻✨