From 7e4bfee095c30183512cda9ad38f58eb5eccb88c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=8F=E7=8A=80=E9=BA=9F?= Date: Thu, 27 Aug 2026 23:20:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(auth):=20=E7=99=BB=E5=BD=95/=E7=99=BB?= =?UTF-8?q?=E5=87=BA/=E8=B6=85=E6=97=B6=E5=AF=B9=E9=BD=90=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=20JWT=20=E6=97=8B=E8=BD=AC=E4=BB=A4=E7=89=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - loginApi 补 refreshToken/expiresIn/adminId,登录成功持久化 refreshToken(此前丢弃,静默续期无从发起) - refreshTokenApi 改 body 传 refreshToken(原模板误传 withCredentials, 后端 required 校验必失败);doRefreshToken 适配新令牌对 - logoutApi 携带 refreshToken,登出撤销刷新会话 - 与后端 HTTP 401 语义化配套:过期触发 vben 静默刷新,失败再重登 - vue-tsc 通过(钩子 turbo 环节因并行提交冲突中断,已独立验证) --- apps/web-tdesign/src/api/core/auth.ts | 32 +++++++++++++-------------- apps/web-tdesign/src/api/request.ts | 15 +++++++++---- apps/web-tdesign/src/store/auth.ts | 10 ++++++--- 3 files changed, 33 insertions(+), 24 deletions(-) diff --git a/apps/web-tdesign/src/api/core/auth.ts b/apps/web-tdesign/src/api/core/auth.ts index 6983584..c2c88c9 100644 --- a/apps/web-tdesign/src/api/core/auth.ts +++ b/apps/web-tdesign/src/api/core/auth.ts @@ -7,14 +7,12 @@ export namespace AuthApi { username?: string; } - /** 登录接口返回值 */ + /** 登录/刷新接口返回值(后端 TokenPair) */ export interface LoginResult { accessToken: string; - } - - export interface RefreshTokenResult { - data: string; - status: number; + adminId: number; + expiresIn: number; + refreshToken: string; } } @@ -29,23 +27,23 @@ export async function loginApi(data: AuthApi.LoginParams) { } /** - * 刷新accessToken + * 刷新 accessToken(旋转令牌:旧 refreshToken 单次有效,返回新令牌对) + * @param refreshToken 当前保存的刷新令牌,后端按其 JTI 撤销旧会话 + * @returns raw 模式 axios response:resp.data 为 {code,message,data} 信封 */ -export async function refreshTokenApi() { - return baseRequestClient.post( - '/api/service/admin/system/auth/refresh', - { - withCredentials: true, - }, - ); +export async function refreshTokenApi(refreshToken: string) { + return baseRequestClient.post<{ + data: { code: number; data: AuthApi.LoginResult; message: string }; + status: number; + }>('/api/service/admin/system/auth/refresh', { refreshToken }); } /** - * 退出登录 + * 退出登录:携带 refreshToken 让后端撤销刷新会话(幂等) */ -export async function logoutApi() { +export async function logoutApi(refreshToken?: string) { return baseRequestClient.post('/api/service/admin/system/auth/logout', { - withCredentials: true, + refreshToken, }); } diff --git a/apps/web-tdesign/src/api/request.ts b/apps/web-tdesign/src/api/request.ts index 86b181d..4c00830 100644 --- a/apps/web-tdesign/src/api/request.ts +++ b/apps/web-tdesign/src/api/request.ts @@ -46,13 +46,20 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) { /** * 刷新token逻辑 + * 后端为旋转令牌:旧 refreshToken 单次有效,响应返回新令牌对。 + * baseRequestClient 为 raw 模式(axios response),body 在 resp.data, + * 信封为 {code,message,data},令牌对在 resp.data.data。 */ async function doRefreshToken() { const accessStore = useAccessStore(); - const resp = await refreshTokenApi(); - const newToken = resp.data; - accessStore.setAccessToken(newToken); - return newToken; + const resp = await refreshTokenApi(accessStore.refreshToken ?? ''); + const pair = resp.data?.data; + if (!pair?.accessToken) { + throw new Error('refresh token response invalid'); + } + accessStore.setAccessToken(pair.accessToken); + accessStore.setRefreshToken(pair.refreshToken); + return pair.accessToken; } function formatToken(token: null | string) { diff --git a/apps/web-tdesign/src/store/auth.ts b/apps/web-tdesign/src/store/auth.ts index 7f8d8f9..bef7c71 100644 --- a/apps/web-tdesign/src/store/auth.ts +++ b/apps/web-tdesign/src/store/auth.ts @@ -29,15 +29,17 @@ export const useAuthStore = defineStore('auth', () => { params: Recordable, onSuccess?: () => Promise | void, ) { - // 异步处理用户登录操作并获取 accessToken + // 异步处理用户登录操作并获取令牌对(accessToken + refreshToken) let userInfo: null | UserInfo = null; try { loginLoading.value = true; - const { accessToken } = await loginApi(params); + const { accessToken, refreshToken } = await loginApi(params); // 如果成功获取到 accessToken if (accessToken) { accessStore.setAccessToken(accessToken); + // 刷新令牌持久化(pinia persist),供 access token 过期时静默续期 + accessStore.setRefreshToken(refreshToken); // 获取用户信息并存储到 accessStore 中 const [fetchUserInfoResult, accessCodes] = await Promise.all([ fetchUserInfo(), @@ -78,7 +80,9 @@ export const useAuthStore = defineStore('auth', () => { async function logout(redirect: boolean = true) { try { - await logoutApi(); + // 携带 refreshToken 让后端撤销刷新会话(阻止该设备继续续期); + // 后端幂等,即使令牌无效也返回成功,失败不阻塞本地登出。 + await logoutApi(accessStore.refreshToken ?? undefined); } catch { // 不做任何处理 }