feat(auth): add opaque bearer sessions
This commit is contained in:
@@ -8,7 +8,10 @@ import {
|
||||
testBootstrapDatabase,
|
||||
testBootstrapRuntime
|
||||
} from '../services/bootstrap.js'
|
||||
import { fetchCurrentAuthUser, login as loginByAccount } from '../services/auth.js'
|
||||
import {
|
||||
fetchCurrentAuthUser,
|
||||
login as loginByAccount
|
||||
} from '../services/auth.js'
|
||||
import { setRuntimeApiBaseUrl } from '../services/api.js'
|
||||
import { checkBackendHealth } from './useBackendHealth.js'
|
||||
import { resolveDefaultAuthorizedRoute } from '../utils/accessControl.js'
|
||||
@@ -16,15 +19,22 @@ import { useToast } from './useToast.js'
|
||||
import { fetchSettings } from '../services/settings.js'
|
||||
import { setThemeSkin } from './useThemeSkin.js'
|
||||
import { normalizeAuthUserSnapshot, resolveAuthUserAdminFlag } from '../utils/authUser.js'
|
||||
import {
|
||||
AUTH_SESSION_EXPIRED_EVENT,
|
||||
clearAuthCredentials,
|
||||
hasValidAuthSession,
|
||||
persistAuthCredentials,
|
||||
readAuthAccessToken,
|
||||
readAuthExpiresAt
|
||||
} from '../utils/authSessionStorage.js'
|
||||
import {
|
||||
clearAuthSessionMetrics,
|
||||
finalizeAndRevokeAuthSession,
|
||||
finalizeAuthSession,
|
||||
incrementAuthActivityCount,
|
||||
persistAuthSessionMetrics
|
||||
} from '../utils/authSessionMetrics.js'
|
||||
|
||||
const AUTH_STORAGE_KEY = 'x-financial-authenticated'
|
||||
const AUTH_USERNAME_KEY = 'x-financial-auth-username'
|
||||
const AUTH_USER_KEY = 'x-financial-auth-user'
|
||||
const AUTH_LAST_ACTIVITY_KEY = 'x-financial-auth-last-activity'
|
||||
const DEFAULT_USER_NAME = '系统管理员'
|
||||
@@ -78,20 +88,8 @@ function readClientBootstrapState() {
|
||||
}
|
||||
}
|
||||
|
||||
function readAuthState() {
|
||||
if (typeof window === 'undefined') {
|
||||
return false
|
||||
}
|
||||
|
||||
return window.sessionStorage.getItem(AUTH_STORAGE_KEY) === 'true'
|
||||
}
|
||||
|
||||
function readStoredUsername() {
|
||||
if (typeof window === 'undefined') {
|
||||
return ''
|
||||
}
|
||||
|
||||
return window.sessionStorage.getItem(AUTH_USERNAME_KEY) || ''
|
||||
function readAuthState() {
|
||||
return hasValidAuthSession()
|
||||
}
|
||||
|
||||
function buildAnonymousUser() {
|
||||
@@ -116,31 +114,6 @@ function buildAnonymousUser() {
|
||||
}
|
||||
}
|
||||
|
||||
function buildLegacyAdminUser(username = '') {
|
||||
const normalized = String(username || '').trim()
|
||||
const name = normalized || DEFAULT_USER_NAME
|
||||
|
||||
return {
|
||||
username: normalized,
|
||||
name,
|
||||
role: DEFAULT_USER_ROLE,
|
||||
department: '',
|
||||
departmentName: '',
|
||||
position: DEFAULT_USER_ROLE,
|
||||
grade: '',
|
||||
employeeNo: '',
|
||||
managerName: '',
|
||||
location: '',
|
||||
costCenter: '',
|
||||
financeOwnerName: '',
|
||||
riskProfile: {},
|
||||
roleCodes: ['manager'],
|
||||
email: '',
|
||||
avatar: name.slice(0, 1).toUpperCase(),
|
||||
isAdmin: true
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePlatformAdminFlag(payload, roleCodes = []) {
|
||||
return resolveAuthUserAdminFlag(payload, roleCodes)
|
||||
}
|
||||
@@ -171,12 +144,11 @@ function readStoredUser() {
|
||||
return normalizeStoredAuthUser(payload)
|
||||
}
|
||||
} catch {
|
||||
return buildLegacyAdminUser(readStoredUsername())
|
||||
}
|
||||
}
|
||||
|
||||
const legacyUsername = readStoredUsername()
|
||||
return legacyUsername ? buildLegacyAdminUser(legacyUsername) : buildAnonymousUser()
|
||||
return buildAnonymousUser()
|
||||
}
|
||||
}
|
||||
|
||||
return buildAnonymousUser()
|
||||
}
|
||||
|
||||
function readLastActivityAt() {
|
||||
@@ -187,10 +159,14 @@ function readLastActivityAt() {
|
||||
return Number(window.sessionStorage.getItem(AUTH_LAST_ACTIVITY_KEY) || 0)
|
||||
}
|
||||
|
||||
function isSessionExpired(now = Date.now()) {
|
||||
if (!readAuthState()) {
|
||||
return false
|
||||
}
|
||||
function isSessionExpired(now = Date.now()) {
|
||||
if (!readAuthAccessToken()) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!hasValidAuthSession(now)) {
|
||||
return true
|
||||
}
|
||||
|
||||
const lastActivityAt = readLastActivityAt()
|
||||
|
||||
@@ -201,24 +177,24 @@ function isSessionExpired(now = Date.now()) {
|
||||
return now - lastActivityAt > authIdleTimeoutMs
|
||||
}
|
||||
|
||||
function persistAuthState(value, user = null, sessionId = '') {
|
||||
function persistAuthState(value, user = null, sessionId = '', accessToken = '', expiresAt = '') {
|
||||
if (typeof window === 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
if (value) {
|
||||
window.sessionStorage.setItem(AUTH_STORAGE_KEY, 'true')
|
||||
if (value) {
|
||||
const normalizedUser = user || buildAnonymousUser()
|
||||
window.sessionStorage.setItem(AUTH_USERNAME_KEY, String(normalizedUser.username || '').trim())
|
||||
window.sessionStorage.setItem(AUTH_USER_KEY, JSON.stringify(normalizedUser))
|
||||
persistAuthCredentials(accessToken, expiresAt)
|
||||
persistAuthSessionMetrics(sessionId)
|
||||
return
|
||||
}
|
||||
|
||||
window.sessionStorage.removeItem(AUTH_STORAGE_KEY)
|
||||
window.sessionStorage.removeItem(AUTH_USERNAME_KEY)
|
||||
|
||||
window.sessionStorage.removeItem(AUTH_USER_KEY)
|
||||
window.sessionStorage.removeItem(AUTH_LAST_ACTIVITY_KEY)
|
||||
window.sessionStorage.removeItem('x-financial-authenticated')
|
||||
window.sessionStorage.removeItem('x-financial-auth-username')
|
||||
clearAuthCredentials()
|
||||
clearAuthSessionMetrics()
|
||||
}
|
||||
|
||||
@@ -228,7 +204,6 @@ function persistAuthUserSnapshot(user = {}) {
|
||||
}
|
||||
|
||||
const normalizedUser = user || buildAnonymousUser()
|
||||
window.sessionStorage.setItem(AUTH_USERNAME_KEY, String(normalizedUser.username || '').trim())
|
||||
window.sessionStorage.setItem(AUTH_USER_KEY, JSON.stringify(normalizedUser))
|
||||
}
|
||||
|
||||
@@ -269,7 +244,9 @@ function scheduleSessionTimeout() {
|
||||
return
|
||||
}
|
||||
|
||||
const remaining = authIdleTimeoutMs - (Date.now() - lastActivityAt)
|
||||
const idleRemaining = authIdleTimeoutMs - (Date.now() - lastActivityAt)
|
||||
const tokenRemaining = readAuthExpiresAt() - Date.now()
|
||||
const remaining = Math.min(idleRemaining, tokenRemaining)
|
||||
|
||||
if (remaining <= 0) {
|
||||
logout('timeout', { notify: true })
|
||||
@@ -307,6 +284,13 @@ function handleSessionActivity(event) {
|
||||
touchAuthActivity()
|
||||
}
|
||||
|
||||
function handleAuthSessionExpired() {
|
||||
if (!readAuthAccessToken()) {
|
||||
return
|
||||
}
|
||||
logout('expired', { notify: true, revoke: false })
|
||||
}
|
||||
|
||||
function handleSessionUnload(event) {
|
||||
if (event?.type === 'pagehide' && event.persisted) {
|
||||
return
|
||||
@@ -323,6 +307,7 @@ function installSessionMonitoring() {
|
||||
SESSION_ACTIVITY_EVENTS.forEach((eventName) => {
|
||||
window.addEventListener(eventName, handleSessionActivity, { passive: true })
|
||||
})
|
||||
window.addEventListener(AUTH_SESSION_EXPIRED_EVENT, handleAuthSessionExpired)
|
||||
window.addEventListener('pagehide', handleSessionUnload, { passive: true })
|
||||
window.addEventListener('beforeunload', handleSessionUnload, { passive: true })
|
||||
}
|
||||
@@ -428,11 +413,11 @@ const runtimeTestMessage = ref('')
|
||||
const databaseTestMessage = ref('')
|
||||
const loginSubmitting = ref(false)
|
||||
const loginError = ref('')
|
||||
const loggedIn = ref(readAuthState() && !isSessionExpired())
|
||||
const loggedIn = ref(readAuthState() && !isSessionExpired())
|
||||
const currentUser = ref(readStoredUser())
|
||||
|
||||
if (!loggedIn.value && readAuthState()) {
|
||||
persistAuthState(false)
|
||||
if (!loggedIn.value && readAuthAccessToken()) {
|
||||
persistAuthState(false)
|
||||
}
|
||||
|
||||
const { toast } = useToast()
|
||||
@@ -674,7 +659,13 @@ async function handleLogin(credentials) {
|
||||
isAdmin: resolvePlatformAdminFlag(responseUser, responseRoleCodes)
|
||||
}
|
||||
loggedIn.value = true
|
||||
persistAuthState(true, user, response?.sessionId || '')
|
||||
persistAuthState(
|
||||
true,
|
||||
user,
|
||||
response?.sessionId || '',
|
||||
response?.accessToken || '',
|
||||
response?.expiresAt || ''
|
||||
)
|
||||
currentUser.value = user
|
||||
touchAuthActivity(true)
|
||||
return true
|
||||
@@ -688,18 +679,27 @@ async function handleLogin(credentials) {
|
||||
}
|
||||
}
|
||||
|
||||
function logout(reason = 'manual', options = {}) {
|
||||
function logout(reason = 'manual', options = {}) {
|
||||
const notify = options.notify ?? reason === 'timeout'
|
||||
const redirect = options.redirect ?? reason !== 'invalid'
|
||||
const revoke = options.revoke ?? (reason !== 'invalid' && reason !== 'expired')
|
||||
|
||||
finalizeAuthSession(reason)
|
||||
if (revoke && readAuthAccessToken()) {
|
||||
finalizeAndRevokeAuthSession(reason).catch((error) => {
|
||||
console.warn('Failed to revoke auth session:', error)
|
||||
})
|
||||
}
|
||||
loggedIn.value = false
|
||||
persistAuthState(false)
|
||||
currentUser.value = buildAnonymousUser()
|
||||
clearSessionTimeout()
|
||||
|
||||
if (notify) {
|
||||
toast(reason === 'timeout' ? '登录已超时,请重新登录。' : '已退出登录。')
|
||||
if (notify) {
|
||||
toast(
|
||||
reason === 'timeout' || reason === 'expired'
|
||||
? '登录已失效,请重新登录。'
|
||||
: '已退出登录。'
|
||||
)
|
||||
}
|
||||
|
||||
if (redirect) {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { normalizeAuthUserSnapshot, resolveAuthUserAdminFlag } from '../utils/authUser.js'
|
||||
import {
|
||||
buildBearerHeaders,
|
||||
notifyAuthSessionExpired,
|
||||
readAuthAccessToken
|
||||
} from '../utils/authSessionStorage.js'
|
||||
|
||||
const API_BASE_STORAGE_KEY = 'x-financial-api-base-url'
|
||||
const AUTH_USER_STORAGE_KEY = 'x-financial-auth-user'
|
||||
|
||||
function isHeaderValueSafe(value) {
|
||||
const normalized = String(value || '').trim()
|
||||
@@ -33,85 +36,6 @@ export function pickSafeHeaderValue(value, fallback = '') {
|
||||
return ''
|
||||
}
|
||||
|
||||
function readCurrentUserHeaders() {
|
||||
if (typeof window === 'undefined') {
|
||||
return {}
|
||||
}
|
||||
|
||||
const raw = window.sessionStorage.getItem(AUTH_USER_STORAGE_KEY)
|
||||
if (!raw) {
|
||||
return {}
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = JSON.parse(raw)
|
||||
const user = normalizeAuthUserSnapshot(payload)
|
||||
const username = user.username
|
||||
const name = user.name || username
|
||||
const roleCodes = user.roleCodes
|
||||
const isAdmin = resolveAuthUserAdminFlag(payload, roleCodes)
|
||||
const department = user.department || user.departmentName
|
||||
const costCenter = user.costCenter
|
||||
const position = user.position
|
||||
const grade = user.grade
|
||||
const employeeNo = user.employeeNo
|
||||
const managerName = user.managerName
|
||||
const safeUsername = pickSafeHeaderValue(username)
|
||||
const safeName = pickSafeHeaderValue(name)
|
||||
const safeDepartment = pickSafeHeaderValue(department)
|
||||
const safeCostCenter = pickSafeHeaderValue(costCenter)
|
||||
const safePosition = pickSafeHeaderValue(position)
|
||||
const safeGrade = pickSafeHeaderValue(grade)
|
||||
const safeEmployeeNo = pickSafeHeaderValue(employeeNo)
|
||||
const safeManagerName = pickSafeHeaderValue(managerName)
|
||||
|
||||
if (!safeUsername && !safeName) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const headers = {
|
||||
'x-auth-role-codes': roleCodes.join(','),
|
||||
'x-auth-is-admin': String(isAdmin)
|
||||
}
|
||||
|
||||
if (safeUsername) {
|
||||
headers['x-auth-username'] = safeUsername
|
||||
}
|
||||
|
||||
if (safeName) {
|
||||
headers['x-auth-name'] = safeName
|
||||
}
|
||||
|
||||
if (safeDepartment) {
|
||||
headers['x-auth-department'] = safeDepartment
|
||||
}
|
||||
|
||||
if (safeCostCenter) {
|
||||
headers['x-auth-cost-center'] = safeCostCenter
|
||||
}
|
||||
|
||||
if (safePosition) {
|
||||
headers['x-auth-position'] = safePosition
|
||||
}
|
||||
|
||||
if (safeGrade) {
|
||||
headers['x-auth-grade'] = safeGrade
|
||||
}
|
||||
|
||||
if (safeEmployeeNo) {
|
||||
headers['x-auth-employee-no'] = safeEmployeeNo
|
||||
}
|
||||
|
||||
if (safeManagerName) {
|
||||
headers['x-auth-manager-name'] = safeManagerName
|
||||
}
|
||||
|
||||
return headers
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeApiBaseUrl(value) {
|
||||
return String(value || '/api/v1').replace(/\/$/, '')
|
||||
}
|
||||
@@ -260,8 +184,20 @@ function sanitizeHeaders(headers) {
|
||||
return nextHeaders
|
||||
}
|
||||
|
||||
function buildApiResponseError(response, payload, { auth, handleUnauthorized }) {
|
||||
const error = new Error(resolveErrorMessage(payload))
|
||||
error.status = response.status
|
||||
if (response.status === 401 && auth && handleUnauthorized && readAuthAccessToken()) {
|
||||
error.code = 'AUTH_SESSION_EXPIRED'
|
||||
notifyAuthSessionExpired()
|
||||
}
|
||||
return error
|
||||
}
|
||||
|
||||
export async function apiRequest(path, options = {}) {
|
||||
const {
|
||||
auth = true,
|
||||
handleUnauthorized = true,
|
||||
contentType = 'application/json',
|
||||
responseType = 'json',
|
||||
headers: customHeaders,
|
||||
@@ -270,10 +206,15 @@ export async function apiRequest(path, options = {}) {
|
||||
...fetchOptions
|
||||
} = options
|
||||
|
||||
const headers = sanitizeHeaders({
|
||||
...readCurrentUserHeaders(),
|
||||
...(customHeaders || {})
|
||||
})
|
||||
const headers = sanitizeHeaders(customHeaders || {})
|
||||
if (auth) {
|
||||
Object.keys(headers).forEach((key) => {
|
||||
if (key.toLowerCase() === 'authorization') {
|
||||
delete headers[key]
|
||||
}
|
||||
})
|
||||
Object.assign(headers, buildBearerHeaders())
|
||||
}
|
||||
|
||||
if (contentType !== null && typeof headers['Content-Type'] === 'undefined') {
|
||||
headers['Content-Type'] = contentType
|
||||
@@ -327,7 +268,7 @@ export async function apiRequest(path, options = {}) {
|
||||
payload = null
|
||||
}
|
||||
|
||||
throw new Error(resolveErrorMessage(payload))
|
||||
throw buildApiResponseError(response, payload, { auth, handleUnauthorized })
|
||||
}
|
||||
|
||||
return response.blob()
|
||||
@@ -341,7 +282,7 @@ export async function apiRequest(path, options = {}) {
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(resolveErrorMessage(payload))
|
||||
throw buildApiResponseError(response, payload, { auth, handleUnauthorized })
|
||||
}
|
||||
|
||||
return payload
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { apiRequest, getRuntimeApiBaseUrl } from './api.js'
|
||||
import { buildBearerHeaders } from '../utils/authSessionStorage.js'
|
||||
|
||||
export function login(payload) {
|
||||
return apiRequest('/auth/login', {
|
||||
auth: false,
|
||||
handleUnauthorized: false,
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
@@ -11,6 +14,14 @@ export function fetchCurrentAuthUser() {
|
||||
return apiRequest('/auth/me')
|
||||
}
|
||||
|
||||
export function revokeCurrentAuthSession(payload) {
|
||||
return apiRequest('/auth/logout', {
|
||||
method: 'POST',
|
||||
handleUnauthorized: false,
|
||||
body: JSON.stringify(payload || {})
|
||||
})
|
||||
}
|
||||
|
||||
export function finishSession(sessionId, payload) {
|
||||
return apiRequest(`/auth/sessions/${encodeURIComponent(sessionId)}/finish`, {
|
||||
method: 'POST',
|
||||
@@ -29,13 +40,12 @@ export function finishSessionOnUnload(sessionId, payload) {
|
||||
const url = `${getRuntimeApiBaseUrl()}/auth/sessions/${encodeURIComponent(normalizedSessionId)}/finish`
|
||||
const body = JSON.stringify(payload || {})
|
||||
|
||||
if (typeof window.navigator?.sendBeacon === 'function') {
|
||||
return window.navigator.sendBeacon(url, new Blob([body], { type: 'application/json' }))
|
||||
}
|
||||
|
||||
window.fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...buildBearerHeaders()
|
||||
},
|
||||
body,
|
||||
keepalive: true
|
||||
}).catch(() => {})
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { apiRequest, getRuntimeApiBaseUrl } from './api.js'
|
||||
import {
|
||||
buildBearerHeaders,
|
||||
notifyAuthSessionExpired
|
||||
} from '../utils/authSessionStorage.js'
|
||||
|
||||
export function fetchStewardPlan(payload, options = {}) {
|
||||
return apiRequest('/steward/plans', {
|
||||
@@ -63,7 +67,8 @@ export async function fetchStewardPlanStream(payload, handlers = {}, options = {
|
||||
response = await fetch(`${getRuntimeApiBaseUrl()}/steward/plans/stream`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
...buildBearerHeaders()
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
signal: controller?.signal
|
||||
@@ -78,6 +83,9 @@ export async function fetchStewardPlanStream(payload, handlers = {}, options = {
|
||||
|
||||
if (!response.ok) {
|
||||
clearAbortTimer()
|
||||
if (response.status === 401) {
|
||||
notifyAuthSessionExpired()
|
||||
}
|
||||
throw new Error(await resolveStreamError(response))
|
||||
}
|
||||
|
||||
|
||||
@@ -242,6 +242,10 @@ export function canAccessAppView(user, viewId) {
|
||||
return VIEW_ROLE_RULES.budget.some((roleCode) => roleCodes.includes(roleCode))
|
||||
}
|
||||
|
||||
if (viewId === 'settings') {
|
||||
return isPlatformAdminUser(user)
|
||||
}
|
||||
|
||||
if (isManagerUser(user)) {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
finishSession,
|
||||
finishSessionOnUnload
|
||||
finishSessionOnUnload,
|
||||
revokeCurrentAuthSession
|
||||
} from '../services/auth.js'
|
||||
|
||||
const AUTH_SESSION_ID_KEY = 'x-financial-auth-session-id'
|
||||
@@ -88,3 +89,11 @@ export function finalizeAuthSession(reason, options = {}) {
|
||||
console.warn('Failed to finish auth session:', error)
|
||||
})
|
||||
}
|
||||
|
||||
export function finalizeAndRevokeAuthSession(reason) {
|
||||
const sessionId = readStoredSessionId()
|
||||
return revokeCurrentAuthSession({
|
||||
...buildSessionFinishPayload(reason),
|
||||
sessionId
|
||||
})
|
||||
}
|
||||
|
||||
71
web/src/utils/authSessionStorage.js
Normal file
71
web/src/utils/authSessionStorage.js
Normal file
@@ -0,0 +1,71 @@
|
||||
export const AUTH_ACCESS_TOKEN_KEY = 'x-financial-auth-access-token'
|
||||
export const AUTH_EXPIRES_AT_KEY = 'x-financial-auth-expires-at'
|
||||
export const AUTH_SESSION_EXPIRED_EVENT = 'x-financial:auth-expired'
|
||||
|
||||
function canUseSessionStorage() {
|
||||
return typeof window !== 'undefined' && typeof window.sessionStorage !== 'undefined'
|
||||
}
|
||||
|
||||
export function readAuthAccessToken() {
|
||||
if (!canUseSessionStorage()) {
|
||||
return ''
|
||||
}
|
||||
return String(window.sessionStorage.getItem(AUTH_ACCESS_TOKEN_KEY) || '').trim()
|
||||
}
|
||||
|
||||
export function readAuthExpiresAt() {
|
||||
if (!canUseSessionStorage()) {
|
||||
return 0
|
||||
}
|
||||
const raw = String(window.sessionStorage.getItem(AUTH_EXPIRES_AT_KEY) || '').trim()
|
||||
if (!raw) {
|
||||
return 0
|
||||
}
|
||||
const timestamp = Date.parse(raw)
|
||||
return Number.isFinite(timestamp) ? timestamp : 0
|
||||
}
|
||||
|
||||
export function hasValidAuthSession(now = Date.now()) {
|
||||
const accessToken = readAuthAccessToken()
|
||||
const expiresAt = readAuthExpiresAt()
|
||||
return Boolean(accessToken && expiresAt && expiresAt > now)
|
||||
}
|
||||
|
||||
export function persistAuthCredentials(accessToken, expiresAt) {
|
||||
if (!canUseSessionStorage()) {
|
||||
return
|
||||
}
|
||||
const normalizedToken = String(accessToken || '').trim()
|
||||
const normalizedExpiresAt = String(expiresAt || '').trim()
|
||||
if (!normalizedToken || !normalizedExpiresAt || !Number.isFinite(Date.parse(normalizedExpiresAt))) {
|
||||
throw new Error('登录接口未返回有效的认证凭证。')
|
||||
}
|
||||
window.sessionStorage.setItem(AUTH_ACCESS_TOKEN_KEY, normalizedToken)
|
||||
window.sessionStorage.setItem(AUTH_EXPIRES_AT_KEY, normalizedExpiresAt)
|
||||
}
|
||||
|
||||
export function clearAuthCredentials() {
|
||||
if (!canUseSessionStorage()) {
|
||||
return
|
||||
}
|
||||
window.sessionStorage.removeItem(AUTH_ACCESS_TOKEN_KEY)
|
||||
window.sessionStorage.removeItem(AUTH_EXPIRES_AT_KEY)
|
||||
}
|
||||
|
||||
export function buildBearerHeaders() {
|
||||
const accessToken = readAuthAccessToken()
|
||||
if (!accessToken) {
|
||||
return {}
|
||||
}
|
||||
return { Authorization: `Bearer ${accessToken}` }
|
||||
}
|
||||
|
||||
export function notifyAuthSessionExpired() {
|
||||
if (typeof window === 'undefined' || typeof window.dispatchEvent !== 'function') {
|
||||
return
|
||||
}
|
||||
const event = typeof CustomEvent === 'function'
|
||||
? new CustomEvent(AUTH_SESSION_EXPIRED_EVENT)
|
||||
: { type: AUTH_SESSION_EXPIRED_EVENT }
|
||||
window.dispatchEvent(event)
|
||||
}
|
||||
Reference in New Issue
Block a user