fix(pda): revoke expired sessions before completing logout
This commit is contained in:
+11
-2
@@ -10,9 +10,10 @@ function authRequest(path, data, options = {}) {
|
||||
timeout: options.timeout || 15000,
|
||||
success(response) {
|
||||
const body = response.data || {}
|
||||
if (String(body.code ?? response.statusCode) !== '200') {
|
||||
if (response.statusCode !== 200 || String(body.code ?? response.statusCode) !== '200') {
|
||||
const error = new Error(body.message || '认证服务请求失败')
|
||||
error.code = body.code || response.statusCode
|
||||
error.statusCode = response.statusCode
|
||||
error.requestId = body.requestId || response.header?.['X-Request-Id'] || response.header?.['x-request-id']
|
||||
error.traceId = body.traceId || response.header?.['X-Trace-Id'] || response.header?.['x-trace-id']
|
||||
reject(error)
|
||||
@@ -33,9 +34,17 @@ export function login(loginId, password) {
|
||||
return authRequest('/auth/login', { loginId, password })
|
||||
}
|
||||
|
||||
const pendingRefreshes = new Map()
|
||||
export function refresh(refreshToken) {
|
||||
return authRequest('/auth/refresh', { refreshToken })
|
||||
if (pendingRefreshes.has(refreshToken)) return pendingRefreshes.get(refreshToken)
|
||||
const pending = authRequest('/auth/refresh', { refreshToken }).finally(() => {
|
||||
if (pendingRefreshes.get(refreshToken) === pending) pendingRefreshes.delete(refreshToken)
|
||||
})
|
||||
pendingRefreshes.set(refreshToken, pending)
|
||||
return pending
|
||||
}
|
||||
export function getPendingRefresh(refreshToken) { return pendingRefreshes.get(refreshToken) || null }
|
||||
|
||||
|
||||
export function logout(refreshToken, accessToken) {
|
||||
return authRequest('/auth/logout', { refreshToken }, {
|
||||
|
||||
+20
-3
@@ -1,8 +1,9 @@
|
||||
import { refresh as refreshRequest } from '@/api/auth.js'
|
||||
import { isAccessTokenExpiring, session, signOut, updateTokenPair } from '@/stores/session.js'
|
||||
import { isAccessTokenExpiring, session, signOut, updateTokenPair, getSessionRevision } from '@/stores/session.js'
|
||||
import { requireOnline } from '@/utils/network.js'
|
||||
|
||||
let refreshPromise = null
|
||||
let refreshRevision = null
|
||||
let redirectingToLogin = false
|
||||
|
||||
function responseCode(response) {
|
||||
@@ -66,14 +67,21 @@ function rawRequest(options, accessToken) {
|
||||
})
|
||||
}
|
||||
|
||||
function requireSameSession(revision) {
|
||||
if (getSessionRevision() !== revision) throw new Error('登录会话已变更,请重新操作')
|
||||
}
|
||||
|
||||
async function refreshAccessToken() {
|
||||
const revision = getSessionRevision()
|
||||
const observedRefreshToken = session.refreshToken
|
||||
if (!observedRefreshToken) return expireSession(null, observedRefreshToken)
|
||||
if (!refreshPromise) {
|
||||
if (!refreshPromise || refreshRevision !== revision) {
|
||||
refreshRevision = revision
|
||||
refreshPromise = (async () => {
|
||||
if (session.refreshToken !== observedRefreshToken && session.token) return session.token
|
||||
try {
|
||||
const tokens = await refreshRequest(observedRefreshToken)
|
||||
requireSameSession(revision)
|
||||
if (session.refreshToken !== observedRefreshToken) {
|
||||
if (session.token) return session.token
|
||||
throw sessionExpiredError()
|
||||
@@ -82,10 +90,14 @@ async function refreshAccessToken() {
|
||||
redirectingToLogin = false
|
||||
return session.token
|
||||
} catch (error) {
|
||||
requireSameSession(revision)
|
||||
if (isUnauthorized(error)) return expireSession(error, observedRefreshToken)
|
||||
throw error
|
||||
}
|
||||
})().finally(() => { refreshPromise = null })
|
||||
})()
|
||||
const pending = refreshPromise
|
||||
const release = () => { if (refreshPromise === pending) { refreshPromise = null; refreshRevision = null } }
|
||||
pending.then(release, release) // Original pending promise still propagates failures to callers.
|
||||
}
|
||||
return refreshPromise
|
||||
}
|
||||
@@ -101,12 +113,17 @@ export function ensureAccessToken() {
|
||||
|
||||
async function runAuthenticated(execute) {
|
||||
requireOnline()
|
||||
const revision = getSessionRevision()
|
||||
let accessToken = await validAccessToken()
|
||||
requireSameSession(revision)
|
||||
let response = await execute(accessToken)
|
||||
requireSameSession(revision)
|
||||
if (!isUnauthorized(response)) return response
|
||||
accessToken = session.token && session.token !== accessToken ? session.token : await refreshAccessToken()
|
||||
requireSameSession(revision)
|
||||
const activeRefreshToken = session.refreshToken
|
||||
response = await execute(accessToken)
|
||||
requireSameSession(revision)
|
||||
if (isUnauthorized(response)) expireSession(response, activeRefreshToken)
|
||||
return response
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { logout, refresh, getPendingRefresh } from './auth.js'
|
||||
import { session, signOut, getSessionRevision, decodeAccessToken } from '../stores/session.js'
|
||||
|
||||
function replacement(captured, tokens) {
|
||||
if (!tokens?.accessToken || typeof tokens.refreshToken !== 'string' || !tokens.refreshToken.trim()) throw new Error('退出刷新响应缺少会话凭据')
|
||||
const previous = decodeAccessToken(captured.accessToken), next = decodeAccessToken(tokens.accessToken)
|
||||
if (!Number.isFinite(Number(next.exp)) || Number(next.exp) * 1000 <= Date.now()) throw new Error('退出刷新凭据无效或已过期')
|
||||
for (const field of ['user_id', 'membership_id', 'tenant_id', 'app_code']) {
|
||||
if (previous[field] == null || String(previous[field]) !== String(next[field])) throw new Error('退出刷新返回了不同作业主体,无法确认原会话撤销')
|
||||
}
|
||||
if (String(previous.domestic_warehouse_id ?? '') !== String(next.domestic_warehouse_id ?? '')) throw new Error('退出刷新返回了不同作业仓库,无法确认原会话撤销')
|
||||
return { accessToken: tokens.accessToken, refreshToken: tokens.refreshToken }
|
||||
}
|
||||
export function logoutCurrentSession() {
|
||||
const captured = { accessToken: session.token, refreshToken: session.refreshToken, pendingRefresh: getPendingRefresh(session.refreshToken) }
|
||||
// Clear sensitive data and invalidate business requests while revocation is pending.
|
||||
signOut()
|
||||
const clearedRevision = getSessionRevision()
|
||||
const revoke = async () => {
|
||||
if (!captured.accessToken && !captured.refreshToken) return { revoked: false, reason: 'NO_CAPTURED_SESSION' }
|
||||
if (!captured.accessToken || !captured.refreshToken) throw new Error('原会话凭据不完整,服务端退出未确认')
|
||||
let credentials = captured, refreshed = false
|
||||
if (captured.pendingRefresh) { refreshed = true; credentials = replacement(captured, await captured.pendingRefresh) }
|
||||
try { await logout(credentials.refreshToken, credentials.accessToken) }
|
||||
catch (error) {
|
||||
if (error.network || error.statusCode !== 401 || refreshed) throw error
|
||||
credentials = replacement(captured, await refresh(captured.refreshToken))
|
||||
await logout(credentials.refreshToken, credentials.accessToken)
|
||||
}
|
||||
return { revoked: true }
|
||||
}
|
||||
return revoke().then(result => ({ ...result, clearedRevision }), error => { error.logoutRevision = clearedRevision; throw error })
|
||||
}
|
||||
Reference in New Issue
Block a user