feat(rbac): 路由级权限过滤 + 403 页
- 路由守卫按可访问菜单 path 校验,无权跳转 /403 - 新增 403 页 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
2f7f83b8b1
commit
84edbc67c0
25
src/pages/403.vue
Normal file
25
src/pages/403.vue
Normal file
@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<div class="error-page">
|
||||
<el-result icon="warning" title="403" sub-title="抱歉,你没有权限访问该页面">
|
||||
<template #extra>
|
||||
<el-button type="primary" @click="goHome">回到首页</el-button>
|
||||
</template>
|
||||
</el-result>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
function goHome(): void {
|
||||
router.push('/')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.error-page {
|
||||
@apply flex h-full items-center justify-center;
|
||||
}
|
||||
</style>
|
||||
@ -9,6 +9,7 @@ import { routes } from 'vue-router/auto-routes'
|
||||
import { setupLayouts } from 'virtual:generated-layouts'
|
||||
import i18n, { loadLanguage } from '@/i18n'
|
||||
import { getAccessToken } from '@/utils/token'
|
||||
import { useMenuStore } from '@/stores/modules/menu'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
@ -39,15 +40,49 @@ async function loadRouteLocales(to: RouteLocationNormalized) {
|
||||
}
|
||||
}
|
||||
|
||||
// 无需菜单权限校验的路径(首页 + 错误页)
|
||||
const FREE_PATHS = new Set(['/', '/403'])
|
||||
|
||||
/** 收集菜单树中所有可访问 path。 */
|
||||
function collectMenuPaths(nodes: { path?: string | null; children?: unknown[] }[]): string[] {
|
||||
const paths: string[] = []
|
||||
for (const n of nodes) {
|
||||
if (n.path) paths.push(n.path)
|
||||
if (n.children?.length) paths.push(...collectMenuPaths(n.children as { path?: string | null; children?: unknown[] }[]))
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
// 路由前置守卫
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
await loadRouteLocales(to)
|
||||
// 未标记 requiresAuth: false 的路由默认需要登录
|
||||
const requiresAuth = to.meta.requiresAuth !== false
|
||||
if (requiresAuth && !getAccessToken()) {
|
||||
next('/login')
|
||||
} else {
|
||||
if (!requiresAuth) {
|
||||
next()
|
||||
return
|
||||
}
|
||||
if (!getAccessToken()) {
|
||||
next('/login')
|
||||
return
|
||||
}
|
||||
// 路由级权限:自由路径放行,其余须在可访问菜单中
|
||||
if (FREE_PATHS.has(to.path)) {
|
||||
next()
|
||||
return
|
||||
}
|
||||
const menuStore = useMenuStore()
|
||||
try {
|
||||
await menuStore.fetch()
|
||||
} catch {
|
||||
next('/login')
|
||||
return
|
||||
}
|
||||
const accessible = new Set(collectMenuPaths(menuStore.menus))
|
||||
if (accessible.has(to.path)) {
|
||||
next()
|
||||
} else {
|
||||
next('/403')
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
13
typed-router.d.ts
vendored
13
typed-router.d.ts
vendored
@ -37,6 +37,13 @@ declare module 'vue-router/auto-routes' {
|
||||
{ all: ParamValue<false> },
|
||||
| never
|
||||
>,
|
||||
'/403': RouteRecordInfo<
|
||||
'/403',
|
||||
'/403',
|
||||
Record<never, never>,
|
||||
Record<never, never>,
|
||||
| never
|
||||
>,
|
||||
'/demo': RouteRecordInfo<
|
||||
'/demo',
|
||||
'/demo',
|
||||
@ -174,6 +181,12 @@ declare module 'vue-router/auto-routes' {
|
||||
views:
|
||||
| never
|
||||
}
|
||||
'src/pages/403.vue': {
|
||||
routes:
|
||||
| '/403'
|
||||
views:
|
||||
| never
|
||||
}
|
||||
'src/pages/demo.vue': {
|
||||
routes:
|
||||
| '/demo'
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user