fix: 未登录访问管理后台不再闪现页面,路由守卫改为服务端校验
- 守卫先经 /profile 校验 token 有效后才放行受保护路由,无效 token 直接跳登录并带 redirect 回跳 - DashboardLayout 用户信息就绪前只渲染 spinner,不渲染后台内容 - manager 路由增加 requiresAdmin 校验(role >= 10),非管理员重定向回仪表盘 - 401 拦截器改用 router.push 软跳转,避免整页刷新与双重跳转 - pinia 先于 router 安装;登录页支持 redirect 参数回跳原目标
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import type { AxiosError, InternalAxiosRequestConfig } from 'axios'
|
import type { AxiosError, InternalAxiosRequestConfig } from 'axios'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import router from '@/router'
|
||||||
|
|
||||||
const baseURL = import.meta.env.VITE_API_BASE_URL || '/api'
|
const baseURL = import.meta.env.VITE_API_BASE_URL || '/api'
|
||||||
if (import.meta.env.DEV) { // Vite 的方式判断开发环境
|
if (import.meta.env.DEV) { // Vite 的方式判断开发环境
|
||||||
@@ -49,7 +50,12 @@ service.interceptors.response.use(
|
|||||||
if (error.response && error.response.status === 401) {
|
if (error.response && error.response.status === 401) {
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
authStore.clear();
|
authStore.clear();
|
||||||
window.location.href = '/login';
|
// 守卫校验期间(尚未进入受保护路由)由守卫负责跳登录;
|
||||||
|
// 这里只处理已登录状态下 token 失效的情况,且不再用 location.href 硬刷新
|
||||||
|
const current = router.currentRoute.value;
|
||||||
|
if (current.matched.some(record => record.meta.requiresAuth)) {
|
||||||
|
router.push({ path: '/login', query: { redirect: current.fullPath } });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
<!-- src/layouts/DashboardLayout.vue -->
|
<!-- src/layouts/DashboardLayout.vue -->
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen bg-base-200">
|
<!-- 用户信息就绪前不渲染后台内容,避免未授权内容闪现 -->
|
||||||
|
<div v-if="!authStore.user" class="flex min-h-screen items-center justify-center bg-base-200">
|
||||||
|
<span class="loading loading-spinner loading-lg text-base-content/30"></span>
|
||||||
|
</div>
|
||||||
|
<div v-else class="min-h-screen bg-base-200">
|
||||||
<div class="drawer" :class="{ 'lg:drawer-open': isLargeSidebarOpen }">
|
<div class="drawer" :class="{ 'lg:drawer-open': isLargeSidebarOpen }">
|
||||||
<input id="ot-drawer" type="checkbox" class="drawer-toggle" />
|
<input id="ot-drawer" type="checkbox" class="drawer-toggle" />
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,6 @@ const pinia = createPinia()
|
|||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
app.provide('request', request)
|
app.provide('request', request)
|
||||||
|
app.use(pinia) // 必须先于 router:路由守卫里会用到 auth store
|
||||||
app.use(router)
|
app.use(router)
|
||||||
app.use(pinia)
|
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
|||||||
@@ -1,19 +1,41 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import { routes } from '@/utils/router_menu'
|
import { routes } from '@/utils/router_menu'
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
routes,
|
routes,
|
||||||
})
|
})
|
||||||
|
|
||||||
router.beforeEach((to, from, next) => {
|
// 受保护页面必须先通过服务端校验才渲染:
|
||||||
const isAuthenticated = localStorage.getItem('token')
|
// 本地 token 存在不代表有效(可能已过期/被重置),若只查 localStorage,
|
||||||
|
// 页面会先渲染约 1 秒、等 /profile 返回 401 后才被踢回登录页。
|
||||||
|
router.beforeEach(async (to) => {
|
||||||
const requiresAuth = to.matched.some(record => record.meta.requiresAuth)
|
const requiresAuth = to.matched.some(record => record.meta.requiresAuth)
|
||||||
if (requiresAuth && !isAuthenticated) {
|
if (!requiresAuth) return true
|
||||||
next('/login')
|
|
||||||
} else {
|
const authStore = useAuthStore()
|
||||||
next()
|
|
||||||
|
if (!authStore.token) {
|
||||||
|
return { path: '/login', query: { redirect: to.fullPath } }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 有 token 但还没加载用户信息时,先向服务端确认身份,失败则不得进入
|
||||||
|
if (!authStore.user) {
|
||||||
|
try {
|
||||||
|
await authStore.getProfile()
|
||||||
|
} catch {
|
||||||
|
authStore.clear()
|
||||||
|
return { path: '/login', query: { redirect: to.fullPath } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 管理后台仅对 role >= 10 开放
|
||||||
|
if (to.matched.some(record => record.meta.requiresAdmin) && (authStore.user?.role ?? 0) < 10) {
|
||||||
|
return '/dashboard/overview'
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ declare module 'vue-router' {
|
|||||||
icon?: Component
|
icon?: Component
|
||||||
showInSidebar?: boolean
|
showInSidebar?: boolean
|
||||||
requiresAuth?: boolean
|
requiresAuth?: boolean
|
||||||
|
requiresAdmin?: boolean
|
||||||
open?: boolean
|
open?: boolean
|
||||||
badge?: string
|
badge?: string
|
||||||
}
|
}
|
||||||
@@ -46,7 +47,7 @@ export const routes: RouteRecordRaw[] = [
|
|||||||
{
|
{
|
||||||
path: 'manager',
|
path: 'manager',
|
||||||
name: 'Manager',
|
name: 'Manager',
|
||||||
meta: { title: '管理后台' },
|
meta: { title: '管理后台', requiresAdmin: true },
|
||||||
redirect: '/dashboard/manager/users',
|
redirect: '/dashboard/manager/users',
|
||||||
children: [
|
children: [
|
||||||
{ path: 'users', name: 'User', component: () => import('@/views/dashboard/User.vue'), meta: { title: '用户管理' } },
|
{ path: 'users', name: 'User', component: () => import('@/views/dashboard/User.vue'), meta: { title: '用户管理' } },
|
||||||
|
|||||||
@@ -62,17 +62,21 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { CircleAlert } from '@lucide/vue'
|
import { CircleAlert } from '@lucide/vue'
|
||||||
import { useAuthStore } from '@/stores/auth';
|
import { useAuthStore } from '@/stores/auth';
|
||||||
import { useWebAuthStore } from '@/stores/webauth';
|
import { useWebAuthStore } from '@/stores/webauth';
|
||||||
import { useToast } from '@/composables/toast';
|
import { useToast } from '@/composables/toast';
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
const webauthStore = useWebAuthStore();
|
const webauthStore = useWebAuthStore();
|
||||||
const { setToast } = useToast();
|
const { setToast } = useToast();
|
||||||
|
|
||||||
|
// 被守卫拦下时带上原始目标,登录成功后回跳
|
||||||
|
const redirectPath = typeof route.query.redirect === 'string' ? route.query.redirect : '/dashboard'
|
||||||
|
|
||||||
const error = ref<string | null>(null)
|
const error = ref<string | null>(null)
|
||||||
const loggingIn = ref(false)
|
const loggingIn = ref(false)
|
||||||
const user = reactive({
|
const user = reactive({
|
||||||
@@ -113,7 +117,7 @@ const handleLogin = async () => {
|
|||||||
localStorage.removeItem('rember');
|
localStorage.removeItem('rember');
|
||||||
}
|
}
|
||||||
setToast('Logged in successfully.', 'success');
|
setToast('Logged in successfully.', 'success');
|
||||||
router.push('/dashboard');
|
router.push(redirectPath);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Login error:', err);
|
console.error('Login error:', err);
|
||||||
@@ -130,7 +134,7 @@ const handlePasskeyLogin = async () => {
|
|||||||
const res = await webauthStore.loginPasskey();
|
const res = await webauthStore.loginPasskey();
|
||||||
if (!!res?.code && res.code === 200) {
|
if (!!res?.code && res.code === 200) {
|
||||||
setToast('Logged in successfully.', 'success');
|
setToast('Logged in successfully.', 'success');
|
||||||
router.push('/dashboard');
|
router.push(redirectPath);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Passkey login error:', err);
|
console.error('Passkey login error:', err);
|
||||||
|
|||||||
Reference in New Issue
Block a user