概述
点关注,不迷路!先点赞,后观看,养成阅读好习惯!你长得这么好看,点个赞不过分吧~
无论是在web端的项目,还是在移动端,H5项目的开发过程中,为了保证请求的合法与安全性,
经常会在请求时带上token(accessToken)。 那么,token是啥呢?
简单来说,token就是用户登录成功后,后端生成的一个字符串序列,
将这个序列返回给前端后,前端对其进行存储,并且在后续的所有请求中都必须携带上这个内容,
以验证用户身份的合法性及保证安全性。token分为accessToken和refreshToken。
accessToken是请求携带的内容,有效时间较短。
refreshToken是用来刷新accessToken的,有效时间较长
举个栗子:如果accessToken的有效期是一个小时,而refreshToken的有效期是七天。
那么在我们登录过后一个小时以内,我们去请求任何接口都没有问题(假设你拥有所有接口的权限)。
但是如果我们登录过后就将页面丢在那里了,过了一个小时过后才去请求其他的接口,
那么请求就会不成功,因为后端会检测到我们的accessToken已过期,请求不是合法有效的了,
就不会给我们返回内容,而是返回一个状态码,一般为401。
此时,你就需要用有效的refreshToken去重新置换一个有效的accessToken,
然后重新发起请求。如果不这样做呢?那用户只要使用你开发的应用,就必须每隔一个小时就登录一次。。。
我相信你也不想开发出这么垃圾的项目吧!
这样做的话,用户在refreshToken有效期内请求接口都是合法有效的, 提高了用户的体验。
当然,刷新的操作也可以由后端完成,这样后端只用返给你一个accessToken,用于验证用户身份状态就行了,
这就取决于你和后端谁能battle过谁了。。嘿嘿
手把手先带你实现一下axios的二次封装
有人可能会说axios已经很好用了,为什么还要对其进行二次封装?我一点都不否认axios的NB之处,但是试想一下,如果我们正在开发一个项目,网络请求采用axios实现,在我们项目开发完毕,即将上线时,axios团队突然解散了,那么axios也就不在维护了,碰巧这时axios出现了重大的bug,而你项目中的几百个甚至上千个接口全部采用axios直接发起的网络请求,这个时候再去重新换一个新的网络请求库,你想想有多爽!!!而对其进行二次封装后则不会出现这种状况了,这是我们只需把封装的axios库换成一个新的网络请求库就可以了,所有的请求均无需更改。啧啧啧啧啧
import axios from 'axios'
import { BASE_URL, TIMEOUT } from './config'
// 说明
// BASE_URL:你自己项目的baseurl
// TIMEOUT: 取决于你自己想设置默认值为多少,一般为6000
// 这里只对其进行简单封装,如果想实现更复杂的封装,
// 或者你自己的项目有其他的要求,可自行扩充
class Request {
constructor(baseURL, timeout = 6000) {
this.instance = axios.create({
baseURL,
timeout
})
}
request(config) {
return new Promise((resolve, reject) => {
this.instance.request(config).then(res => {
resolve(res)
}).catch(err => {
reject(err)
})
})
}
get(config) {
return this.request({ ...config, method: "get" })
}
post(config) {
return this.request({ ...config, method: "post" })
}
delete(config) {
return this.request({ ...config, method: "delete" })
}
put(config) {
return this.request({ ...config, method: "put" })
}
}
export default new Request(BASE_URL, TIMEOUT)
基于axios,在响应拦截器中实现无痛刷新token
import axios from 'axios'
import { BASE_URL, TIMEOUT } from './config'
// 说明
// BASE_URL:你自己项目的baseurl
// TIMEOUT: 取决于你自己想设置默认值为多少,一般为6000
// 这里只对其进行简单封装,如果想实现更复杂的封装,
// 或者你自己的项目有其他的要求,可自行扩充
class Request {
constructor(baseURL, timeout = 6000) {
this.instance = axios.create({
baseURL,
timeout
})
// 请求拦截
this.instance.interceptors.request.use(res => {
const accessToken = sessionStorage.getItem('accessToken')
if (accessToken) {
res.headers.authorization = accessToken
}
return res
}, err => {
console.log("请求失败")
return err
})
// 响应拦截
this.instance.interceptors.response.use(response => {
console.log(response)
const config = response.config
const code = response.data.code
switch(code) {
case 200:
break;
case 401:
const refreshToken = sessionStorage.getItem('refreshToken')
// 判断refreshToken是否有效,有效再去判断是否已经刷新,无效直接退出,重新登录
if(refreshToken) {
return axios.post({
// 仅为示例,需替换为你自己项目中刷新token的接口
url: baseURL + '/refreshToken',
headers: {
authorization: refreshToken
}
}).then(res => {
sessionStorage.removeItem('accessToken')
newAccessToken = 'Bearer ' + res.data.accessToken
sessionStorage.setItem('accessToken', newAccessToken)
// 刷新过后重新发起请求
axios.request(config)
})
} else {
// 提示登录过期,重新登录
}
}
return response
}, err => {
console.log(err);
console.log("响应失败")
return err
})
}
request(config) {
return new Promise((resolve, reject) => {
this.instance.request(config).then(res => {
resolve(res)
}).catch(err => {
reject(err)
})
})
}
get(config) {
return this.request({ ...config, method: "get" })
}
post(config) {
return this.request({ ...config, method: "post" })
}
delete(config) {
return this.request({ ...config, method: "delete" })
}
put(config) {
return this.request({ ...config, method: "put" })
}
}
export default new Request(BASE_URL, TIMEOUT)
上面的代码有一些问题就是,accessToken过期了,如果同时发起的请求有多个,那么多个请求都会去刷新token,造成资源的浪费,同时增加了服务器的压力,因为可以设置一个标志位来优化一下。
优化1:防止重复刷新
import axios from 'axios'
import { BASE_URL, TIMEOUT } from './config'
// 说明
// BASE_URL:你自己项目的baseurl
// TIMEOUT: 取决于你自己想设置默认值为多少,一般为6000
// 这里只对其进行简单封装,如果想实现更复杂的封装,
// 或者你自己的项目有其他的要求,可自行扩充
class Request {
constructor(baseURL, timeout = 6000) {
this.instance = axios.create({
baseURL,
timeout
})
// 请求拦截
this.instance.interceptors.request.use(res => {
const accessToken = sessionStorage.getItem('accessToken')
if (accessToken) {
res.headers.authorization = accessToken
}
return res
}, err => {
console.log("请求失败")
return err
})
/* 用来判断是否需要刷新token以及防止重复刷新token
因为如果一次发起了多个请求,但是此时token已过期,需要刷新
第一个请求将会去刷新token,但是刷新token也需要时间
如果没有一个变量用来控制的话,其他的请求也会去再次刷新token,造成资源浪费
*/
let isRefreshing = false
let newAccessToken = ''
// 响应拦截
this.instance.interceptors.response.use(response => {
console.log(response)
const config = response.config
const code = response.data.code
switch(code) {
case 200:
break;
case 401:
const refreshToken = sessionStorage.getItem('refreshToken')
// 判断refreshToken是否有效,有效再去判断是否已经刷新,无效直接退出,重新登录
if(refreshToken) {
// 判断是否正在刷新token,如果没在刷新就去刷新
if(!isRefreshing) {
isRefreshing = true
return axios.post({
// 仅为示例,需替换为你自己项目中刷新token的接口
url: baseURL + '/refreshToken',
headers: {
authorization: refreshToken
}
}).then(res => {
sessionStorage.removeItem('accessToken')
newAccessToken = 'Bearer ' + res.data.accessToken
sessionStorage.setItem('accessToken', newAccessToken)
// 刷新成功后重新发起请求
axios.request(config)
}).finally(() => {
// 刷新token后将标志位置为false
isRefreshing = false
})
}
} else {
// 提示登录过期,重新登录
router.replace('/login')
}
}
return response
}, err => {
console.log(err);
console.log("响应失败")
return err
})
}
request(config) {
return new Promise((resolve, reject) => {
this.instance.request(config).then(res => {
resolve(res)
}).catch(err => {
reject(err)
})
})
}
get(config) {
return this.request({ ...config, method: "get" })
}
post(config) {
return this.request({ ...config, method: "post" })
}
delete(config) {
return this.request({ ...config, method: "delete" })
}
put(config) {
return this.request({ ...config, method: "put" })
}
}
export default new Request(BASE_URL, TIMEOUT)
上面代码还有一个问题就是,重新刷新accessToken过后,负责刷新的那个接口会重新发起请求,而同时打进来的其他请求就不会再次发起请求了。因此,需要用一个重试队列,保存发起的请求,等待token刷新后,再依次重新发起请求。
优化2:保证刷新后不丢失请求
```javascript
import axios from 'axios'
import { BASE_URL, TIMEOUT } from './config'
// 说明
// BASE_URL:你自己项目的baseurl
// TIMEOUT: 取决于你自己想设置默认值为多少,一般为6000
// 这里只对其进行简单封装,如果想实现更复杂的封装,
// 或者你自己的项目有其他的要求,可自行扩充
class Request {
constructor(baseURL, timeout = 6000) {
this.instance = axios.create({
baseURL,
timeout
})
// 请求拦截
this.instance.interceptors.request.use(res => {
const accessToken = sessionStorage.getItem('accessToken')
if (accessToken) {
res.headers.authorization = accessToken
}
return res
}, err => {
console.log("请求失败")
return err
})
/* 用来判断是否需要刷新token以及防止重复刷新token
因为如果一次发起了多个请求,但是此时token已过期,需要刷新
第一个请求将会去刷新token,但是刷新token也需要时间
如果没有一个变量用来控制的话,其他的请求也会去再次刷新token,造成资源浪费
*/
let isRefreshing = false
/* 需要重新发起请求的队列,每一项将是一个待执行的函数形式
如果一次发起了多个请求,但是此时token已过期,需要刷新
刷新过后,还要重新发起请求,所以需要有一个队列来存储
*/
let requests = []
let newAccessToken = ''
// 响应拦截
this.instance.interceptors.response.use(response => {
console.log(response)
const config = response.config
const code = response.data.code
switch(code) {
case 200:
break;
case 401:
const refreshToken = sessionStorage.getItem('refreshToken')
// 判断refreshToken是否有效,有效再去判断是否已经刷新,无效直接退出,重新登录
if(refreshToken) {
// 判断是否正在刷新token,如果没在刷新就去刷新
if(!isRefreshing) {
isRefreshing = true
return axios.post({
// 仅为示例,需替换为你自己项目中刷新token的接口
url: baseURL + '/refreshToken',
headers: {
authorization: refreshToken
}
}).then(res => {
sessionStorage.removeItem('accessToken')
newAccessToken = 'Bearer ' + res.data.accessToken
sessionStorage.setItem('accessToken', newAccessToken)
axios.request(config)
// 刷新成功后将请求队列里的请求依次执行
requests.forEach(cb => cb(newAccessToken))
requests = [] // 执行完毕后将请求队列置空
}).finally(() => {
// 刷新token后将标志位置为false
isRefreshing = false
})
}
// 已经在刷新了,就不用再刷新了。
// 同时将发起的其他请求存储起来,等待刷新成功后重新发起请求
else {
return new Promise(resolve => {
requests.push(token => {
config.headers.authorization = token
resolve(axios.request(config))
})
})
}
} else {
// 提示登录过期,重新登录
}
}
return response
}, err => {
console.log(err);
console.log("响应失败")
return err
})
}
request(config) {
return new Promise((resolve, reject) => {
this.instance.request(config).then(res => {
resolve(res)
}).catch(err => {
reject(err)
})
})
}
get(config) {
return this.request({ ...config, method: "get" })
}
post(config) {
return this.request({ ...config, method: "post" })
}
delete(config) {
return this.request({ ...config, method: "delete" })
}
put(config) {
return this.request({ ...config, method: "put" })
}
}
export default new Request(BASE_URL, TIMEOUT)
手把手带你实现uniapp中的无痛刷新token
在uniapp中开发项目,然后部署到微信小程序,是不能直接使用axios的。因为axios 的底层是依赖于 XMLHttpRequest 函数来发起请求的,但是微信小程序禁用了 XMLHttpRequest 函数,仅开放了wx.request 来发起请求,所以说axios是没法直接在为微信小程序上面使用的,可以直接使用uniapp 官方封装的uni.request来发起请求。但是uni.request有个问题就是,它没有拦截器,那我们刷新token不就成了一个大麻烦了吗?别怕,这不是有我吗,来,手把手带你实现。uniapp官方的插件市场有一个不错的基于Promise,使用条件编译开发,专用于uni-app项目的网络请求库luch-request,我们基于这个库,就可以实现无痛刷新token了。思想代码逻辑都是一样的,这里简单实现一下:
import Request from './luch-request/luch-request/index.js'
export const apiBaseUrl = 'http://xxx/'
const request= new Request()
request.setConfig((config) => { /* 设置全局配置 */
config.header = {
...config.header,
}
return config
})
// 请求拦截
request.interceptors.request.use((config) => { // 可使用async await 做异步操作
config.baseURL = apiBaseUrl
config.header = {
...config.header,
Authorization: uni.getStorageSync('accessToken'),
}
return config
}, err => {
return Promise.reject(err)
})
// 防止重复刷新标志位
let isRefreshing = false
// 重试队列,每一项将是一个待执行的函数形式
let requests = []
// 响应拦截
request.interceptors.response.use((response) => {
const accessToken = uni.getStorageSync('accessToken')
const refreshToken = uni.getStorageSync('refreshToken')
if (response) {
switch (response.statusCode) {
case 200:
break
case 401:
const config = response.config
let newToken;
if(!isRefreshing) {
isRefreshing = true
return uni.request({
url: 'https://xxx/refreshToken',
method: 'POST',
header: {
Authorization: refreshToken
}
}).then(res => {
uni.removeStorageSync('accessToken')
newToken = uni.setStorageSync('accessToken', 'Bearer ' + res.data.accessToken)
return request.request(config)
requests.forEach(cb => cb(newToken))
requests = []
}).finally(() => {
isRefreshing = false
})
}
else{
return new Promise((resolve) => {
requests.push((newToken) => {
config.header.Authorization = newToken
resolve(http.request(config))
})
})
}
return Promise.reject(response)
default:
break
}
}
return response
}, err => {
return Promise.reject(err)
})
export default request