双 token 技术

690 阅读2分钟

一、refresh_token和token的作用

当用户登陆成功之后,返回的 token 中有两个值,不知道你见过没有?

image.png

token:

  • 作用:一般情况下,在访问接口时,需要传入的token值就是它。
  • 有效期:2小时(安全)。

refresh_token

  • 作用: 当token的有效期过了之后,可以使用refresh_token去请求一个特殊接口(这个接口也是后端指定的,明确需要传入refresh_token),并返回一个新的token回来(有效期还是2小时),以替换过期的那个token。
  • 有效期:14天。(最理想的情况下,一次登陆可以持续14天。)

二、让用户对因token过期产生的401错误无感知

image.png

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
  }

注意:

  1. 响应拦截器要加在 axios 实例 request 上。
  2. 用 refresh_token 请求新 token 时,要用 axios ,不要用实例 request
  3. 得到新 token 之后,再发请求时,要用 request 实例