ts 联合react 实现ajax的封装,refreshtoken的功能

ts 联合react 实现ajax的封装,refreshtoken的功能

一、背景与问题

在现代Web开发中,基于Token的认证机制已成为主流。其中Refresh Token的使用场景非常典型:当用户进行敏感操作时,系统会返回Access Token和Refresh Token。Access Token用于短期认证(通常有效期为15分钟),Refresh Token用于长期认证(通常有效期为30天)。这种机制在OAuth2.0协议中尤为常见。

在实际开发中,我们面临两个核心问题:

  1. 如何在前端优雅地处理Token过期后的重认证逻辑?
  2. 如何在React组件中封装统一的AJAX请求逻辑?

传统做法往往在每个API调用中重复处理Token刷新逻辑,这导致代码冗余和维护困难。本文将通过TypeScript和React的结合,构建一个可复用的AJAX封装方案,实现自动的Token刷新机制。

二、基本原理

1. Token刷新机制

Token刷新的核心流程如下:

  • 在用户登录时获取Access Token和Refresh Token
  • 在后续请求中携带Access Token
  • 当检测到401错误时,使用Refresh Token向服务器申请新的Access Token
  • 成功刷新后,将新的Access Token存储并重新发送请求

2. React的上下文管理

通过React的Context API,可以创建全局的Token管理器:

  • 维护当前的Access Token和Refresh Token
  • 提供刷新Token的异步方法
  • 提供请求拦截器处理401错误

3. TypeScript的类型安全

利用TypeScript的类型系统,可以定义:

  • 请求配置类型
  • 响应类型
  • 错误类型
  • 状态管理类型

三、环境准备

# 创建React项目
npx create-react-app token-axios
cd token-axios

# 安装依赖
npm install axios @types/axios

四、核心实现

1. 定义基础类型

// src/types/auth.ts
export interface AuthTokens {
  accessToken: string;
  refreshToken: string;
  expiresIn: number;
}

export interface AuthContextType {
  accessToken: string | null;
  refreshToken: string | null;
  refresh: () => Promise<void>;
  setTokens: (tokens: AuthTokens) => void;
}

2. 创建Token上下文

// src/context/AuthContext.tsx
import React, { createContext, useContext, useState, useEffect } from 'react';

interface AuthContextType {
  accessToken: string | null;
  refreshToken: string | null;
  refresh: () => Promise<void>;
  setTokens: (tokens: AuthTokens) => void;
}

const AuthContext = createContext<AuthContextType | undefined>(undefined);

export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const [accessToken, setAccessToken] = useState<string | null>(null);
  const [refreshToken, setRefreshToken] = useState<string | null>(null);

  const setTokens = (tokens: AuthTokens) => {
    setAccessToken(tokens.accessToken);
    setRefreshToken(tokens.refreshToken);
    localStorage.setItem('authTokens', JSON.stringify(tokens));
  };

  const refresh = async (): Promise<void> => {
    if (!refreshToken) throw new Error('Missing refresh token');

    try {
      const response = await axios.post('/api/refresh', { refreshToken });
      const newTokens = response.data as AuthTokens;
      setTokens(newTokens);
    } catch (error) {
      console.error('Failed to refresh token:', error);
      throw error;
    }
  };

  return (
    <AuthContext.Provider value={{ accessToken, refreshToken, refresh, setTokens }}>
      {children}
    </AuthContext.Provider>
  );
};

export const useAuth = () => {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within an AuthProvider');
  }
  return context;
};

3. 封装AJAX请求

// src/utils/ajax.ts
import axios from 'axios';
import { useAuth } from './context/AuthContext';

interface RequestConfig {
  url: string;
  method: 'GET' | 'POST' | 'PUT' | 'DELETE';
  data?: Record<string, any>;
  headers?: Record<string, string>;
}

interface ApiResponse<T> {
  data: T;
  status: number;
  statusText: string;
}

export const ajax = <T,>(config: RequestConfig): Promise<ApiResponse<T>> => {
  const { accessToken, refresh } = useAuth();
  
  return axios(config)
    .catch((error) => {
      if (error.response?.status === 401 && accessToken) {
        return refresh().then(() => {
          // 重新发送原始请求
          return axios(config);
        });
      }
      throw error;
    });
};

五、完整案例

1. 登录组件

// src/components/Login.tsx
import React, { useState } from 'react';
import { useAuth } from '../context/AuthContext';

const Login: React.FC = () => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const { setTokens } = useAuth();

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    
    try {
      const response = await axios.post('/api/login', { email, password });
      const tokens = response.data as AuthTokens;
      setTokens(tokens);
    } catch (error) {
      console.error('Login failed:', error);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Password"
      />
      <button type="submit">Login</button>
    </form>
  );
};

2. 数据获取组件

// src/components/DataFetcher.tsx
import React, { useEffect } from 'react';
import { useAuth } from '../context/AuthContext';

const DataFetcher: React.FC = () => {
  const { accessToken } = useAuth();

  useEffect(() => {
    if (accessToken) {
      ajax({
        url: '/api/data',
        method: 'GET',
      }).then((response) => {
        console.log('Data received:', response.data);
      }).catch((error) => {
        console.error('Error fetching data:', error);
      });
    }
  }, [accessToken]);

  return <div>Data Fetcher Component</div>;
};

六、源码解析

1. Token刷新逻辑

const refresh = async (): Promise<void> => {
  if (!refreshToken) throw new Error('Missing refresh token');

  try {
    const response = await axios.post('/api/refresh', { refreshToken });
    const newTokens = response.data as AuthTokens;
    setTokens(newTokens);
  } catch (error) {
    console.error('Failed to refresh token:', error);
    throw error;
  }
};
  • 使用Refresh Token向服务器请求新的Access Token
  • 通过setTokens更新本地存储
  • 如果刷新失败,抛出错误让调用方处理

2. 请求拦截逻辑

return axios(config)
  .catch((error) => {
    if (error.response?.status === 401 && accessToken) {
      return refresh().then(() => {
        // 重新发送原始请求
        return axios(config);
      });
    }
    throw error;
  });
  • 捕获401错误时触发刷新流程
  • 使用refresh方法获取新Token
  • 使用axios(config)重新发送请求
  • 如果刷新失败,抛出错误让调用方处理

七、进阶使用

1. 带超时的请求封装

export const ajaxWithTimeout = <T,>(config: RequestConfig): Promise<ApiResponse<T>> => {
  const { accessToken, refresh } = useAuth();
  
  return axios(config)
    .timeout(5000)
    .catch((error) => {
      if (error.response?.status === 401 && accessToken) {
        return refresh().then(() => {
          return axios(config)
            .timeout(5000)
            .catch((innerError) => {
              console.error('Failed to refresh token:', innerError);
              throw innerError;
            });
        });
      }
      throw error;
    });
};

2. 响应拦截器

axios.interceptors.response.use(
  (response) => {
    // 处理成功响应
    return response;
  },
  (error) => {
    // 处理错误响应
    console.error('Global error handler:', error);
    return Promise.reject(error);
  }
);

八、性能与工程实践

1. 性能优化方案

  1. Token缓存:避免重复刷新

    const [isRefreshing, setIsRefreshing] = useState(false);
    const refresh = async (): Promise<void> => {
      if (isRefreshing) return;
      setIsRefreshing(true);
      // ...
      setIsRefreshing(false);
    };
  2. 请求防抖:避免频繁刷新

    const debouncedRefresh = debounce(refresh, 1000);
  3. 缓存策略:使用localStorage持久化Token

2. 安全注意事项

  1. HTTPS强制:确保所有通信使用HTTPS
  2. Token存储:使用HttpOnly Cookie存储Access Token
  3. 敏感信息处理:避免在日志中暴露Token信息
  4. CSRF防护:使用SameSite Cookie属性

3. 异常处理机制

try {
  const response = await ajax({
    url: '/api/data',
    method: 'GET',
  });
  console.log('Success:', response.data);
} catch (error) {
  if (error.response?.status === 500) {
    console.error('Server error:', error);
  } else {
    console.error('Request failed:', error);
  }
}

九、常见问题与踩坑

1. 常见错误

问题原因解决方案
刷新失败未正确处理401错误使用axios的catch拦截器
请求中断未处理超时添加timeout配置
Token过期未更新本地存储使用localStorage持久化
状态丢失未正确管理上下文使用useContext和useAuth

2. 常见坑点

  1. 多次刷新:在刷新过程中多次触发刷新逻辑

    // 错误示例
    const refresh = async (): Promise<void> => {
      if (!refreshToken) throw new Error('Missing refresh token');
      try {
        const response = await axios.post('/api/refresh', { refreshToken });
        const newTokens = response.data as AuthTokens;
        setTokens(newTokens);
      } catch (error) {
        console.error('Failed to refresh token:', error);
      }
    };
  2. 未重试请求:刷新Token后未重新发送请求

    // 正确示例
    return refresh().then(() => {
      return axios(config);
    });
  3. 上下文未正确传递:未在组件树中正确使用AuthProvider

    // 错误示例
    <AuthProvider>
      <App />
    </AuthProvider>

十、最佳实践

1. 推荐使用场景

  • 需要长期保持登录状态的系统
  • 需要进行敏感操作的业务场景
  • 需要统一处理Token刷新的复杂系统

2. 不推荐使用场景

  • 对实时性要求极高的系统(如股票交易)
  • 无需持久化Token的轻量级应用
  • 需要频繁进行短时认证的场景

3. 优化建议

  1. 使用缓存策略:避免重复刷新
  2. 添加重试机制:处理网络波动
  3. 使用装饰器模式:扩展请求功能
  4. 分离逻辑:将刷新逻辑和请求逻辑分离

十一、总结

本文深入探讨了基于TypeScript和React的AJAX封装方案,重点分析了Refresh Token的实现原理和应用场景。通过创建Token上下文管理器,实现了统一的请求拦截和自动刷新机制。在实际开发中,我们需要根据具体需求选择合适的封装方案,同时注意处理常见的边界情况和异常场景。

关键收获包括:

  • 理解了Token刷新的完整流程
  • 掌握了React上下文管理的使用技巧
  • 熟悉了TypeScript的类型定义方法
  • 知道了如何处理常见的网络异常
  • 理解了安全和性能优化的重要性

在实际项目中,建议根据业务需求选择合适的封装方案,合理使用上下文管理,同时注意处理好Token的存储、刷新和失效等关键环节。对于需要长期保持登录状态的系统,这种封装方案可以显著提升开发效率和系统稳定性。

评论已关闭

推荐阅读

AIGC实战——Transformer模型
2024年12月01日
Socket TCP 和 UDP 编程基础(Python)
2024年11月30日
python , tcp , udp
如何使用 ChatGPT 进行学术润色?你需要这些指令
2024年12月01日
AI
最新 Python 调用 OpenAi 详细教程实现问答、图像合成、图像理解、语音合成、语音识别(详细教程)
2024年11月24日
ChatGPT 和 DALL·E 2 配合生成故事绘本
2024年12月01日
omegaconf,一个超强的 Python 库!
2024年11月24日
【视觉AIGC识别】误差特征、人脸伪造检测、其他类型假图检测
2024年12月01日
[超级详细]如何在深度学习训练模型过程中使用 GPU 加速
2024年11月29日
Python 物理引擎pymunk最完整教程
2024年11月27日
MediaPipe 人体姿态与手指关键点检测教程
2024年11月27日
深入了解 Taipy:Python 打造 Web 应用的全面教程
2024年11月26日
基于Transformer的时间序列预测模型
2024年11月25日
Python在金融大数据分析中的AI应用(股价分析、量化交易)实战
2024年11月25日
AIGC Gradio系列学习教程之Components
2024年12月01日
Python3 `asyncio` — 异步 I/O,事件循环和并发工具
2024年11月30日
llama-factory SFT系列教程:大模型在自定义数据集 LoRA 训练与部署
2024年12月01日
Python 多线程和多进程用法
2024年11月24日
Python socket详解,全网最全教程
2024年11月27日
python之plot()和subplot()画图
2024年11月26日
理解 DALL·E 2、Stable Diffusion 和 Midjourney 工作原理
2024年12月01日