fix(auth): 长任务活跃期间续期会话
This commit is contained in:
@@ -12,6 +12,10 @@ const confirmDialogPath = path.resolve(scriptDir, '../src/components/AppConfirmD
|
||||
const layoutPath = path.resolve(scriptDir, '../src/layouts/MainLayout.vue')
|
||||
const apiPath = path.resolve(scriptDir, '../src/api/modules/dataProcess.ts')
|
||||
const contractTypesPath = path.resolve(scriptDir, '../src/types/dataProcess.ts')
|
||||
const requestPath = path.resolve(scriptDir, '../src/api/request.ts')
|
||||
const authStorePath = path.resolve(scriptDir, '../src/stores/auth.ts')
|
||||
const routerPath = path.resolve(scriptDir, '../src/router/index.ts')
|
||||
const sessionActivityPath = path.resolve(scriptDir, '../src/utils/sessionActivity.ts')
|
||||
const sourceUploadWorkerPath = path.join(createDir, 'useDataProcessSourceUpload.ts')
|
||||
const viewSource = await readFile(viewPath, 'utf8')
|
||||
const layoutSource = await readFile(layoutPath, 'utf8')
|
||||
@@ -25,6 +29,18 @@ const [stateSource, generationSource, previewBuildSource, sourceUploadWorkerSour
|
||||
readFile(contractTypesPath, 'utf8'),
|
||||
])
|
||||
const implementationSource = [viewSource, stateSource, generationSource, previewBuildSource, sourceUploadWorkerSource].join('\n')
|
||||
const [requestSource, authStoreSource, routerSource, sessionActivitySource] = await Promise.all([
|
||||
readFile(requestPath, 'utf8'),
|
||||
readFile(authStorePath, 'utf8'),
|
||||
readFile(routerPath, 'utf8'),
|
||||
readFile(sessionActivityPath, 'utf8'),
|
||||
])
|
||||
|
||||
assert.match(requestSource, /if \(res\.code === 0\) \{[\s\S]*?touchSessionActivity\(\)/, '成功 API 请求没有刷新会话活跃时间')
|
||||
assert.match(authStoreSource, /const loginTime = sessionActivityTime/, '认证状态没有共享请求层的会话活跃时间')
|
||||
assert.match(authStoreSource, /if \(currentUser\.value\) touchSessionActivity\(\)/, '会话续期仍可能在长任务结束后失效')
|
||||
assert.match(routerSource, /const auth = useAuthStore\(\)[\s\S]*?auth\.syncSession\(\)[\s\S]*?if \(!auth\.isLoggedIn\)/, '路由守卫没有在登录判断前同步 API 活跃时间')
|
||||
assert.match(sessionActivitySource, /export function touchSessionActivity\(\)/, '缺少统一会话活跃续期函数')
|
||||
|
||||
assert.ok(existsSync(confirmDialogPath), '缺少公共确认弹窗组件 AppConfirmDialog')
|
||||
const confirmDialogSource = await readFile(confirmDialogPath, 'utf8')
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import axios, { type AxiosInstance, type AxiosRequestConfig } from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { touchSessionActivity } from '@/utils/sessionActivity'
|
||||
|
||||
/**
|
||||
* 后端统一响应格式
|
||||
@@ -29,9 +30,12 @@ service.interceptors.response.use(
|
||||
const res = response.data as ApiResult
|
||||
// 二进制流等非 JSON 响应直接返回
|
||||
if (response.config.responseType === 'blob' || response.config.responseType === 'arraybuffer') {
|
||||
touchSessionActivity()
|
||||
return response
|
||||
}
|
||||
if (res.code === 0) {
|
||||
// 生成进度轮询也属于用户正在使用系统,避免长任务结束后被误判为会话过期。
|
||||
touchSessionActivity()
|
||||
return res.data
|
||||
}
|
||||
// 业务错误
|
||||
|
||||
@@ -298,6 +298,7 @@ function requiredPermission(path: string, explicit?: unknown) {
|
||||
// 全局守卫:登录校验 + 会话超时
|
||||
router.beforeEach((to, _from, next) => {
|
||||
const auth = useAuthStore()
|
||||
auth.syncSession()
|
||||
document.title = to.meta.title ? `${to.meta.title} - 远光软件微调平台` : '远光软件微调平台'
|
||||
|
||||
if (to.meta.public) {
|
||||
|
||||
@@ -3,6 +3,13 @@ import { ref, computed } from 'vue'
|
||||
import { login as loginApi } from '@/api/modules/system'
|
||||
import { SESSION_TIMEOUT } from '@/constants'
|
||||
import type { PermissionCode, SystemUser } from '@/types'
|
||||
import {
|
||||
clearSessionActivity,
|
||||
sessionActivityTime,
|
||||
startSessionActivity,
|
||||
syncSessionActivity,
|
||||
touchSessionActivity,
|
||||
} from '@/utils/sessionActivity'
|
||||
|
||||
const USER_STORAGE_KEY = 'currentUser'
|
||||
|
||||
@@ -60,7 +67,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
if (currentUser.value?.role === 'operator') return '操作员'
|
||||
return '观察员'
|
||||
})
|
||||
const loginTime = ref<number>(parseInt(localStorage.getItem('loginTime') || '0', 10) || 0)
|
||||
const loginTime = sessionActivityTime
|
||||
|
||||
const isLoggedIn = computed(() => {
|
||||
if (!loginTime.value) return false
|
||||
@@ -71,10 +78,9 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
async function login(user: string, password: string) {
|
||||
const response = await loginApi(user, password)
|
||||
currentUser.value = response.user
|
||||
loginTime.value = Date.now()
|
||||
startSessionActivity()
|
||||
localStorage.setItem('username', response.user.username)
|
||||
localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(response.user))
|
||||
localStorage.setItem('loginTime', String(loginTime.value))
|
||||
}
|
||||
|
||||
/** 检查当前账号是否拥有指定模块权限。 */
|
||||
@@ -85,19 +91,20 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
|
||||
/** 续期会话(活跃时刷新) */
|
||||
function refresh() {
|
||||
if (isLoggedIn.value) {
|
||||
loginTime.value = Date.now()
|
||||
localStorage.setItem('loginTime', String(loginTime.value))
|
||||
}
|
||||
if (currentUser.value) touchSessionActivity()
|
||||
}
|
||||
|
||||
/** 在路由判断前吸收其他标签页写入的最后活跃时间。 */
|
||||
function syncSession() {
|
||||
syncSessionActivity()
|
||||
}
|
||||
|
||||
/** 退出 */
|
||||
function logout() {
|
||||
currentUser.value = null
|
||||
loginTime.value = 0
|
||||
clearSessionActivity()
|
||||
localStorage.removeItem('username')
|
||||
localStorage.removeItem(USER_STORAGE_KEY)
|
||||
localStorage.removeItem('loginTime')
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -110,6 +117,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
hasPermission,
|
||||
login,
|
||||
refresh,
|
||||
syncSession,
|
||||
logout,
|
||||
}
|
||||
})
|
||||
|
||||
32
frontend/src/utils/sessionActivity.ts
Normal file
32
frontend/src/utils/sessionActivity.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
const LOGIN_TIME_STORAGE_KEY = 'loginTime'
|
||||
|
||||
function storedActivityTime() {
|
||||
return Number.parseInt(localStorage.getItem(LOGIN_TIME_STORAGE_KEY) || '0', 10) || 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话按“最后活跃时间”计算,而不是从首次登录起固定倒计时。
|
||||
* 该 ref 被认证 store 与请求层共享,确保 API 活动可以立即影响路由守卫。
|
||||
*/
|
||||
export const sessionActivityTime = ref(storedActivityTime())
|
||||
|
||||
export function startSessionActivity() {
|
||||
sessionActivityTime.value = Date.now()
|
||||
localStorage.setItem(LOGIN_TIME_STORAGE_KEY, String(sessionActivityTime.value))
|
||||
}
|
||||
|
||||
export function touchSessionActivity() {
|
||||
if (!sessionActivityTime.value) return
|
||||
startSessionActivity()
|
||||
}
|
||||
|
||||
export function syncSessionActivity() {
|
||||
sessionActivityTime.value = storedActivityTime()
|
||||
}
|
||||
|
||||
export function clearSessionActivity() {
|
||||
sessionActivityTime.value = 0
|
||||
localStorage.removeItem(LOGIN_TIME_STORAGE_KEY)
|
||||
}
|
||||
Reference in New Issue
Block a user