ts 联合react 实现ajax的封装,refreshtoken的功能
ts 联合react 实现ajax的封装,refreshtoken的功能
一、背景与问题
在现代Web开发中,基于Token的认证机制已成为主流。其中Refresh Token的使用场景非常典型:当用户进行敏感操作时,系统会返回Access Token和Refresh Token。Access Token用于短期认证(通常有效期为15分钟),Refresh Token用于长期认证(通常有效期为30天)。这种机制在OAuth2.0协议中尤为常见。
在实际开发中,我们面临两个核心问题:
- 如何在前端优雅地处理Token过期后的重认证逻辑?
- 如何在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. 性能优化方案
Token缓存:避免重复刷新
const [isRefreshing, setIsRefreshing] = useState(false); const refresh = async (): Promise<void> => { if (isRefreshing) return; setIsRefreshing(true); // ... setIsRefreshing(false); };请求防抖:避免频繁刷新
const debouncedRefresh = debounce(refresh, 1000);- 缓存策略:使用
localStorage持久化Token
2. 安全注意事项
- HTTPS强制:确保所有通信使用HTTPS
- Token存储:使用
HttpOnlyCookie存储Access Token - 敏感信息处理:避免在日志中暴露Token信息
- 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. 常见坑点
多次刷新:在刷新过程中多次触发刷新逻辑
// 错误示例 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); } };未重试请求:刷新Token后未重新发送请求
// 正确示例 return refresh().then(() => { return axios(config); });上下文未正确传递:未在组件树中正确使用
AuthProvider// 错误示例 <AuthProvider> <App /> </AuthProvider>
十、最佳实践
1. 推荐使用场景
- 需要长期保持登录状态的系统
- 需要进行敏感操作的业务场景
- 需要统一处理Token刷新的复杂系统
2. 不推荐使用场景
- 对实时性要求极高的系统(如股票交易)
- 无需持久化Token的轻量级应用
- 需要频繁进行短时认证的场景
3. 优化建议
- 使用缓存策略:避免重复刷新
- 添加重试机制:处理网络波动
- 使用装饰器模式:扩展请求功能
- 分离逻辑:将刷新逻辑和请求逻辑分离
十一、总结
本文深入探讨了基于TypeScript和React的AJAX封装方案,重点分析了Refresh Token的实现原理和应用场景。通过创建Token上下文管理器,实现了统一的请求拦截和自动刷新机制。在实际开发中,我们需要根据具体需求选择合适的封装方案,同时注意处理常见的边界情况和异常场景。
关键收获包括:
- 理解了Token刷新的完整流程
- 掌握了React上下文管理的使用技巧
- 熟悉了TypeScript的类型定义方法
- 知道了如何处理常见的网络异常
- 理解了安全和性能优化的重要性
在实际项目中,建议根据业务需求选择合适的封装方案,合理使用上下文管理,同时注意处理好Token的存储、刷新和失效等关键环节。对于需要长期保持登录状态的系统,这种封装方案可以显著提升开发效率和系统稳定性。
评论已关闭