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 layoutPath = path.resolve(scriptDir, '../src/layouts/MainLayout.vue')
|
||||||
const apiPath = path.resolve(scriptDir, '../src/api/modules/dataProcess.ts')
|
const apiPath = path.resolve(scriptDir, '../src/api/modules/dataProcess.ts')
|
||||||
const contractTypesPath = path.resolve(scriptDir, '../src/types/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 sourceUploadWorkerPath = path.join(createDir, 'useDataProcessSourceUpload.ts')
|
||||||
const viewSource = await readFile(viewPath, 'utf8')
|
const viewSource = await readFile(viewPath, 'utf8')
|
||||||
const layoutSource = await readFile(layoutPath, 'utf8')
|
const layoutSource = await readFile(layoutPath, 'utf8')
|
||||||
@@ -25,6 +29,18 @@ const [stateSource, generationSource, previewBuildSource, sourceUploadWorkerSour
|
|||||||
readFile(contractTypesPath, 'utf8'),
|
readFile(contractTypesPath, 'utf8'),
|
||||||
])
|
])
|
||||||
const implementationSource = [viewSource, stateSource, generationSource, previewBuildSource, sourceUploadWorkerSource].join('\n')
|
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')
|
assert.ok(existsSync(confirmDialogPath), '缺少公共确认弹窗组件 AppConfirmDialog')
|
||||||
const confirmDialogSource = await readFile(confirmDialogPath, 'utf8')
|
const confirmDialogSource = await readFile(confirmDialogPath, 'utf8')
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import axios, { type AxiosInstance, type AxiosRequestConfig } from 'axios'
|
import axios, { type AxiosInstance, type AxiosRequestConfig } from 'axios'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { touchSessionActivity } from '@/utils/sessionActivity'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 后端统一响应格式
|
* 后端统一响应格式
|
||||||
@@ -29,9 +30,12 @@ service.interceptors.response.use(
|
|||||||
const res = response.data as ApiResult
|
const res = response.data as ApiResult
|
||||||
// 二进制流等非 JSON 响应直接返回
|
// 二进制流等非 JSON 响应直接返回
|
||||||
if (response.config.responseType === 'blob' || response.config.responseType === 'arraybuffer') {
|
if (response.config.responseType === 'blob' || response.config.responseType === 'arraybuffer') {
|
||||||
|
touchSessionActivity()
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
|
// 生成进度轮询也属于用户正在使用系统,避免长任务结束后被误判为会话过期。
|
||||||
|
touchSessionActivity()
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
// 业务错误
|
// 业务错误
|
||||||
|
|||||||
@@ -298,6 +298,7 @@ function requiredPermission(path: string, explicit?: unknown) {
|
|||||||
// 全局守卫:登录校验 + 会话超时
|
// 全局守卫:登录校验 + 会话超时
|
||||||
router.beforeEach((to, _from, next) => {
|
router.beforeEach((to, _from, next) => {
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
|
auth.syncSession()
|
||||||
document.title = to.meta.title ? `${to.meta.title} - 远光软件微调平台` : '远光软件微调平台'
|
document.title = to.meta.title ? `${to.meta.title} - 远光软件微调平台` : '远光软件微调平台'
|
||||||
|
|
||||||
if (to.meta.public) {
|
if (to.meta.public) {
|
||||||
|
|||||||
@@ -3,6 +3,13 @@ import { ref, computed } from 'vue'
|
|||||||
import { login as loginApi } from '@/api/modules/system'
|
import { login as loginApi } from '@/api/modules/system'
|
||||||
import { SESSION_TIMEOUT } from '@/constants'
|
import { SESSION_TIMEOUT } from '@/constants'
|
||||||
import type { PermissionCode, SystemUser } from '@/types'
|
import type { PermissionCode, SystemUser } from '@/types'
|
||||||
|
import {
|
||||||
|
clearSessionActivity,
|
||||||
|
sessionActivityTime,
|
||||||
|
startSessionActivity,
|
||||||
|
syncSessionActivity,
|
||||||
|
touchSessionActivity,
|
||||||
|
} from '@/utils/sessionActivity'
|
||||||
|
|
||||||
const USER_STORAGE_KEY = 'currentUser'
|
const USER_STORAGE_KEY = 'currentUser'
|
||||||
|
|
||||||
@@ -60,7 +67,7 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
if (currentUser.value?.role === 'operator') return '操作员'
|
if (currentUser.value?.role === 'operator') return '操作员'
|
||||||
return '观察员'
|
return '观察员'
|
||||||
})
|
})
|
||||||
const loginTime = ref<number>(parseInt(localStorage.getItem('loginTime') || '0', 10) || 0)
|
const loginTime = sessionActivityTime
|
||||||
|
|
||||||
const isLoggedIn = computed(() => {
|
const isLoggedIn = computed(() => {
|
||||||
if (!loginTime.value) return false
|
if (!loginTime.value) return false
|
||||||
@@ -71,10 +78,9 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
async function login(user: string, password: string) {
|
async function login(user: string, password: string) {
|
||||||
const response = await loginApi(user, password)
|
const response = await loginApi(user, password)
|
||||||
currentUser.value = response.user
|
currentUser.value = response.user
|
||||||
loginTime.value = Date.now()
|
startSessionActivity()
|
||||||
localStorage.setItem('username', response.user.username)
|
localStorage.setItem('username', response.user.username)
|
||||||
localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(response.user))
|
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() {
|
function refresh() {
|
||||||
if (isLoggedIn.value) {
|
if (currentUser.value) touchSessionActivity()
|
||||||
loginTime.value = Date.now()
|
}
|
||||||
localStorage.setItem('loginTime', String(loginTime.value))
|
|
||||||
}
|
/** 在路由判断前吸收其他标签页写入的最后活跃时间。 */
|
||||||
|
function syncSession() {
|
||||||
|
syncSessionActivity()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 退出 */
|
/** 退出 */
|
||||||
function logout() {
|
function logout() {
|
||||||
currentUser.value = null
|
currentUser.value = null
|
||||||
loginTime.value = 0
|
clearSessionActivity()
|
||||||
localStorage.removeItem('username')
|
localStorage.removeItem('username')
|
||||||
localStorage.removeItem(USER_STORAGE_KEY)
|
localStorage.removeItem(USER_STORAGE_KEY)
|
||||||
localStorage.removeItem('loginTime')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -110,6 +117,7 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
hasPermission,
|
hasPermission,
|
||||||
login,
|
login,
|
||||||
refresh,
|
refresh,
|
||||||
|
syncSession,
|
||||||
logout,
|
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