Switch language
zh
Switch theme
Light

access_token 和 refresh_token 的使用

在 Web 开发中,使用 access_token 和 refresh_token 的双 token 机制是为了在安全性与用户体验之间取得平衡。以下是它们各自的作用以及为什么需要使用双 token:

access_token 的作用

  • 访问受保护资源:access_token 是用于直接访问受保护资源的令牌。它通常包含用户的身份信息和权限范围。
  • 短生命周期:access_token 的有效期较短,通常为几分钟到几小时。这样做的目的是减少令牌被泄露后可能造成的损害,因为攻击者只有很短的时间窗口可以利用这个令牌。

refresh_token 的作用

  • 刷新 access_token:当 access_token 过期时,refresh_token 用于获取新的 access_token。它不需要用户重新登录,从而提高了用户体验。
  • 长生命周期:refresh_token 的有效期较长,通常为几天到几周。它通常存储在客户端的安全存储中,如加密的本地存储。
  • 安全存储:由于 refresh_token 的有效期较长,它通常不会频繁地在网络中传输,从而降低了被盗的风险。

为什么要使用双 token

  • 提高安全性:通过将 access_token 和 refresh_token 分开使用,即使 access_token 被泄露,攻击者也无法长时间利用它。而 refresh_token 由于其长生命周期和安全存储,即使被盗,服务器也可以随时将其标记为无效。
  • 优化用户体验:用户不需要频繁重新登录。当 access_token 过期时,系统可以自动使用 refresh_token 获取新的 access_token,用户几乎感觉不到任何中断。
  • 简化 Token 管理:双 token 机制使得 Token 的管理更加灵活,可以根据不同的安全需求设置不同的有效期。

实际 axios 使用时, 使用 axios 的 interceptor 拦截 access_token 失效的请求, 通过 refresh_token 无感刷新 access_token 后, 再重放 之前失败的请求

import axios from 'axios'

const instance = axios.create({
  baseURL: '/',
  timeout: 10000,
  validateStatus: (status) => {
    return status < 500
  },
})

instance.interceptors.request.use((config) => {
  const token = localStorage.getItem('access_token')
  if (token) {
    config.headers.Authorization = token
  }

  return config
})

instance.interceptors.response.use(
  async (response) => {
    if (response.status === 401) {
      // 刷新 token
      if (await refreshToken()) {
        // 重放请求
        return await instance.request(response.config)
      } else {
        window.location.href = '/login'
      }
    } else if (response.status === 403) {
      localStorage.removeItem('access_token')
      localStorage.removeItem('refresh_token')
      window.location.href = '/login'

      return Promise.reject('error to refresh token')
    }

    return response
  },
  (error) => {
    return Promise.reject(error)
  },
)

let promise: Promise<boolean> | null //Promise
async function refreshToken() {
  if (promise) {
    return promise
  }
  promise = new Promise(async (resolve) => {
    const { status, data } = await axios.put('/api/refresh-token', {
      refresh_token: localStorage.getItem('refresh_token'),
    }, {
      validateStatus: (status) => status < 500,
    })

    if (status !== 200) {
      localStorage.removeItem('access_token')
      localStorage.removeItem('refresh_token')
      resolve(false)
    } else {
      const { access_token, refresh_token } = data
      localStorage.setItem('access_token', JSON.stringify(access_token))
      localStorage.setItem('refresh_token', JSON.stringify(refresh_token))
      resolve(true)
    }
  })

  promise.finally(() => {
    promise = null
  })

  return promise
}

export {
  instance,
}

参考掘金:聊聊单点登录(SSO)
该文中提到双 token 中的 refresh_token 用于服务端控制访问权限(退出登录),但实际应用中,refresh_token 的作用是刷新 access_token,而不是控制访问权限; 并且后端存储 access_token 也能控制 access_token 的失效。

代码见github: demo-refresh-token-202501

🍀