在小程序开发中,我们都知道小程序是没有cookie的,那么用户身份是如何确定的,后段颁发token,前端每次请求头部附带token。
既然是token,那么肯定有它的过期时间,没有一个token是永久的,永久的token就相当于一串永久的密码,是不安全的,
那么既然有刷新时间,问题就来了
1.前后端交互的过程中token如何存储?
2.token过期时,前端该怎么处理?
3.当用户正在操作时,遇到token过期该怎么办?直接跳回登陆页面?
token如何存储?
cookie的大小约4k,兼容性在ie6及以上 都兼容,在浏览器和服务器间来回传递,因此它得在服务器的环境下运行,而且可以设定过期时间,默认的过期时间是session会话结束。
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](/user/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;
这种方法适用余有登陆页面的小程序,但同样存在问题,假如用户在填写表单,填写完毕你却告诉我重新登陆,确定用户不会删掉你的小程序???
有人说了 异常退出 我会本地缓存填写的表单内容,当然你要是能接受这种我也无话可说!!!
我们要做的是无痛刷新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](/user/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](/user/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](/user/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一样在重新登陆完成之后更换新的时间戳新的签名等字段
附上本请求封装的uniapp基础性脚手架 https://github.com/wkiwi/uni-app-base