feat: 刷新 Access Token(带并发锁)

This commit is contained in:
2026-06-24 12:27:55 +08:00
parent 7b3d31a57c
commit dd97f9cb70
2 changed files with 34 additions and 18 deletions

View File

@@ -7,9 +7,6 @@ import { useAccountStore } from '@/stores'
/** SSE 连接 URL */ /** SSE 连接 URL */
const SSE_URL = '/api/sse/subscribe' const SSE_URL = '/api/sse/subscribe'
/** 是否正在刷新 token防并发 */
let isRefreshing = false
// ==================== 模块级单例 ==================== // ==================== 模块级单例 ====================
const isConnected = ref(false) const isConnected = ref(false)
@@ -37,16 +34,19 @@ const client = new SseClient(SSE_URL, () => useAccountStore().accessToken, {
// ==================== Token 刷新 ==================== // ==================== Token 刷新 ====================
/**
* SSE 收到 401 → 调用 store 的 refreshAccessToken() 刷新 token 后重连
* <p>
* 无需维护独立并发锁accountStore.refreshAccessToken() 内部已有 Promise 级锁,
* 若 HTTP 刷新拦截器已在刷新中,此调用会复用同一个 Promise确保只发一次请求。
*/
async function refreshTokenAndReconnect(): Promise<void> { async function refreshTokenAndReconnect(): Promise<void> {
if (isRefreshing) return
const accountStore = useAccountStore() const accountStore = useAccountStore()
if (!accountStore.refreshToken) { if (!accountStore.refreshToken) {
sseBus.emit('error', { message: '登录已过期,请重新登录', retryCount: 0 }) sseBus.emit('error', { message: '登录已过期,请重新登录', retryCount: 0 })
return return
} }
isRefreshing = true
try { try {
const newToken = await accountStore.refreshAccessToken() const newToken = await accountStore.refreshAccessToken()
if (newToken) { if (newToken) {
@@ -54,8 +54,6 @@ async function refreshTokenAndReconnect(): Promise<void> {
} }
} catch { } catch {
sseBus.emit('error', { message: '实时连接异常,请刷新页面', retryCount: 0 }) sseBus.emit('error', { message: '实时连接异常,请刷新页面', retryCount: 0 })
} finally {
isRefreshing = false
} }
} }

View File

@@ -95,26 +95,44 @@ export const useAccountStore = defineStore(
} }
/** /**
* 刷新 Access Token * 刷新 Access Token(带并发锁)
* *
* 由刷新拦截器的 doRefreshToken 调用。 * 由刷新拦截器的 doRefreshToken 和 SSE 的 onUnauthorized 调用。
* 返回新的 Access Token 字符串,方便拦截器直接使用。 * 返回新的 Access Token 字符串,方便调用方直接使用。
*
* 并发控制:若已有刷新正在进行中,返回同一个 Promise
* 确保无论并发触发多少次,实际只发出一次 /account/refresh 请求。
* *
* @returns 新的 Access Token * @returns 新的 Access Token
*/ */
/** 正在进行的刷新 Promisenull 表示无刷新进行中) */
let refreshPromise: Promise<string> | null = null
const refreshAccessToken = async (): Promise<string> => { const refreshAccessToken = async (): Promise<string> => {
// 已有刷新进行中 → 复用同一个 Promise避免并发重复请求
if (refreshPromise) {
return refreshPromise
}
if (!refreshToken.value) { if (!refreshToken.value) {
throw new Error('无 Refresh Token无法刷新') throw new Error('无 Refresh Token无法刷新')
} }
const res = await refreshApi({ refreshToken: refreshToken.value }) refreshPromise = (async () => {
accessToken.value = res.data.accessToken try {
if (res.data.refreshToken) { const res = await refreshApi({ refreshToken: refreshToken.value })
refreshToken.value = res.data.refreshToken accessToken.value = res.data.accessToken
} if (res.data.refreshToken) {
isTokenValid.value = true refreshToken.value = res.data.refreshToken
}
isTokenValid.value = true
return accessToken.value
} finally {
refreshPromise = null
}
})()
return accessToken.value return refreshPromise
} }
/** /**