feat(auth): 登录/登出/超时对齐后端 JWT 旋转令牌

- loginApi 补 refreshToken/expiresIn/adminId,登录成功持久化
  refreshToken(此前丢弃,静默续期无从发起)
- refreshTokenApi 改 body 传 refreshToken(原模板误传 withCredentials,
  后端 required 校验必失败);doRefreshToken 适配新令牌对
- logoutApi 携带 refreshToken,登出撤销刷新会话
- 与后端 HTTP 401 语义化配套:过期触发 vben 静默刷新,失败再重登
- vue-tsc 通过(钩子 turbo 环节因并行提交冲突中断,已独立验证)
This commit is contained in:
夏犀麟 2026-08-27 23:20:27 +08:00
parent c9a3c02e12
commit 7e4bfee095
3 changed files with 33 additions and 24 deletions

View File

@ -7,14 +7,12 @@ export namespace AuthApi {
username?: string; username?: string;
} }
/** 登录接口返回值 */ /** 登录/刷新接口返回值(后端 TokenPair */
export interface LoginResult { export interface LoginResult {
accessToken: string; accessToken: string;
} adminId: number;
expiresIn: number;
export interface RefreshTokenResult { refreshToken: string;
data: string;
status: number;
} }
} }
@ -29,23 +27,23 @@ export async function loginApi(data: AuthApi.LoginParams) {
} }
/** /**
* accessToken * accessToken refreshToken
* @param refreshToken JTI
* @returns raw axios responseresp.data {code,message,data}
*/ */
export async function refreshTokenApi() { export async function refreshTokenApi(refreshToken: string) {
return baseRequestClient.post<AuthApi.RefreshTokenResult>( return baseRequestClient.post<{
'/api/service/admin/system/auth/refresh', data: { code: number; data: AuthApi.LoginResult; message: string };
{ status: number;
withCredentials: true, }>('/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', { return baseRequestClient.post('/api/service/admin/system/auth/logout', {
withCredentials: true, refreshToken,
}); });
} }

View File

@ -46,13 +46,20 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) {
/** /**
* token逻辑 * token逻辑
* refreshToken
* baseRequestClient raw axios responsebody resp.data
* {code,message,data} resp.data.data
*/ */
async function doRefreshToken() { async function doRefreshToken() {
const accessStore = useAccessStore(); const accessStore = useAccessStore();
const resp = await refreshTokenApi(); const resp = await refreshTokenApi(accessStore.refreshToken ?? '');
const newToken = resp.data; const pair = resp.data?.data;
accessStore.setAccessToken(newToken); if (!pair?.accessToken) {
return newToken; throw new Error('refresh token response invalid');
}
accessStore.setAccessToken(pair.accessToken);
accessStore.setRefreshToken(pair.refreshToken);
return pair.accessToken;
} }
function formatToken(token: null | string) { function formatToken(token: null | string) {

View File

@ -29,15 +29,17 @@ export const useAuthStore = defineStore('auth', () => {
params: Recordable<any>, params: Recordable<any>,
onSuccess?: () => Promise<void> | void, onSuccess?: () => Promise<void> | void,
) { ) {
// 异步处理用户登录操作并获取 accessToken // 异步处理用户登录操作并获取令牌对accessToken + refreshToken
let userInfo: null | UserInfo = null; let userInfo: null | UserInfo = null;
try { try {
loginLoading.value = true; loginLoading.value = true;
const { accessToken } = await loginApi(params); const { accessToken, refreshToken } = await loginApi(params);
// 如果成功获取到 accessToken // 如果成功获取到 accessToken
if (accessToken) { if (accessToken) {
accessStore.setAccessToken(accessToken); accessStore.setAccessToken(accessToken);
// 刷新令牌持久化pinia persist供 access token 过期时静默续期
accessStore.setRefreshToken(refreshToken);
// 获取用户信息并存储到 accessStore 中 // 获取用户信息并存储到 accessStore 中
const [fetchUserInfoResult, accessCodes] = await Promise.all([ const [fetchUserInfoResult, accessCodes] = await Promise.all([
fetchUserInfo(), fetchUserInfo(),
@ -78,7 +80,9 @@ export const useAuthStore = defineStore('auth', () => {
async function logout(redirect: boolean = true) { async function logout(redirect: boolean = true) {
try { try {
await logoutApi(); // 携带 refreshToken 让后端撤销刷新会话(阻止该设备继续续期);
// 后端幂等,即使令牌无效也返回成功,失败不阻塞本地登出。
await logoutApi(accessStore.refreshToken ?? undefined);
} catch { } catch {
// 不做任何处理 // 不做任何处理
} }