一、refresh_token和token的作用
当用户登陆成功之后,返回的 token 中有两个值,不知道你见过没有?
token:
- 作用:一般情况下,在访问接口时,需要传入的token值就是它。
- 有效期:2小时(安全)。
refresh_token
- 作用: 当token的有效期过了之后,可以使用refresh_token去请求一个特殊接口(这个接口也是后端指定的,明确需要传入refresh_token),并返回一个新的token回来(有效期还是2小时),以替换过期的那个token。
- 有效期:14天。(最理想的情况下,一次登陆可以持续14天。)
二、让用户对因token过期产生的401错误无感知
request的响应拦截器中:
-
对于某次请求A,如果是401错误 (2)
-
有refresh_token,用refresh_token去请求回新的token (3)
-
新token请求成功 (4)
- 更新本地token (5)
- 再发一次请求A (6)
-
新token请求失败
- 携带请求地址,跳转到登陆页
-
-
没有refresh_token,说明没有登陆
- 携带请求地址,跳转到登陆页
-
三、代码落地
1. 封装独立的history
创建文件 src\utils\history.ts
import { createBrowserHistory } from 'history'
const history = createBrowserHistory()
export default history
2. 在app.tsx中使用
import { Router, Redirect, Route } from 'react-router-dom'
import history from '@/utils/history'
function App () {
return (
<Router history={history}>
<div className="app">
......省略其他
</div>
</Router>
)
}
export default App
3. 补充action
在 actions/login.ts 中,补充一个 action 用来去做更新 token 的操作
export function saveToken (token: Token) : LoginAction {
// 1. 本地持久化
setToken(token)
// 2. 保存token
return {
type: 'login/token',
payload: token
}
}
4. 核心代码
在request.ts中,添加响应拦截器
/* eslint-disable camelcase */
// 封装axios
import axios, { AxiosError } from 'axios'
import { getToken, setToken } from './storage'
import history from '@/utils/history'
import store from '@/store'
const baseURL = '×××'
const instance = axios.create({
baseURL,
timeout: 5000
})
// ◆ 添加请求拦截器
instance.interceptors.request.use(
function (config) {
// 在发送请求之前做些什么
const token = getToken().token
if (token) {
// 法1:非空断言
config.headers!.Authorization = `Bearer ${token}`
// 法2:类型收窄
// if (config.headers) {
// config.headers.Authorization = `Bearer ${token}`
// }
}
return config
},
function (error) {
// 对请求错误做些什么
return Promise.reject(error)
}
)
// ◆ 添加响应拦截器
instance.interceptors.response.use(
function (response) {
// 对响应数据做点什么
return response
},
async function (error) {
// test
// console.log(1)
// console.log(error.response, 'response')
const err = error as AxiosError
// console.dir(err, 'error')
if (err.response?.status === 401) {
// eslint-disable-next-line no-unused-vars
const { refresh_token } = getToken()
// 判断是否有 token
if (!refresh_token) {
history.push('/login', { from: history.location.pathname })
return Promise.reject(error)
}
try {
// 获取新的 token
const res = await axios.put(baseURL + 'authorizations', null, {
headers: {
Authorization: `Bearer ${refresh_token}`
}
})
// 保存新的 token
const newtokenInfo = { token: res.data.data.token, refresh_token }
// 存本地
setToken(newtokenInfo)
console.log(res, 'refresh_token')
// 存redux
store.dispatch({
type: 'login/token',
payload: newtokenInfo
})
// 重发请求
return instance(error.config)
} catch (err) {
console.log(err)
history.push({
pathname: '/login',
state: { from: history.location.pathname }
})
return Promise.reject(error)
}
}
// 对响应错误做点什么
return Promise.reject(error)
}
)
export default instance
utils/storage.ts
import { TokenType } from '@/types/data'
// ◆封装本地存储的操作
const TOKEN_KEY = 'lalala_linwanxia'
export function getToken ():TokenType {
return JSON.parse(localStorage.getItem(TOKEN_KEY) || '{}')
}
export function setToken (token:TokenType) {
localStorage.setItem(TOKEN_KEY, JSON.stringify(token))
}
export function removeToken ():void {
localStorage.removeItem(TOKEN_KEY)
}
export function hasToken ():boolean {
return !!getToken()
}
src/types/data.ts
export type TokenType={
token: string,
refresh_token: string
}
注意:
- 响应拦截器要加在 axios 实例 request 上。
- 用 refresh_token 请求新 token 时,要用 axios ,不要用实例 request
- 得到新 token 之后,再发请求时,要用 request 实例