后台管理系统登录模块(双token的实现思路)

发布于:2025-07-21 ⋅ 阅读:(20) ⋅ 点赞:(0)

最近在写后台管理,这里分享一下我的登录模块的实现,我是使用react+typescript实现的,主要是登录的逻辑和双token的处理方式,请求接口的二次封装aixos

1.首先我们需要渲染登录界面的窗口,这个很简单就不详细讲解了,然后主要就是关于点击登录按钮的接口的调用

封装我们的接口(封装是非常有必要的):

下面是我们的登录接口,然后request就是我二次封装的axios

export function login(data: LoginData) {
  return request.post<LoginResult>('/backstage/login', data);
}

这里详细讲解一下关于axios的封装,对于每一个要写项目的时候,只要有后端请求接口,我们都需要封装axios,这一步很重要,下面是对axios封装的主要实现,主要是基于双tokenaxios二次封装请求

创建axios的封装核心文件(request)讲解:

1.双token的实现逻辑?
1.1长短token是什么:

当用户登录成功之后会返回一个json数据,里面有连个token,一个是短token,access_token,一个是长token,refresh_token

access_token是访问令牌,因为在请求具有权限接口的时候需要请求头,里面需要放用户token,这个请求头里面的token就是我们的access_token,access_token的存在时间很短

refresh_token是刷新令牌,用于生成短token

1.2双token的更新更新逻辑:

当access_token过期时,需要使用refresh_token来生成一个新的access_token,那么什么时候会触发这个刷新机制呢,其实就是当调用权限接口的时候,如果access_token过期了,服务器教会返回一个401 unauthorized,前端的响应拦截器会获取所有的api请求,当它获取到401的时候,就知道access_token过期了,然后就刷新token了

当获取到了access_token之后,需要存储access_token,这是为了确保后续所有新发起的 API 请求都能使用这个最新的access_token。之前失败的请求并没有被直接抛弃,而是被暂存到了一个“待重试队列”(requestsToRetry)中。重新发送新的请求

2.axios二次封装实现思路:

实现引入我们的核心库,第一行就不进行解释了,第二行就是自己封装的一下存储,获取,清除token的自己封装的一些方法,见名知意

import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';
import { getAccessToken, getRefreshToken, setTokens, clearTokens } from './token';
2.1创建队列和函数:

isRefreshing,用于确保在多个请求同时 401 时,只有一个 token刷新请求被发送,防止重复刷新。

failedQueue队列:当isRefreshing为true时(表示已经有刷新 token 的请求在进行中),所有后续因 401 失败的请求都会被推入failedQueue。这些请求会返回一个新的 Promise,等待 token 刷新成功后被解决。

processQueue 函数:负责在 token 刷新成功或失败后,处理 failedQueue 中的所有请求。如果成功,用新的 token 重新发送;如果失败,则拒绝这些请求。

let isRefreshing: boolean = false;
// 存储因 token 过期而失败的请求队列
// 定义队列中每个元素的类型
interface FailedRequest {
  resolve: (value?: string | PromiseLike<string>) => void;
  reject: (reason?: any) => void;
}
let failedQueue: FailedRequest[] = []

const processQueue = (error: Error | null, token: string | null = null): void => {
  failedQueue.forEach(prom => {
    if (error) {
      prom.reject(error);
    } else {
      prom.resolve(token as string); // 确保在没有错误时 token 不为 null
    }
  });
  failedQueue = [];
};
2.2创建axios实例:

使用 axios.create() 创建一个独立的 axios 实例,避免污染全局 axios

const request: AxiosInstance = axios.create({
  // 在 .env 文件中配置 中的请求配置
  baseURL: api基础路径,
  timeout: 10000, // 请求超时时间
});
2.3创建请求拦截器:
  • 动态添加access_token,从存储位置获取access_token,并将其添加到请求头 Authorization 中。通常格式为 Bearer ${token}。
  • 除了上面的请求头,还可以添加其他请求头,如 Content-Type。
  • 可以实现全局的请求 Loading 动画。
// --- 请求拦截器 ---
request.interceptors.request.use(
  (config: InternalAxiosRequestConfig) => {
    const accessToken = getAccessToken();
    if (accessToken) {
      // 在请求头中添加 Authorization 字段
      if (!config.headers) {
        config.headers = new axios.AxiosHeaders();
      }
      config.headers['Authorization'] = `Bearer ${accessToken}`;
    }
    return config;
  },
  (error: AxiosError) => {
    return Promise.reject(error);
  },
);
2.4创建响应拦截器:

后端返回的数据通常会包裹在data当中,可以直接返回response.data

处理 HTTP 状态码非 2xx 的错误

无感刷新 token (核心):

  • 捕获401 Unauthorized错误:这是最关键的一步。当后端因为access_token过期而返回401 时,拦截此错误。                
  • 调用刷新接口:使用refresh_token去请求新的access_token。                
  • 重发失败请求:获取到新的access_token后,将刚才失败的请求(error.config)用新 token重新发送一次。              
  •  并发请求处理:当多个请求同时因为 token 过期而失败时,要确保刷新 token的接口只被调用一次。后续失败的请求应被“暂存”,等待新 token 获取后再统一重发。
// --- 响应拦截器 ---
request.interceptors.response.use(
  // 响应成功 (HTTP 状态码为 2xx)
  (response: AxiosResponse<any>) => {
    // 通常后端会把数据包裹在 data 中,这里直接返回 data,简化业务代码
    return response.data;
  }, // 响应失败 (HTTP 状态码非 2xx)

  async (error: AxiosError) => {
    const originalRequest = error.config as
      | (InternalAxiosRequestConfig & { _retry?: boolean })
      | undefined;

    // 如果没有config,直接返回错误
    if (!originalRequest) {
      console.error('Request Error: No config available');
      return Promise.reject(error);
    } // 检查是否是 401 Unauthorized 错误,并且不是刷新 token 的请求本身
    if (error.response?.status === 401 && !originalRequest._retry) {
      // 如果正在刷新 token,则将当前失败的请求加入队列
      if (isRefreshing) {
        return new Promise<string>((resolve, reject) => {
          failedQueue.push({
            resolve: (value?: string | PromiseLike<string>) => resolve(value as string),
            reject,
          });
        })
          .then((token) => {
            if (!originalRequest.headers) {
              originalRequest.headers = new axios.AxiosHeaders();
            }
            originalRequest.headers['Authorization'] = `Bearer ${token}`;
            return request(originalRequest); // 使用新 token 重新发送请求
          })
          .catch((err) => {
            return Promise.reject(err);
          });
      }
      originalRequest._retry = true; // 标记此请求已尝试过重试
      isRefreshing = true;

      const refreshToken = getRefreshToken();
      if (!refreshToken) {
        // 如果没有 refresh_token,直接跳转到登录页
        console.error('No refresh token available.');
        clearTokens(); // window.location.href = '/login'; // 或使用 router.push('/login')
        return Promise.reject(new Error('No refresh token, redirect to login.'));
      }

      try {
        // --- 调用刷新 Token 的 API ---
        // 注意:这里需要使用一个不带拦截器的 axios 实例来发请求,避免循环调
        const response = await axios.post<{
          data: {
            access_token: string;
            refresh_token: string;
          };
        }>('登录接口api', {
          refresh_token: refreshToken,
        });

        const { access_token: newAccessToken, refresh_token: newRefreshToken } = response.data.data; // 1. 更新本地存储的 token

        setTokens(newAccessToken, newRefreshToken); // 2. 处理并重发等待队列中的请求

        processQueue(null, newAccessToken); // 3. 重发本次失败的请求

        if (!originalRequest.headers) {
          originalRequest.headers = new axios.AxiosHeaders();
        }
        originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`;
        return request(originalRequest);
      } catch (refreshError: unknown) {
        // 刷新 token 失败,清除所有 token 并重定向到登录页
        console.error('Failed to refresh token:', refreshError);
        clearTokens();
        processQueue(refreshError as Error, null); // window.location.href = '/login'; // 或使用 router.push('/login')
        return Promise.reject(refreshError);
      } finally {
        isRefreshing = false;
      }
    } // 对于其他错误,直接抛出

    // 处理错误信息,确保类型安全
    const errorMessage =
      error.response?.data &&
      typeof error.response.data === 'object' &&
      'message' in error.response.data
        ? (error.response.data as { message: string }).message
        : error.message;
    console.error('Request Error:', errorMessage);
    return Promise.reject(error);
  },
);

 

2.5完整的axios二次封装(request):
import axios, { AxiosInstance, InternalAxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';
import { getAccessToken, getRefreshToken, setTokens, clearTokens } from '../stores/token';

// --- 状态变量 ---
// 标记是否正在刷新 token,防止重复刷新
let isRefreshing: boolean = false;
// 存储因 token 过期而失败的请求队列
// 定义队列中每个元素的类型
interface FailedRequest {
  resolve: (value?: string | PromiseLike<string>) => void;
  reject: (reason?: unknown) => void;
}
let failedQueue: FailedRequest[] = [];

/**
 * @description 处理队列中的请求
 * @param {Error | null} error - 刷新 token 过程中的错误
 * @param {string | null} token - 新的 access_token
 */

const processQueue = (error: Error | null, token: string | null = null): void => {
  failedQueue.forEach((prom) => {
    if (error) {
      prom.reject(error);
    } else {
      prom.resolve(token as string); // 确保在没有错误时 token 不为 null
    }
  });
  failedQueue = [];
};

// --- 创建 Axios 实例 ---
const request: AxiosInstance = axios.create({
  // 在 .env 文件中配置 中的请求配置
  baseURL: '基础api',
  timeout: 10000, // 请求超时时间
});

// --- 请求拦截器 ---
request.interceptors.request.use(
  (config: InternalAxiosRequestConfig) => {
    const accessToken = getAccessToken();
    if (accessToken) {
      // 在请求头中添加 Authorization 字段
      if (!config.headers) {
        config.headers = new axios.AxiosHeaders();
      }
      config.headers['Authorization'] = `Bearer ${accessToken}`;
    }
    return config;
  },
  (error: AxiosError) => {
    return Promise.reject(error);
  },
);

// --- 响应拦截器 ---
request.interceptors.response.use(
  // 响应成功 (HTTP 状态码为 2xx)
  (response: AxiosResponse<any>) => {
    // 通常后端会把数据包裹在 data 中,这里直接返回 data,简化业务代码
    return response.data;
  }, // 响应失败 (HTTP 状态码非 2xx)

  async (error: AxiosError) => {
    const originalRequest = error.config as
      | (InternalAxiosRequestConfig & { _retry?: boolean })
      | undefined;

    // 如果没有config,直接返回错误
    if (!originalRequest) {
      console.error('Request Error: No config available');
      return Promise.reject(error);
    } // 检查是否是 401 Unauthorized 错误,并且不是刷新 token 的请求本身
    if (error.response?.status === 401 && !originalRequest._retry) {
      // 如果正在刷新 token,则将当前失败的请求加入队列
      if (isRefreshing) {
        return new Promise<string>((resolve, reject) => {
          failedQueue.push({
            resolve: (value?: string | PromiseLike<string>) => resolve(value as string),
            reject,
          });
        })
          .then((token) => {
            if (!originalRequest.headers) {
              originalRequest.headers = new axios.AxiosHeaders();
            }
            originalRequest.headers['Authorization'] = `Bearer ${token}`;
            return request(originalRequest); // 使用新 token 重新发送请求
          })
          .catch((err) => {
            return Promise.reject(err);
          });
      }
      originalRequest._retry = true; // 标记此请求已尝试过重试
      isRefreshing = true;

      const refreshToken = getRefreshToken();
      if (!refreshToken) {
        // 如果没有 refresh_token,直接跳转到登录页
        console.error('No refresh token available.');
        clearTokens(); // window.location.href = '/login'; // 或使用 router.push('/login')
        return Promise.reject(new Error('No refresh token, redirect to login.'));
      }

      try {
        // --- 调用刷新 Token 的 API ---
        // 注意:这里需要使用一个不带拦截器的 axios 实例来发请求,避免循环调
        const response = await axios.post<{
          data: {
            access_token: string;
            refresh_token: string;
          };
        }>('login接口', {
          refresh_token: refreshToken,
        });

        const { access_token: newAccessToken, refresh_token: newRefreshToken } = response.data.data; // 1. 更新本地存储的 token

        setTokens(newAccessToken, newRefreshToken); // 2. 处理并重发等待队列中的请求

        processQueue(null, newAccessToken); // 3. 重发本次失败的请求

        if (!originalRequest.headers) {
          originalRequest.headers = new axios.AxiosHeaders();
        }
        originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`;
        return request(originalRequest);
      } catch (refreshError: unknown) {
        // 刷新 token 失败,清除所有 token 并重定向到登录页
        console.error('Failed to refresh token:', refreshError);
        clearTokens();
        processQueue(refreshError as Error, null); // window.location.href = '/login'; // 或使用 router.push('/login')
        return Promise.reject(refreshError);
      } finally {
        isRefreshing = false;
      }
    } // 对于其他错误,直接抛出

    // 处理错误信息,确保类型安全
    const errorMessage =
      error.response?.data &&
      typeof error.response.data === 'object' &&
      'message' in error.response.data
        ? (error.response.data as { message: string }).message
        : error.message;
    console.error('Request Error:', errorMessage);
    return Promise.reject(error);
  },
);

export default request;


网站公告

今日签到

点亮在社区的每一天
去签到