在小程序開(kāi)發(fā)中,我們都知道小程序是沒(méi)有cookie的,那么用戶身份是如何確定的,后段頒發(fā)token,前端每次請(qǐng)求頭部附帶token。
既然是token,那么肯定有它的過(guò)期時(shí)間,沒(méi)有一個(gè)token是永久的,永久的token就相當(dāng)于一串永久的密碼,是不安全的,
那么既然有刷新時(shí)間,問(wèn)題就來(lái)了
1.前后端交互的過(guò)程中token如何存儲(chǔ)?
2.token過(guò)期時(shí),前端該怎么處理?
3.當(dāng)用戶正在操作時(shí),遇到token過(guò)期該怎么辦?直接跳回登陸頁(yè)面?
token如何存儲(chǔ)?
cookie的大小約4k,兼容性在ie6及以上 都兼容,在瀏覽器和服務(wù)器間來(lái)回傳遞,因此它得在服務(wù)器的環(huán)境下運(yùn)行,而且可以設(shè)定過(guò)期時(shí)間,默認(rèn)的過(guò)期時(shí)間是session會(huì)話結(jié)束。
localStorage的大小約5M,兼容性在ie7及以上都兼容,有瀏覽器就可以,不需要在服務(wù)器的環(huán)境下運(yùn)行, 會(huì)一直存在,除非手動(dòng)清除 。
答案大致分為2種
存在cookie中
存在localStorage中
token過(guò)期時(shí),前端該怎么處理?
1.第一種:跳回登陸頁(yè)面重新登陸
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 = {};
? ? }
? ? // 重新設(shè)置 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 返回?cái)?shù)據(jù), 無(wú) success、fail、complete 參數(shù)
? ? // let response = uni.request(requestOptions);
? ? // 使用Promise包裝一下, 以 complete方式來(lái)接收接口調(diào)用結(jié)果
? ? let response = new Promise((resolve, reject) => {
? ? ? requestOptions.complete = (res) => {
? ? ? ? const { statusCode } = res;
? ? ? ? const isSuccess = (statusCode >= 200 && statusCode < 300) || statusCode === 304;
? ? ? ? if(statusCode==401){ //攔截401請(qǐng)求
? ? ? ? ? uni.reLaunch({ //關(guān)閉所有頁(yè)面直接跳到登陸頁(yè)面
? ? ? ? ? ? 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;
這種方法適用余有登陸頁(yè)面的小程序,但同樣存在問(wèn)題,假如用戶在填寫表單,填寫完畢你卻告訴我重新登陸,確定用戶不回卸掉你的APP???
有人說(shuō)了? 異常退出 我會(huì)本地緩存填寫的表單內(nèi)容,當(dāng)然你要是能接受這種我也無(wú)話可說(shuō)!?。?/p>
我們要做的是無(wú)痛刷新toekn,那么首先y要在401攔截的時(shí)候去重新登陸獲取新的token
繼續(xù)優(yōu)化改造
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 = {};
? ? }
? ? // 重新設(shè)置 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 返回?cái)?shù)據(jù), 無(wú) success、fail、complete 參數(shù)
? ? // let response = uni.request(requestOptions);
? ? // 使用Promise包裝一下, 以 complete方式來(lái)接收接口調(diào)用結(jié)果
? ? let response = new Promise((resolve, reject) => {
? ? ? requestOptions.complete = (res) => {
? ? ? ? const { statusCode } = res;
? ? ? ? const isSuccess = (statusCode >= 200 && statusCode < 300) || statusCode === 304;
? ? ? ? if(statusCode==401){ //攔截401請(qǐng)求
? ? ? ? ? store
? ? ? ? ? ? .dispatch("auth/login")? ? //調(diào)用vueX中的登陸將登陸信息保存到VueX
? ? ? ? ? ? .then(()=>{ //提示用戶剛才操作無(wú)效重新操作一次
? ? ? ? ? ? ? uni.showToast({
? ? ? ? ? ? ? ? title: '請(qǐng)重新操作',
? ? ? ? ? ? ? ? duration: 2000,
? ? ? ? ? ? ? ? icon: "none",
? ? ? ? ? ? ? });
? ? ? ? ? ? })
? ? ? ? ? ? .catch(()=>{
? ? ? ? ? ? ? uni.showToast({
? ? ? ? ? ? ? ? title: '賬戶異常請(qǐng)重啟程序',
? ? ? ? ? ? ? ? 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;
到此我們實(shí)現(xiàn)的在401錯(cuò)誤時(shí)候去重新登陸獲取新的token,且告知用戶重新操作一次
到此你會(huì)發(fā)現(xiàn)一個(gè)問(wèn)題,當(dāng)頁(yè)面存在一個(gè)請(qǐng)求,目前方案毫無(wú)問(wèn)題,但是當(dāng)存在兩個(gè)、三個(gè)、四個(gè)請(qǐng)求,你會(huì)罵娘
失敗3個(gè)請(qǐng)求會(huì)重新調(diào)用3次登陸會(huì)刷新3次token
那么此時(shí)要做的就是保證不多次登陸
思路加一個(gè)開(kāi)關(guān),當(dāng)在登陸過(guò)程中后續(xù)錯(cuò)誤不再走登陸接口
import store from "../store/index.js";
// 是否正在重新登陸刷新的標(biāo)記
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 = {};
? ? }
? ? // 重新設(shè)置 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 返回?cái)?shù)據(jù), 無(wú) success、fail、complete 參數(shù)
? ? // let response = uni.request(requestOptions);
? ? // 使用Promise包裝一下, 以 complete方式來(lái)接收接口調(diào)用結(jié)果
? ? let response = new Promise((resolve, reject) => {
? ? ? requestOptions.complete = (res) => {
? ? ? ? const { statusCode } = res;
? ? ? ? const isSuccess = (statusCode >= 200 && statusCode < 300) || statusCode === 304;
? ? ? ? if(statusCode==401){ //攔截401請(qǐng)求
? ? ? ? ? if(!loginRefreshing){//防止重復(fù)登陸
? ? ? ? ? ? loginRefreshing = true
? ? ? ? ? ? store
? ? ? ? ? ? ? .dispatch("auth/login")? ? //調(diào)用vueX中的登陸將登陸信息保存到VueX
? ? ? ? ? ? ? .then(()=>{ //提示用戶剛才操作無(wú)效重新操作一次
? ? ? ? ? ? ? ? uni.showToast({
? ? ? ? ? ? ? ? ? title: '請(qǐng)重新操作',
? ? ? ? ? ? ? ? ? duration: 2000,
? ? ? ? ? ? ? ? ? icon: "none",
? ? ? ? ? ? ? ? });
? ? ? ? ? ? ? })
? ? ? ? ? ? ? .catch(()=>{
? ? ? ? ? ? ? ? uni.showToast({
? ? ? ? ? ? ? ? ? title: '賬戶異常請(qǐng)重啟程序',
? ? ? ? ? ? ? ? ? duration: 2000,
? ? ? ? ? ? ? ? ? icon: "none",
? ? ? ? ? ? ? ? });
? ? ? ? ? ? ? })
? ? ? ? ? ? ? .finally(()=>{
? ? ? ? ? ? ? ? //銷毀 是否正在重新登陸刷新的標(biāo)記
? ? ? ? ? ? ? ? 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;

我們可以看到在遇到兩個(gè)401錯(cuò)誤時(shí)候并沒(méi)有請(qǐng)求兩次login,只請(qǐng)求一次,到此刷新token算是完成了,但是需要用戶配合去重新操作一次,還不是真正的無(wú)痛刷線token,做到用戶無(wú)感知
思路:將請(qǐng)求401的請(qǐng)求緩存起來(lái),在重新登陸完成之后再將緩存中的請(qǐng)求重新發(fā)出,
廢話不多說(shuō)直接上代碼
import AuthService from "@/services/auth.service";
import store from "../store/index.js";
// 是否正在重新登陸刷新的標(biāo)記
var loginRefreshing = false
// 重試隊(duì)列,每一項(xiàng)將是一個(gè)待執(zhí)行的函數(shù)形式
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 = {};
? ? }
? ? // 重新設(shè)置 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 返回?cái)?shù)據(jù), 無(wú) success、fail、complete 參數(shù)
? ? // let response = uni.request(requestOptions);
? ? // 使用Promise包裝一下, 以 complete方式來(lái)接收接口調(diào)用結(jié)果
? ? 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){ //無(wú)痛刷新token
? ? ? ? ? if(!loginRefreshing){//防止重復(fù)登陸
? ? ? ? ? ? loginRefreshing = true
? ? ? ? ? ? store.dispatch("auth/logout");
? ? ? ? ? ? store
? ? ? ? ? ? ? .dispatch("auth/login")
? ? ? ? ? ? ? .then(()=>{
? ? ? ? ? ? ? ? //所有存儲(chǔ)到對(duì)列組中的請(qǐng)求重新執(zhí)行。
? ? ? ? ? ? ? ? requests.forEach(callback=>{
? ? ? ? ? ? ? ? ? callback(AuthService.getToken() ? AuthService.getToken() : "")
? ? ? ? ? ? ? ? })
? ? ? ? ? ? ? ? //重試隊(duì)列清空
? ? ? ? ? ? ? ? requests = []
? ? ? ? ? ? ? })
? ? ? ? ? ? ? .catch(()=>{
? ? ? ? ? ? ? ? uni.showToast({
? ? ? ? ? ? ? ? ? title: '賬戶異常請(qǐng)重啟程序',
? ? ? ? ? ? ? ? ? duration: 2000,
? ? ? ? ? ? ? ? ? icon: "none",
? ? ? ? ? ? ? ? });
? ? ? ? ? ? ? })
? ? ? ? ? ? ? .finally(()=>{
? ? ? ? ? ? ? ? //銷毀 是否正在重新登陸刷新的標(biāo)記
? ? ? ? ? ? ? ? loginRefreshing = false
? ? ? ? ? ? ? });
? ? ? ? ? }
? ? ? ? ? return new Promise((resolve) => {
? ? ? ? ? ? // 將resolve放進(jìn)隊(duì)列,用一個(gè)函數(shù)形式來(lái)保存,等token刷新后直接執(zhí)行
? ? ? ? ? ? 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;
知識(shí)點(diǎn),在重新獲取新的token后要將緩存起來(lái)的請(qǐng)求中的token替換為重新登陸后新的token
到此無(wú)痛刷新token續(xù)接401請(qǐng)求的方法已經(jīng)處理完畢,在用戶提交表單時(shí)候遇到token失效重新獲取新的token再續(xù)接表單請(qǐng)求,此時(shí)用戶毫無(wú)感知,可能在請(qǐng)求時(shí)間上多了延遲,體驗(yàn)好感度+99,哈哈哈哈哈? ? ?
到此無(wú)痛刷新token續(xù)接401已經(jīng)完成請(qǐng)求快去試試吧
提示:有些后段在接口請(qǐng)求做了簽名,記得像更換token一樣在重新登陸完成之后更換新的時(shí)間戳新的簽名等字段