无感知刷新TOKEN过期

无感知刷新TOKEN过期

关于TOKEN相关参数

  • access_token:获取需要授权的接口数据

  • expires_in: access_token过期时间

  • refresh_token: 刷新获取新的 access_token

实现刷新TOKEN方案

方法一

  • 在请求发起前拦截每个请求,判断token的有效时间是否已经过期,若已经过期,先刷新token,需要把保存失败的请求遍历重新请求,请求成功后进行重置。
  • 优点:节省请求、流量
  • 缺点:需要后端额外提供一个token过期时间的字段,如果使用本地时间判断,若本地时间被篡改,特别是本地时间比服务器时间慢时,拦截会失败。

方法二

  • 在拦截返回后的数据,先发起请求,接口返回过期后,先刷新token,需要把保存失败的请求遍历重新请求,请求成功后进行重置。
  • 优点:不需要额外的字段,不需要判断时间。
  • 缺点:耗请求、流量

axios处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
const request = axios.create({
baseURL: 'xxx'
// 配置选项
// timeout
})

function redirectLogin () {
router.push({
name: 'login',
query: {
redirect: router.currentRoute.fullPath
}
})
}

function refreshToken () {
return axios.create()({
method: 'POST',
url: '/refresh_token',
data: qs.stringify({
// refresh_token 只能使用1次
refreshtoken: store.state.user.refresh_token
})
})
}

// 请求拦截器
request.interceptors.request.use(function (config) {
// 我们就在这里通过改写 config 配置信息来实现业务功能的统一处理
const { user } = store.state
if (user && user.access_token) {
config.headers.Authorization = user.access_token
}

// 注意:这里一定要返回 config,否则请求就发不出去了
return config
}, function (error) {
// Do something with request error
return Promise.reject(error)
})

// 响应拦截器
let isRfreshing = false // 控制刷新 token 的状态
let requests: any[] = [] // 存储刷新 token 期间过来的 401 请求
request.interceptors.response.use(function (response) { // 状态码为 2xx 都会进入这里
// console.log('请求响应成功了 => ', response)
// 如果是自定义错误状态码,错误处理就写到这里
return response
}, async function (error) { // 超出 2xx 状态码都都执行这里
// 如果是使用的 HTTP 状态码,错误处理就写到这里
// console.dir(error)
if (error.response) { // 请求发出去收到响应了,但是状态码超出了 2xx 范围
const { status } = error.response
if (status === 400) {
Message.error('请求参数错误')
} else if (status === 401) {
// token 无效(没有提供 token、token 是无效的、token 过期了)
// 如果有 refresh_token 则尝试使用 refresh_token 获取新的 access_token
if (!store.state.user) {
redirectLogin()
return Promise.reject(error)
}

// 刷新 token
if (!isRfreshing) {
isRfreshing = true // 开启刷新状态
// 尝试刷新获取新的 token
return refreshToken().then(res => {
if (!res.data.success) {
throw new Error('刷新 Token 失败')
}

// 刷新 token 成功了
store.commit('setUser', res.data.content)
// 把 requests 队列中的请求重新发出去
requests.forEach(cb => cb())
// 重置 requests 数组
requests = []
return request(error.config)
}).catch(err => {
console.log(err)
Message.warning('登录已过期,请重新登录')
store.commit('setUser', null)
redirectLogin()
return Promise.reject(error)
}).finally(() => {
isRfreshing = false // 重置刷新状态
})
}

// 刷新状态下,把请求挂起放到 requests 数组中
return new Promise(resolve => {
requests.push(() => {
resolve(request(error.config))
})
})
} else if (status === 403) {
Message.error('没有权限,请联系管理员')
} else if (status === 404) {
Message.error('请求资源不存在')
} else if (status >= 500) {
Message.error('服务端错误,请联系管理员')
}
} else if (error.request) { // 请求发出去没有收到响应
Message.error('请求超时,请刷新重试')
} else { // 在设置请求时发生了一些事情,触发了一个错误
Message.error('请求失败')
}

// 把请求失败的错误对象继续抛出,扔给上一个调用者
return Promise.reject(error)
})

export default request