基于 RBAC 模型的完整权限解决方案
src/
├── permissions/ # 权限核心模块
│ ├── guards/ # 路由守卫
│ ├── directives/ # 权限指令
│ ├── utils/ # 权限工具
│ └── types/ # 类型定义
├── stores/ # 状态管理
│ └── auth.ts # 认证状态
├── router/ # 路由配置
│ ├── index.ts # 路由入口
│ ├── routes/ # 路由定义
│ └── permission.ts # 权限路由
├── components/ # 权限组件
│ └── Permission/ # 权限相关组件
└── views/ # 页面组件
└── admin/ # 管理后台页面
// stores/auth.ts
import { defineStore } from 'pinia'
interface User {
id: string
username: string
email: string
roles: string[]
permissions: string[]
}
interface AuthState {
user: User | null
token: string | null
isAuthenticated: boolean
}
export const useAuthStore = defineStore('auth', {
state: (): AuthState => ({
user: null,
token: localStorage.getItem('token'),
isAuthenticated: false
}),
getters: {
hasPermission: (state) => (permission: string) => {
return state.user?.permissions.includes(permission) ?? false
},
hasRole: (state) => (role: string) => {
return state.user?.roles.includes(role) ?? false
},
hasAnyPermission: (state) => (permissions: string[]) => {
return permissions.some(permission =>
state.user?.permissions.includes(permission)
)
}
},
actions: {
async login(credentials: { username: string; password: string }) {
try {
const response = await api.login(credentials)
this.token = response.token
this.user = response.user
this.isAuthenticated = true
localStorage.setItem('token', response.token)
localStorage.setItem('user', JSON.stringify(response.user))
} catch (error) {
throw error
}
},
async logout() {
this.token = null
this.user = null
this.isAuthenticated = false
localStorage.removeItem('token')
localStorage.removeItem('user')
},
async checkAuth() {
if (!this.token) return false
try {
const user = await api.getCurrentUser()
this.user = user
this.isAuthenticated = true
return true
} catch {
this.logout()
return false
}
}
}
})
// utils/request.ts
import axios from 'axios'
import { useAuthStore } from '@/stores/auth'
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000
})
// 请求拦截器
request.interceptors.request.use((config) => {
const authStore = useAuthStore()
if (authStore.token) {
config.headers.Authorization = `Bearer ${authStore.token}`
}
return config
})
// 响应拦截器
request.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
const authStore = useAuthStore()
authStore.logout()
window.location.href = '/login'
}
return Promise.reject(error)
}
)
export default request
// router/routes.ts
import type { RouteRecordRaw } from 'vue-router'
export const constantRoutes: RouteRecordRaw[] = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue'),
meta: {
title: '登录',
requiresAuth: false
}
},
{
path: '/',
component: () => import('@/layouts/DefaultLayout.vue'),
redirect: '/dashboard',
meta: { requiresAuth: true },
children: [
{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue'),
meta: {
title: '仪表板',
icon: 'dashboard',
requiresAuth: true
}
}
]
}
]
export const asyncRoutes: RouteRecordRaw[] = [
{
path: '/system',
component: () => import('@/layouts/DefaultLayout.vue'),
meta: {
title: '系统管理',
icon: 'setting',
requiresAuth: true,
roles: ['admin'] // 需要管理员角色
},
children: [
{
path: 'users',
name: 'UserManagement',
component: () => import('@/views/system/UserManagement.vue'),
meta: {
title: '用户管理',
permissions: ['user:read', 'user:write']
}
},
{
path: 'roles',
name: 'RoleManagement',
component: () => import('@/views/system/RoleManagement.vue'),
meta: {
title: '角色管理',
permissions: ['role:read', 'role:write'],
roles: ['admin']
}
}
]
}
]
// router/permission.ts
import type { Router } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
export function setupPermissionGuard(router: Router) {
router.beforeEach(async (to, from, next) => {
const authStore = useAuthStore()
// 检查是否需要认证
if (to.meta.requiresAuth) {
if (!authStore.isAuthenticated) {
// 尝试重新认证
const isAuthenticated = await authStore.checkAuth()
if (!isAuthenticated) {
next('/login')
return
}
}
// 检查角色权限
if (to.meta.roles) {
const hasRole = (to.meta.roles as string[]).some(role =>
authStore.hasRole(role)
)
if (!hasRole) {
next('/403') // 无权限页面
return
}
}
// 检查具体权限
if (to.meta.permissions) {
const hasPermission = (to.meta.permissions as string[]).some(
permission => authStore.hasPermission(permission)
)
if (!hasPermission) {
next('/403')
return
}
}
}
next()
})
}
// utils/permission.ts
import { asyncRoutes } from '@/router/routes'
import type { RouteRecordRaw } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
export function filterAsyncRoutes(routes: RouteRecordRaw[]): RouteRecordRaw[] {
const authStore = useAuthStore()
return routes.filter(route => {
if (route.meta?.roles) {
const hasRole = (route.meta.roles as string[]).some(role =>
authStore.hasRole(role)
)
if (!hasRole) return false
}
if (route.meta?.permissions) {
const hasPermission = (route.meta.permissions as string[]).some(
permission => authStore.hasPermission(permission)
)
if (!hasPermission) return false
}
if (route.children) {
route.children = filterAsyncRoutes(route.children)
}
return true
})
}
export function addRoutes(router: any) {
const accessedRoutes = filterAsyncRoutes(asyncRoutes)
accessedRoutes.forEach(route => {
router.addRoute(route)
})
}
// permissions/directives/permission.ts
import type { App } from 'vue'
import { useAuthStore } from '@/stores/auth'
export const permissionDirective = {
mounted(el: HTMLElement, binding: any) {
const { value } = binding
const authStore = useAuthStore()
if (value && value instanceof Array && value.length > 0) {
const hasPermission = authStore.hasAnyPermission(value)
if (!hasPermission) {
el.parentNode?.removeChild(el)
}
} else {
throw new Error('需要权限数组,例如 v-permission="[\'user:read\']"')
}
}
}
export function setupPermissionDirective(app: App) {
app.directive('permission', permissionDirective)
}
<template>
<div v-if="hasPermission">
<slot />
</div>
</template>
<script setup lang="ts">
import { useAuthStore } from '@/stores/auth'
interface Props {
permission?: string | string[]
role?: string | string[]
mode?: 'and' | 'or'
}
const props = withDefaults(defineProps<Props>(), {
mode: 'and'
})
const authStore = useAuthStore()
const hasPermission = computed(() => {
if (props.permission) {
const permissions = Array.isArray(props.permission)
? props.permission
: [props.permission]
if (props.mode === 'and') {
return permissions.every(p => authStore.hasPermission(p))
} else {
return permissions.some(p => authStore.hasPermission(p))
}
}
if (props.role) {
const roles = Array.isArray(props.role) ? props.role : [props.role]
if (props.mode === 'and') {
return roles.every(r => authStore.hasRole(r))
} else {
return roles.some(r => authStore.hasRole(r))
}
}
return true
})
</script>
<template>
<div>
<!-- 指令方式 -->
<button v-permission="['user:write']">编辑用户</button>
<!-- 组件方式 -->
<Permission :permission="['user:delete']">
<button>删除用户</button>
</Permission>
<!-- 角色控制 -->
<Permission role="admin">
<button>管理员功能</button>
</Permission>
</div>
</template>
<template>
<div class="role-management">
<div class="header">
<h2>角色权限管理</h2>
<Permission :permission="['role:write']">
<button @click="showCreateDialog">新增角色</button>
</Permission>
</div>
<table>
<thead>
<tr>
<th>角色名称</th>
<th>描述</th>
<th>权限数量</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="role in roles" :key="role.id">
<td>{{ role.name }}</td>
<td>{{ role.description }}</td>
<td>{{ role.permissions.length }}</td>
<td>
<Permission :permission="['role:write']">
<button @click="editRole(role)">编辑</button>
</Permission>
<Permission :permission="['role:delete']">
<button @click="deleteRole(role)">删除</button>
</Permission>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useAuthStore } from '@/stores/auth'
interface Role {
id: string
name: string
description: string
permissions: string[]
}
const authStore = useAuthStore()
const roles = ref<Role[]>([])
onMounted(async () => {
await loadRoles()
})
async function loadRoles() {
// 调用API获取角色列表
roles.value = await api.getRoles()
}
</script>
前端权限控制只是用户体验优化,真正的安全必须在后端实现!
所有接口都必须进行权限验证,防止直接API调用绕过前端控制。建议: