feat(auth): 实现认证API和token自动刷新功能
添加认证服务API模块,包括登录、token刷新和验证功能 在axios拦截器中实现token自动刷新机制,处理401错误 更新tsconfig配置以支持ES2020特性 重构API导入路径以使用新的auth模块结构
This commit is contained in:
@@ -24,7 +24,8 @@ export interface LoginResponse {
|
|||||||
unionId: string | null;
|
unionId: string | null;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
refreshToken: string;
|
refreshToken: string;
|
||||||
expire: number | null;
|
accessTokenExpireTime: number | null;
|
||||||
|
refreshTokenExpireTime: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 刷新令牌响应数据
|
// 刷新令牌响应数据
|
||||||
@@ -34,7 +35,8 @@ export interface RefreshTokenResponse {
|
|||||||
unionId: string | null;
|
unionId: string | null;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
refreshToken: string;
|
refreshToken: string;
|
||||||
expire: number | null;
|
accessTokenExpireTime: number | null;
|
||||||
|
refreshTokenExpireTime: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export { chatAPI, type ChatMessage, type ChatRequest } from './aiChat'
|
export { chatAPI, type ChatMessage, type ChatRequest } from '../aiChat'
|
||||||
|
|
||||||
// 认证 API
|
// 认证 API
|
||||||
export {
|
export {
|
||||||
@@ -3,7 +3,18 @@ import axios, {
|
|||||||
type InternalAxiosRequestConfig,
|
type InternalAxiosRequestConfig,
|
||||||
} from "axios";
|
} from "axios";
|
||||||
import { ENV } from "../config";
|
import { ENV } from "../config";
|
||||||
import { getAccessToken } from "@/utils/token";
|
import {
|
||||||
|
getAccessToken,
|
||||||
|
getRefreshToken,
|
||||||
|
setTokens,
|
||||||
|
clearTokens,
|
||||||
|
} from "@/utils/token";
|
||||||
|
import { Message } from "@arco-design/web-vue";
|
||||||
|
|
||||||
|
// 是否正在刷新 token
|
||||||
|
let isRefreshing = false;
|
||||||
|
// 存储因为 token 过期而挂起的请求
|
||||||
|
let requests: ((token: string) => void)[] = [];
|
||||||
|
|
||||||
const request: AxiosInstance = axios.create({
|
const request: AxiosInstance = axios.create({
|
||||||
baseURL: ENV.API_BASE_URL,
|
baseURL: ENV.API_BASE_URL,
|
||||||
@@ -32,17 +43,83 @@ request.interceptors.request.use(
|
|||||||
// =======================
|
// =======================
|
||||||
request.interceptors.response.use(
|
request.interceptors.response.use(
|
||||||
(response) => {
|
(response) => {
|
||||||
console.log("响应: ", response);
|
// console.log("响应: ", response);
|
||||||
const data = response.data;
|
const data = response.data;
|
||||||
/**TODO: 增加响应码处理 */
|
/**TODO: 增加响应码处理 */
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
(error) => {
|
async (error) => {
|
||||||
|
const originalRequest = error.config;
|
||||||
|
|
||||||
|
// 处理 401 未授权情况 (Token 过期)
|
||||||
|
// 确保不是刷新 token 的请求本身 (避免死循环)
|
||||||
|
if (
|
||||||
|
error.response?.status === 401 &&
|
||||||
|
!originalRequest._retry &&
|
||||||
|
!originalRequest.url.includes("/auth/refresh")
|
||||||
|
) {
|
||||||
|
// 如果正在刷新,将当前请求加入队列等待
|
||||||
|
if (isRefreshing) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
requests.push((token: string) => {
|
||||||
|
originalRequest.headers.Authorization = `Bearer ${token}`;
|
||||||
|
resolve(request(originalRequest));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
originalRequest._retry = true;
|
||||||
|
isRefreshing = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const refreshToken = getRefreshToken();
|
||||||
|
if (!refreshToken) {
|
||||||
|
throw new Error("No refresh token available");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用原生 axios 发送刷新请求,避免拦截器循环
|
||||||
|
const response = await axios.post(
|
||||||
|
`${ENV.API_BASE_URL}/v1/auth/refresh`,
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
params: { refreshToken },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.data?.success) {
|
||||||
|
const { accessToken, refreshToken: newRefreshToken } =
|
||||||
|
response.data.data;
|
||||||
|
// 更新本地存储
|
||||||
|
setTokens(accessToken, newRefreshToken);
|
||||||
|
|
||||||
|
// 执行队列中的请求
|
||||||
|
requests.forEach((cb) => cb(accessToken));
|
||||||
|
requests = [];
|
||||||
|
|
||||||
|
// 重试当前请求
|
||||||
|
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
|
||||||
|
return request(originalRequest);
|
||||||
|
} else {
|
||||||
|
throw new Error("Refresh token failed");
|
||||||
|
}
|
||||||
|
} catch (refreshError) {
|
||||||
|
console.error("Token 刷新失败:", refreshError);
|
||||||
|
// 清除过期 token
|
||||||
|
clearTokens();
|
||||||
|
Message.error("登录已过期,请重新登录");
|
||||||
|
// 这里可以选择跳转到登录页,例如 window.location.href = '/user/login'
|
||||||
|
// 建议让路由守卫或者页面自行处理未登录状态
|
||||||
|
return Promise.reject(refreshError);
|
||||||
|
} finally {
|
||||||
|
isRefreshing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Promise.reject(error) 用来 返回一个状态为 rejected 的 Promise,相当于“主动抛出错误”,
|
* Promise.reject(error) 用来 返回一个状态为 rejected 的 Promise,相当于“主动抛出错误”,
|
||||||
*
|
*
|
||||||
* 让调用者能够在 .catch() 或 try/catch 中捕获这个错误。
|
* 让调用者能够在 .catch() 或 try/catch 中捕获这个错误。
|
||||||
它在 异步流程、拦截器、错误处理 中非常常见。
|
* 它在 异步流程、拦截器、错误处理 中非常常见。
|
||||||
*/
|
*/
|
||||||
console.error("❌ 网络错误:", error);
|
console.error("❌ 网络错误:", error);
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
import { ref, nextTick, onMounted } from 'vue'
|
import { ref, nextTick, onMounted } from 'vue'
|
||||||
import { Message } from '@arco-design/web-vue'
|
import { Message } from '@arco-design/web-vue'
|
||||||
import { IconRobot, IconUser, IconSend } from '@arco-design/web-vue/es/icon'
|
import { IconRobot, IconUser, IconSend } from '@arco-design/web-vue/es/icon'
|
||||||
import { chatAPI, type ChatMessage as APIChatMessage } from '@/api'
|
import { chatAPI, type ChatMessage as APIChatMessage } from '@/api/auth'
|
||||||
|
|
||||||
interface ChatMessage {
|
interface ChatMessage {
|
||||||
role: 'user' | 'assistant'
|
role: 'user' | 'assistant'
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ import { reactive, ref } from "vue";
|
|||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { Message } from "@arco-design/web-vue";
|
import { Message } from "@arco-design/web-vue";
|
||||||
import { IconUser, IconLock } from "@arco-design/web-vue/es/icon";
|
import { IconUser, IconLock } from "@arco-design/web-vue/es/icon";
|
||||||
import { login } from "@/api/auth";
|
import { login } from "@/api/auth/auth";
|
||||||
import { setTokens } from "@/utils/token";
|
import { setTokens } from "@/utils/token";
|
||||||
import { useUserStore } from "@/store/user";
|
import { useUserStore } from "@/store/user";
|
||||||
import ACCESS_ENUM from "@/access/accessEnum";
|
import ACCESS_ENUM from "@/access/accessEnum";
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
{
|
{
|
||||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"emitDeclarationOnly": false,
|
"emitDeclarationOnly": false,
|
||||||
"declarationDir": "./dist/types",
|
"declarationDir": "./dist/types",
|
||||||
@@ -26,4 +28,4 @@
|
|||||||
"src/**/*.tsx",
|
"src/**/*.tsx",
|
||||||
"src/**/*.vue"
|
"src/**/*.vue"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user