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;
}
/** 登录接口返回值 */
/** 登录/刷新接口返回值(后端 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 responseresp.data {code,message,data}
*/
export async function refreshTokenApi() {
return baseRequestClient.post<AuthApi.RefreshTokenResult>(
'/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,
});
}

View File

@ -46,13 +46,20 @@ function createRequestClient(baseURL: string, options?: RequestClientOptions) {
/**
* token逻辑
* refreshToken
* baseRequestClient raw axios responsebody 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) {

View File

@ -29,15 +29,17 @@ export const useAuthStore = defineStore('auth', () => {
params: Recordable<any>,
onSuccess?: () => Promise<void> | 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 {
// 不做任何处理
}