在小程序开发中,我们都知道小程序是没有cookie的,那么用户身份是如何确定的,后段颁发token,前端每次请求头部附带token。
既然是token,那么肯定有它的过期时间,没有一个token是永久的,永久的token就相当于一串永久的密码,是不安全的,
那么既然有刷新时间,问题就来了
1.前后端交互的过程中token如何存储?
2.token过期时,前端该怎么处理?
3.当用户正在操作时,遇到token过期该怎么办?直接跳回登陆页面?
token如何存储?
localStorage的大小约5M,兼容性在ie7及以上都兼容,有浏览器就可以,不需要在服务器的环境下运行, 会一直存在,除非手动清除 。
答案大致分为2种
存在 cookie
中
存在 localStorage
中
token过期时,前端该怎么处理?
1.第一种:跳回登陆页面重新登陆
2.第二种:拦截401重新获取token
class HttpClient { /** * Create a new instance of HttpClient. */ constructor() { this.interceptors = { request: [], response: [] }; } /** * Sends a single request to server. * * @param {Object} options - Coming soon. */ sendRequest(options) { let requestOptions = options; if (!requestOptions.header) { requestOptions.header = {}; } // 重新设置 Accept 和 Content-Type requestOptions.header = Object.assign( { Accept: 'application/json, text/plain, */*', 'Content-Type': 'application/json;charset=utf-8' }, requestOptions.header ); this.interceptors.request.forEach((interceptor) => { const request = interceptor(requestOptions); requestOptions = request.options; }); // 将以 Promise 返回数据, 无 success、fail、complete 参数 // let response = uni.request(requestOptions); // 使用Promise包装一下, 以 complete方式来接收接口调用结果 let response = new Promise((resolve, reject) => { requestOptions.complete = (res) => { const { statusCode } = res; const isSuccess = (statusCode >= 200 && statusCode < 300) || statusCode === 304; if(statusCode==401){ //拦截401请求 uni.reLaunch({ //关闭所有页面直接跳到登陆页面 url: '/pages/login/login' }); } if (isSuccess) { if(res.data.code==1){ resolve(res.data); }else{ reject(res); } } else { reject(res); } }; requestOptions.requestId = new Date().getTime(); uni.request(requestOptions); }); this.interceptors.response.forEach((interceptor) => { response = interceptor(response); }); return response; } } export default HttpClient;
这种方法适用于有登陆页面的小程序,但同样存在问题,假如用户在填写表单,填写完毕你却告诉我重新登陆,确定用户不会卸掉你的APP???
有人说了 异常退出 我会本地缓存填写的表单内容,当然你要是能接受这种我也无话可说!!!
我们要做的是无痛刷新toekn,那么首先要在401拦截的时候去重新登陆获取新的token
继续优化改造
import store from "../store/index.js"; class HttpClient { /** * Create a new instance of HttpClient. */ constructor() { this.interceptors = { request: [], response: [] }; } /** * Sends a single request to server. * * @param {Object} options - Coming soon. */ sendRequest(options) { let requestOptions = options; if (!requestOptions.header) { requestOptions.header = {}; } // 重新设置 Accept 和 Content-Type requestOptions.header = Object.assign( { Accept: 'application/json, text/plain, */*', 'Content-Type': 'application/json;charset=utf-8' }, requestOptions.header ); this.interceptors.request.forEach((interceptor) => { const request = interceptor(requestOptions); requestOptions = request.options; }); // 将以 Promise 返回数据, 无 success、fail、complete 参数 // let response = uni.request(requestOptions); // 使用Promise包装一下, 以 complete方式来接收接口调用结果 let response = new Promise((resolve, reject) => { requestOptions.complete = (res) => { const { statusCode } = res; const isSuccess = (statusCode >= 200 && statusCode < 300) || statusCode === 304; if(statusCode==401){ //拦截401请求 store .dispatch("auth/login") //调用vueX中的登陆将登陆信息保存到VueX .then(()=>{ //提示用户刚才操作无效重新操作一次 uni.showToast({ title: '请重新操作', duration: 2000, icon: "none", }); }) .catch(()=>{ uni.showToast({ title: '账户异常请重启程序', duration: 2000, icon: "none", }); }) } if (isSuccess) { if(res.data.code==1){ resolve(res.data); }else{ reject(res); } } else { reject(res); } }; requestOptions.requestId = new Date().getTime(); uni.request(requestOptions); }); this.interceptors.response.forEach((interceptor) => { response = interceptor(response); }); return response; } } export default HttpClient;
到此我们实现的在401错误时候去重新登陆获取新的token,且告知用户重新操作一次
到此你会发现一个问题,当页面存在一个请求,目前方案毫无问题,但是当存在两个、三个、四个请求,你会骂娘
失败3个请求会重新调用3次登陆会刷新3次token
那么此时要做的就是保证不多次登陆
思路加一个开关,当在登陆过程中后续错误不再走登陆接口
import store from "../store/index.js"; // 是否正在重新登陆刷新的标记 var loginRefreshing = false class HttpClient { /** * Create a new instance of HttpClient. */ constructor() { this.interceptors = { request: [], response: [] }; } /** * Sends a single request to server. * * @param {Object} options - Coming soon. */ sendRequest(options) { let requestOptions = options; if (!requestOptions.header) { requestOptions.header = {}; } // 重新设置 Accept 和 Content-Type requestOptions.header = Object.assign( { Accept: 'application/json, text/plain, */*', 'Content-Type': 'application/json;charset=utf-8' }, requestOptions.header ); this.interceptors.request.forEach((interceptor) => { const request = interceptor(requestOptions); requestOptions = request.options; }); // 将以 Promise 返回数据, 无 success、fail、complete 参数 // let response = uni.request(requestOptions); // 使用Promise包装一下, 以 complete方式来接收接口调用结果 let response = new Promise((resolve, reject) => { requestOptions.complete = (res) => { const { statusCode } = res; const isSuccess = (statusCode >= 200 && statusCode < 300) || statusCode === 304; if(statusCode==401){ //拦截401请求 if(!loginRefreshing){//防止重复登陆 loginRefreshing = true store .dispatch("auth/login") //调用vueX中的登陆将登陆信息保存到VueX .then(()=>{ //提示用户刚才操作无效重新操作一次 uni.showToast({ title: '请重新操作', duration: 2000, icon: "none", }); }) .catch(()=>{ uni.showToast({ title: '账户异常请重启程序', duration: 2000, icon: "none", }); }) .finally(()=>{ //销毁 是否正在重新登陆刷新的标记 loginRefreshing = false }); } } if (isSuccess) { if(res.data.code==1){ resolve(res.data); }else{ reject(res); } } else { reject(res); } }; requestOptions.requestId = new Date().getTime(); uni.request(requestOptions); }); this.interceptors.response.forEach((interceptor) => { response = interceptor(response); }); return response; } } export default HttpClient;
我们可以看到在遇到两个401错误时候并没有请求两次login,只请求一次,到此刷新token算是完成了,但是需要用户配合去重新操作一次,还不是真正的无痛刷线token,做到用户无感知
思路:将请求401的请求缓存起来,在重新登陆完成之后再将缓存中的请求重新发出,
废话不多说直接上代码
import AuthService from "@/services/auth.service"; import store from "../store/index.js"; // 是否正在重新登陆刷新的标记 var loginRefreshing = false // 重试队列,每一项将是一个待执行的函数形式 let requests = [] class HttpClient { /** * Create a new instance of HttpClient. */ constructor() { this.interceptors = { request: [], response: [] }; } /** * Sends a single request to server. * * @param {Object} options - Coming soon. */ sendRequest(options) { let requestOptions = options; if (!requestOptions.header) { requestOptions.header = {}; } // 重新设置 Accept 和 Content-Type requestOptions.header = Object.assign( { Accept: 'application/json, text/plain, */*', 'Content-Type': 'application/json;charset=utf-8' }, requestOptions.header ); this.interceptors.request.forEach((interceptor) => { const request = interceptor(requestOptions); requestOptions = request.options; }); // 将以 Promise 返回数据, 无 success、fail、complete 参数 // let response = uni.request(requestOptions); // 使用Promise包装一下, 以 complete方式来接收接口调用结果 let response = new Promise((resolve, reject) => { let timeId = setTimeout(()=>{ reject({statusCode:504}); },10000) requestOptions.complete = (res) => { clearTimeout(timeId) const { statusCode } = res; const isSuccess = (statusCode >= 200 && statusCode < 300) || statusCode === 304; if(statusCode==401){ //无痛刷新token if(!loginRefreshing){//防止重复登陆 loginRefreshing = true store.dispatch("auth/logout"); store .dispatch("auth/login") .then(()=>{ //所有存储到对列组中的请求重新执行。 requests.forEach(callback=>{ callback(AuthService.getToken() ? AuthService.getToken() : "") }) //重试队列清空 requests = [] }) .catch(()=>{ uni.showToast({ title: '账户异常请重启程序', duration: 2000, icon: "none", }); }) .finally(()=>{ //销毁 是否正在重新登陆刷新的标记 loginRefreshing = false }); } return new Promise((resolve) => { // 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行 requests.push((token) => { requestOptions.header.token = token //带着登陆后的新token resolve(uni.request(requestOptions)) }) }) } if (isSuccess) { if(res.data.code==1){ resolve(res.data); }else{ reject(res); } } else { reject(res); } }; requestOptions.requestId = new Date().getTime(); uni.request(requestOptions); }); this.interceptors.response.forEach((interceptor) => { response = interceptor(response); }); return response; } } export default HttpClient;
知识点,在重新获取新的token后要将缓存起来的请求中的token替换为重新登陆后新的token
到此无痛刷新token续接401请求的方法已经处理完毕,在用户提交表单时候遇到token失效重新获取新的token再续接表单请求,此时用户毫无感知,可能在请求时间上多了延迟,体验好感度+99,哈哈哈哈哈
到此无痛刷新token续接401已经完成请求快去试试吧
提示:有些后端开发在接口请求做了签名,记得像更换token一样在重新登陆完成之后更换新的时间戳新的签名等字段