最近在写后台管理,这里分享一下我的登录模块的实现,我是使用react+typescript实现的,主要是登录的逻辑和双token的处理方式,请求接口的二次封装aixos
1.首先我们需要渲染登录界面的窗口,这个很简单就不详细讲解了,然后主要就是关于点击登录按钮的接口的调用
封装我们的接口(封装是非常有必要的):
下面是我们的登录接口,然后request就是我二次封装的axios
export function login(data: LoginData) {
return request.post<LoginResult>('/backstage/login', data);
}
这里详细讲解一下关于axios的封装,对于每一个要写项目的时候,只要有后端请求接口,我们都需要封装axios,这一步很重要,下面是对axios封装的主要实现,主要是基于双token的axios二次封装请求
创建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;