登录获取 Token 和刷新 Token 是两个配合使用的接口,下面是完整的使用流程和代码实现。
一、两个接口的作用
| 接口类型 | 使用时机 | 返回内容 | 有效期 |
|---|---|---|---|
| 登录接口 | 用户首次登录 | accessToken + refreshToken | accessToken 短期(如30分钟) refreshToken 长期(如7天) |
| 刷新Token接口 | accessToken 过期时 | 新的 accessToken + 新的 refreshToken(可选) | 重新计时 |
二、完整流程图
用户登录 ↓ 调用登录接口 → 获取 accessToken + refreshToken ↓ 存储到本地 ↓ 正常请求业务接口(携带 accessToken) ↓ ┌─────────────────┐ │ 接口返回401过期?│ └─────────────────┘ ↓ 是 调用刷新Token接口(携带 refreshToken) ↓ 获取新的 accessToken ↓ 更新本地存储 ↓ 重试刚才失败的请求 ↓ 继续正常请求...三、完整代码实现
1. 定义 API 接口
// api/auth.jsconstBASE_URL='https://your-api.com';exportdefault{// 登录接口login(data){returnuni.request({url:`${BASE_URL}/login`,method:'POST',data:data});},// 刷新Token接口refreshToken(refreshToken){returnuni.request({url:`${BASE_URL}/refresh`,method:'POST',data:{refreshToken:refreshToken}});}};2. 登录逻辑
// pages/login/login.vueexportdefault{data(){return{username:'',password:''}},methods:{asynchandleLogin(){try{// 1. 调用登录接口constres=awaitthis.$api.login({username:this.username,password:this.password});if(res.code===0){const{accessToken,refreshToken,expiresIn,tokenType}=res.data;// 2. 存储Tokenthis.saveToken({accessToken,refreshToken,expiresIn,// 有效期时长(毫秒)tokenType// Bearer 或 null});// 3. 跳转到首页uni.switchTab({url:'/pages/index/index'});}else{uni.showToast({title:res.msg||'登录失败',icon:'none'});}}catch(error){console.error('登录失败',error);}},// 存储TokensaveToken(tokenData){constnow=Date.now();// 存储基础信息uni.setStorageSync('accessToken',tokenData.accessToken);uni.setStorageSync('refreshToken',tokenData.refreshToken);uni.setStorageSync('tokenType',tokenData.tokenType||'');// 计算过期时间(绝对时间戳)if(tokenData.expiresIn){uni.setStorageSync('expiresTime',now+tokenData.expiresIn);}elseif(tokenData.expiresTime){uni.setStorageSync('expiresTime',tokenData.expiresTime);}}}};3. 刷新Token逻辑
// utils/refreshToken.js// 防止多个请求同时刷新Token(锁机制)letisRefreshing=false;letrefreshSubscribers=[];// 待刷新期间缓存的请求functionsubscribeTokenRefresh(callback){refreshSubscribers.push(callback);}functiononTokenRefreshed(newToken){refreshSubscribers.forEach(callback=>callback(newToken));refreshSubscribers=[];}// 刷新TokenasyncfunctionrefreshAccessToken(){try{constrefreshToken=uni.getStorageSync('refreshToken');if(!refreshToken){// 没有refreshToken,跳转登录redirectToLogin();returnnull;}// 调用刷新接口constres=awaituni.request({url:'https://your-api.com/refresh',method:'POST',data:{refreshToken:refreshToken}});if(res.statusCode===200&&res.data.code===0){const{accessToken,refreshToken:newRefreshToken,expiresIn}=res.data.data;// 更新存储uni.setStorageSync('accessToken',accessToken);uni.setStorageSync('expiresTime',Date.now()+expiresIn);// 如果返回了新的refreshToken(通常刷新接口也会返回新的)if(newRefreshToken){uni.setStorageSync('refreshToken',newRefreshToken);}// 通知所有等待的请求onTokenRefreshed(accessToken);returnaccessToken;}else{// 刷新失败,清空Token并跳转登录clearTokenAndRedirect();returnnull;}}catch(error){console.error('刷新Token失败',error);clearTokenAndRedirect();returnnull;}finally{isRefreshing=false;}}// 跳转登录页functionredirectToLogin(){clearToken();uni.reLaunch({url:'/pages/login/login'});}// 清空TokenfunctionclearToken(){uni.removeStorageSync('accessToken');uni.removeStorageSync('refreshToken');uni.removeStorageSync('expiresTime');uni.removeStorageSync('tokenType');}// 导出刷新方法export{refreshAccessToken,clearToken,redirectToLogin};4. 请求拦截器(自动处理Token过期)
// utils/request.jsimport{refreshAccessToken,redirectToLogin}from'./refreshToken.js';// 请求队列(存放过期期间等待的请求)letrequestQueue=[];letisRefreshing=false;// 发起请求的核心方法functionrequest(config){returnnewPromise((resolve,reject)=>{// 1. 检查Token是否过期constexpiresTime=uni.getStorageSync('expiresTime');constnow=Date.now();// 判断是否需要刷新Tokenif(expiresTime&&now>=expiresTime){// Token已过期,需要刷新if(!isRefreshing){isRefreshing=true;// 刷新TokenrefreshAccessToken().then(newToken=>{if(newToken){// 刷新成功,执行队列中的请求requestQueue.forEach(cb=>cb(newToken));requestQueue=[];// 重新发起当前请求doRequest(config).then(resolve).catch(reject);}else{reject({message:'Token刷新失败'});}isRefreshing=false;}).catch(()=>{isRefreshing=false;reject({message:'Token刷新失败'});});}// 将当前请求加入队列,等刷新完成后重试requestQueue.push((newToken)=>{// 更新config中的tokenconfig.header=getAuthHeader(newToken);doRequest(config).then(resolve).catch(reject);});}else{// Token未过期,直接发起请求config.header=getAuthHeader();doRequest(config).then(resolve).catch(reject);}});}// 实际发起请求functiondoRequest(config){returnnewPromise((resolve,reject)=>{uni.request({url:config.url,method:config.method||'GET',data:config.data,header:config.header,success:(res)=>{// 处理业务错误(如Token无效)if(res.data&&res.data.code===401){// Token无效,强制刷新refreshAccessToken().then(()=>{// 重试请求request(config).then(resolve).catch(reject);}).catch(()=>{redirectToLogin();reject({message:'登录已过期'});});}else{resolve(res);}},fail:(err)=>{reject(err);}});});}// 获取认证请求头functiongetAuthHeader(customToken=null){consttoken=customToken||uni.getStorageSync('accessToken');consttokenType=uni.getStorageSync('tokenType');if(tokenType==='Bearer'){return{'Authorization':`Bearer${token}`};}else{return{'accessToken':token};}}exportdefaultrequest;5. 实际使用示例
// 在 main.js 中全局挂载importrequestfrom'@/utils/request.js';Vue.prototype.$request=request;// 在页面中使用// pages/index/index.vueexportdefault{asyncmounted(){try{// 自动处理Token过期和刷新constres=awaitthis.$request({url:'https://your-api.com/user/info',method:'GET'});console.log('用户信息:',res.data);}catch(error){console.error('请求失败:',error);if(error.message==='登录已过期'){// 跳转到登录页uni.navigateTo({url:'/pages/login/login'});}}}};四、关键点总结
1. 刷新策略
- 主动刷新:每次请求前检查,剩余时间 < 5分钟就提前刷新
- 被动刷新:请求返回401时触发刷新
2. 并发控制(重要)
// 避免短时间内多次刷新TokenletisRefreshing=false;// 锁标志letrequestQueue=[];// 等待队列3. 刷新时机
// 在请求拦截器中判断constexpiresTime=uni.getStorageSync('expiresTime');consttimeRemaining=expiresTime-Date.now();if(timeRemaining<5*60*1000){// 剩余不足5分钟awaitrefreshAccessToken();// 提前刷新}4. Token失效后处理
// 刷新接口也返回401时,清空所有Token并跳转登录if(res.statusCode===401){uni.removeStorageSync('accessToken');uni.removeStorageSync('refreshToken');uni.reLaunch({url:'/pages/login/login'});}五、注意事项
- refreshToken 也有有效期,通常比 accessToken 长(如7天、30天),过期后需要重新登录
- 刷新接口也要做防抖,避免短时间内多次调用
- 存储到本地时建议加密(特别是 refreshToken)
- 登出时要同时清空accessToken 和 refreshToken
- 刷新Token接口建议使用 POST 请求,并将 refreshToken 放在 Body 中
按照以上方案实现,就能完美处理 Token 的获取和自动刷新了。