Access Token与Refresh Token

person c猿人    watch_later 2024-09-18 21:20:48
visibility 199    class Refresh Token    bookmark 分享

在前后端分离的项目中,Token 认证通常采用 JWT(JSON Web Token)机制来实现用户的身份认证。为了确保用户体验良好、安全性高,并且避免用户频繁登录,需要合理设计 Token 的时效及更新机制

常见 Token 方案及时效设计

前后端分离的项目通常会使用两个 Token:

  • 访问 Token(Access Token):用于用户的每次请求,通常有效时间较短,确保安全。
  • 刷新 Token(Refresh Token):用于刷新访问 Token,通常有效时间较长,避免用户频繁登录。

1. 访问 Token(Access Token)

  • 有效期:一般为 15 分钟到 1 小时之间。过期后不可再使用,需要通过刷新 Token 获取新的访问 Token。
  • 用途:每次请求时在 Authorization 头部携带访问 Token 进行身份认证。
  • 存储位置:通常保存在 浏览器的内存前端状态管理工具(如 Vuex, Redux) 中,避免长期保存带来的安全问题。

2. 刷新 Token(Refresh Token)

  • 有效期:通常为几天或几周,具体根据业务需要决定。
  • 用途:当访问 Token 过期时,通过刷新 Token 获取新的访问 Token,避免用户重新登录。
  • 存储位置:可以存放在 HTTP-Only Cookie 中,这样能减少 XSS 攻击的风险。

实现方案

Token 时效机制的具体实现流程:

  1. 用户登录时生成两个 Token

    • 用户通过用户名和密码进行登录,服务器验证成功后,生成一个短期有效的 访问 Token 和一个长期有效的 刷新 Token,并将它们返回给客户端。
  2. 客户端存储 Token

    • 访问 Token 存储在内存或短期存储(如 Vuex),有效期内随每个请求通过 Authorization 头部传递给后端。
    • 刷新 Token 存储在 HTTP-Only Cookie 中,浏览器不能直接访问,增加安全性。
  3. 使用访问 Token 进行身份验证

    • 每次前端请求时,前端从内存中提取访问 Token,并将其放入请求头部 Authorization: Bearer <token> 中发送给后端。
    • 后端解密 Token,验证 Token 的有效性。
  4. 访问 Token 过期时

    • 当后端发现访问 Token 过期时,返回 401 错误码。
    • 前端捕获到 401 错误时,自动携带刷新 Token 向服务器发送请求,获取新的访问 Token。
  5. 刷新 Token 更新访问 Token

    • 后端接收到前端传来的刷新 Token,并验证其有效性。
    • 若刷新 Token 有效,后端重新生成一个新的访问 Token 返回给前端。
    • 前端更新内存中的访问 Token,继续进行后续操作。
  6. 刷新 Token 过期处理

    • 如果刷新 Token 也过期,则向前端返回 401 错误,并要求用户重新登录。
    • 前端跳转到登录页面,提示用户重新进行身份验证。

Token 刷新机制的时序图

+-------------+            +-------------+            +--------------------+
|  Client     |            |  Backend    |            | Authentication Flow |
+-------------+            +-------------+            +--------------------+
      |                          |                           |
      | Login Request             |                           |
      | ------------------------> |                           |
      |                          | Verify User                |
      |                          | ------------------------>  |
      |                          |                           |
      |                          | Return Access & Refresh    |
      | <------------------------| Token                      |
      |                          |                           |
      | Store Access Token in     |                           |
      | memory, Refresh Token     |                           |
      | in HTTP-Only Cookie       |                           |
      |                          |                           |
      | Send API Requests with    |                           |
      | Access Token              |                           |
      | ------------------------> | Verify Access Token       |
      |                          |                           |
      |                          |                           |
      |                          | [Access Token Expired]     |
      |                          | <-------------------------|
      |                          |                           |
      | [Request New Access       |                           |
      | Token using Refresh Token]|                           |
      | ------------------------> | Verify Refresh Token      |
      |                          |                           |
      |                          | Generate New Access Token  |
      |                          | <-------------------------|
      |                          |                           |
      | [Update Access Token]     |                           |
      |                          |                           |
      | [Refresh Token Expired]   |                           |
      | <-------------------------| Return 401, Re-login      |
      |                          |                           |

前端代码示例(Vue.js + Axios)

下面是一个实现 Token 自动刷新机制 的 Vue.js 和 Axios 配置代码:

import axios from 'axios';

// 创建 Axios 实例
const api = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 10000,
});

// 请求拦截器,携带 Access Token
api.interceptors.request.use(
  (config) => {
    const accessToken = localStorage.getItem('accessToken'); // 或从 Vuex 中获取
    if (accessToken) {
      config.headers['Authorization'] = `Bearer ${accessToken}`;
    }
    return config;
  },
  (error) => Promise.reject(error)
);

// 响应拦截器,处理 Token 过期的情况
api.interceptors.response.use(
  (response) => {
    return response;
  },
  async (error) => {
    const originalRequest = error.config;
    if (error.response.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;
      const refreshToken = getCookie('refreshToken'); // 从 HTTP-Only Cookie 获取 Refresh Token
      if (refreshToken) {
        try {
          const { data } = await api.post('/auth/refresh-token', { refreshToken });
          localStorage.setItem('accessToken', data.accessToken); // 更新 Access Token
          originalRequest.headers['Authorization'] = `Bearer ${data.accessToken}`;
          return api(originalRequest); // 重新发起原始请求
        } catch (refreshError) {
          console.log('Refresh token expired, please re-login');
          // 跳转到登录页
          window.location.href = '/login';
        }
      }
    }
    return Promise.reject(error);
  }
);

export default api;

后端代码示例(Node.js + Express + JWT)

以下是使用 jsonwebtoken 来生成和验证 Token 的示例:

const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();

const ACCESS_TOKEN_SECRET = 'your-access-token-secret';
const REFRESH_TOKEN_SECRET = 'your-refresh-token-secret';
let refreshTokens = []; // 存储有效的刷新 Token

// 登录接口
app.post('/login', (req, res) => {
  const username = req.body.username;
  // 验证用户身份,略...
  
  const accessToken = jwt.sign({ username }, ACCESS_TOKEN_SECRET, { expiresIn: '15m' });
  const refreshToken = jwt.sign({ username }, REFRESH_TOKEN_SECRET, { expiresIn: '7d' });
  refreshTokens.push(refreshToken);
  
  res.json({
    accessToken,
    refreshToken,
  });
});

// 刷新 Token 接口
app.post('/auth/refresh-token', (req, res) => {
  const { refreshToken } = req.body;
  if (!refreshToken || !refreshTokens.includes(refreshToken)) {
    return res.sendStatus(403);
  }
  
  jwt.verify(refreshToken, REFRESH_TOKEN_SECRET, (err, user) => {
    if (err) return res.sendStatus(403);
    const accessToken = jwt.sign({ username: user.username }, ACCESS_TOKEN_SECRET, { expiresIn: '15m' });
    res.json({ accessToken });
  });
});

// 保护路由示例
app.get('/protected', authenticateToken, (req, res) => {
  res.send('This is a protected route');
});

// 中间件验证 Access Token
function authenticateToken(req, res, next) {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];
  
  if (!token) return res.sendStatus(401);
  
  jwt.verify(token, ACCESS_TOKEN_SECRET, (err, user) => {
    if (err) return res.sendStatus(403);
    req.user = user;
    next();
  });
}

app.listen(3000, () => {
  console.log('Server is running on port 3000');
});

刷新 Token 的过期处理

  • 前端:在捕获到刷新 Token 过期的情况下,前端应清除本地存储的 Token 并跳转到登录页面。
  • 后端:刷新 Token 过期后,后端应返回 401 错误码并记录下可能的异常。

总结

  • 使用 访问 Token 短期内进行认证,使用 刷新 Token 定期获取新的访问 Token。
  • 刷新 Token 存储在安全的 HTTP-Only Cookie 中,

避免 XSS 攻击。

  • 通过 401 状态码和拦截器 自动处理 Token 的刷新和过期处理。
评论区
评论列表
menu