feat(权限): 实现基于角色的路由权限控制
- 新增路由元信息类型定义 (requiresAuth, roles, title) - 实现路由守卫中的角色权限校验逻辑 - 新增 403 禁止访问页面 - 提取权限校验函数 checkRoutePermission,提高可测试性 - 修复 JSON.parse 异常处理,增强健壮性 - 优化页面标题动态设置 测试优化: - 重构 global-setup.ts,支持 JAR 文件启动后端服务 - 优化测试用例等待逻辑,减少硬编码延迟 - 简化 playwright 配置,移除多浏览器支持 - 新增路由权限守卫单元测试 关联需求:权限系统完善
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
const mockLocalStorage = {
|
||||
store: {} as Record<string, string>,
|
||||
getItem(key: string) {
|
||||
return this.store[key] || null
|
||||
},
|
||||
setItem(key: string, value: string) {
|
||||
this.store[key] = value
|
||||
},
|
||||
removeItem(key: string) {
|
||||
delete this.store[key]
|
||||
},
|
||||
clear() {
|
||||
this.store = {}
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
value: mockLocalStorage
|
||||
})
|
||||
|
||||
const createTestRouter = (routes: RouteRecordRaw[]) => {
|
||||
return createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
}
|
||||
|
||||
describe('路由守卫权限检查', () => {
|
||||
beforeEach(() => {
|
||||
mockLocalStorage.clear()
|
||||
})
|
||||
|
||||
describe('基础认证检查', () => {
|
||||
it('未登录用户访问受保护路由应重定向到登录页', async () => {
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: { template: '<div>Login</div>' }
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: { template: '<div>Layout</div>' },
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
component: { template: '<div>Dashboard</div>' }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const router = createTestRouter(routes)
|
||||
|
||||
router.beforeEach((to, _from, next) => {
|
||||
const token = localStorage.getItem('token')
|
||||
|
||||
if (to.meta.requiresAuth && !token) {
|
||||
next('/login')
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
await router.push('/dashboard')
|
||||
expect(router.currentRoute.value.path).toBe('/login')
|
||||
})
|
||||
|
||||
it('已登录用户访问受保护路由应允许通过', async () => {
|
||||
mockLocalStorage.setItem('token', 'valid-token')
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: { template: '<div>Login</div>' }
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: { template: '<div>Layout</div>' },
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
component: { template: '<div>Dashboard</div>' }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const router = createTestRouter(routes)
|
||||
|
||||
router.beforeEach((to, _from, next) => {
|
||||
const token = localStorage.getItem('token')
|
||||
|
||||
if (to.meta.requiresAuth && !token) {
|
||||
next('/login')
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
await router.push('/dashboard')
|
||||
expect(router.currentRoute.value.path).toBe('/dashboard')
|
||||
})
|
||||
})
|
||||
|
||||
describe('角色权限检查', () => {
|
||||
it('普通用户访问管理员路由应重定向到403页面', async () => {
|
||||
mockLocalStorage.setItem('token', 'valid-token')
|
||||
mockLocalStorage.setItem('roles', JSON.stringify(['user']))
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: { template: '<div>Login</div>' }
|
||||
},
|
||||
{
|
||||
path: '/403',
|
||||
name: 'Forbidden',
|
||||
component: { template: '<div>403 Forbidden</div>' }
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: { template: '<div>Layout</div>' },
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
component: { template: '<div>Dashboard</div>' }
|
||||
},
|
||||
{
|
||||
path: 'users',
|
||||
name: 'UserManagement',
|
||||
component: { template: '<div>UserManagement</div>' },
|
||||
meta: { roles: ['admin'] }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const router = createTestRouter(routes)
|
||||
|
||||
router.beforeEach((to, _from, next) => {
|
||||
const token = localStorage.getItem('token')
|
||||
const rolesStr = localStorage.getItem('roles')
|
||||
const userRoles = rolesStr ? JSON.parse(rolesStr) : []
|
||||
|
||||
if (to.meta.requiresAuth && !token) {
|
||||
next('/login')
|
||||
return
|
||||
}
|
||||
|
||||
if (to.meta.roles && Array.isArray(to.meta.roles)) {
|
||||
const hasRole = to.meta.roles.some((role: string) => userRoles.includes(role))
|
||||
if (!hasRole) {
|
||||
next('/403')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
await router.push('/users')
|
||||
expect(router.currentRoute.value.path).toBe('/403')
|
||||
})
|
||||
|
||||
it('管理员用户访问管理员路由应允许通过', async () => {
|
||||
mockLocalStorage.setItem('token', 'valid-token')
|
||||
mockLocalStorage.setItem('roles', JSON.stringify(['admin']))
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: { template: '<div>Login</div>' }
|
||||
},
|
||||
{
|
||||
path: '/403',
|
||||
name: 'Forbidden',
|
||||
component: { template: '<div>403 Forbidden</div>' }
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: { template: '<div>Layout</div>' },
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
component: { template: '<div>Dashboard</div>' }
|
||||
},
|
||||
{
|
||||
path: 'users',
|
||||
name: 'UserManagement',
|
||||
component: { template: '<div>UserManagement</div>' },
|
||||
meta: { roles: ['admin'] }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const router = createTestRouter(routes)
|
||||
|
||||
router.beforeEach((to, _from, next) => {
|
||||
const token = localStorage.getItem('token')
|
||||
const rolesStr = localStorage.getItem('roles')
|
||||
const userRoles = rolesStr ? JSON.parse(rolesStr) : []
|
||||
|
||||
if (to.meta.requiresAuth && !token) {
|
||||
next('/login')
|
||||
return
|
||||
}
|
||||
|
||||
if (to.meta.roles && Array.isArray(to.meta.roles)) {
|
||||
const hasRole = to.meta.roles.some((role: string) => userRoles.includes(role))
|
||||
if (!hasRole) {
|
||||
next('/403')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
await router.push('/users')
|
||||
expect(router.currentRoute.value.path).toBe('/users')
|
||||
})
|
||||
|
||||
it('无角色要求的路由所有登录用户都可访问', async () => {
|
||||
mockLocalStorage.setItem('token', 'valid-token')
|
||||
mockLocalStorage.setItem('roles', JSON.stringify(['user']))
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: { template: '<div>Login</div>' }
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: { template: '<div>Layout</div>' },
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
component: { template: '<div>Dashboard</div>' }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const router = createTestRouter(routes)
|
||||
|
||||
router.beforeEach((to, _from, next) => {
|
||||
const token = localStorage.getItem('token')
|
||||
const rolesStr = localStorage.getItem('roles')
|
||||
const userRoles = rolesStr ? JSON.parse(rolesStr) : []
|
||||
|
||||
if (to.meta.requiresAuth && !token) {
|
||||
next('/login')
|
||||
return
|
||||
}
|
||||
|
||||
if (to.meta.roles && Array.isArray(to.meta.roles)) {
|
||||
const hasRole = to.meta.roles.some((role: string) => userRoles.includes(role))
|
||||
if (!hasRole) {
|
||||
next('/403')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
await router.push('/dashboard')
|
||||
expect(router.currentRoute.value.path).toBe('/dashboard')
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user