添加动态权限控制

This commit is contained in:
xie7654
2025-07-02 21:54:13 +08:00
parent eace8a524d
commit 78b9f9e832
12 changed files with 186 additions and 24 deletions

View File

@@ -47,10 +47,12 @@ def gen_menu(app_name, model_name, parent_menu_name, creator='admin'):
# 按钮权限 # 按钮权限
buttons = [ buttons = [
{"name": "Query", "title": "common.query", "auth_code": f"{app_name}:{model_lower}:query"},
{"name": "Create", "title": "common.create", "auth_code": f"{app_name}:{model_lower}:create"}, {"name": "Create", "title": "common.create", "auth_code": f"{app_name}:{model_lower}:create"},
{"name": "Edit", "title": "common.edit", "auth_code": f"{app_name}:{model_lower}:edit"}, {"name": "Edit", "title": "common.edit", "auth_code": f"{app_name}:{model_lower}:edit"},
{"name": "Delete", "title": "common.delete", "auth_code": f"{app_name}:{model_lower}:delete"}, {"name": "Delete", "title": "common.delete", "auth_code": f"{app_name}:{model_lower}:delete"},
{"name": "Query", "title": "common.query", "auth_code": f"{app_name}:{model_lower}:query"},
{"name": "Query", "title": "common.query", "auth_code": f"{app_name}:{model_lower}:import"},
{"name": "Query", "title": "common.query", "auth_code": f"{app_name}:{model_lower}:export"},
] ]
for idx, btn in enumerate(buttons): for idx, btn in enumerate(buttons):
btn_meta = MenuMeta.objects.create( btn_meta = MenuMeta.objects.create(

View File

@@ -2,6 +2,7 @@ from system.models import LoginLog
from utils.serializers import CustomModelSerializer from utils.serializers import CustomModelSerializer
from utils.custom_model_viewSet import CustomModelViewSet from utils.custom_model_viewSet import CustomModelViewSet
from rest_framework import serializers from rest_framework import serializers
from utils.permissions import HasButtonPermission
class LoginLogSerializer(CustomModelSerializer): class LoginLogSerializer(CustomModelSerializer):
""" """
@@ -28,3 +29,4 @@ class LoginLogViewSet(CustomModelViewSet):
search_fields = ['name'] # 根据实际字段调整 search_fields = ['name'] # 根据实际字段调整
ordering_fields = ['create_time', 'id'] ordering_fields = ['create_time', 'id']
ordering = ['-create_time'] ordering = ['-create_time']
permission_classes = [HasButtonPermission]

View File

@@ -8,7 +8,6 @@ from django.contrib.auth.hashers import make_password
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from system.models import User, Menu, LoginLog from system.models import User, Menu, LoginLog
from system.views.menu import MenuSerializer
from utils.serializers import CustomModelSerializer from utils.serializers import CustomModelSerializer
from utils.custom_model_viewSet import CustomModelViewSet from utils.custom_model_viewSet import CustomModelViewSet
@@ -79,13 +78,14 @@ class UserInfo(APIView):
if user.is_superuser: if user.is_superuser:
roles = ['admin'] roles = ['admin']
# menus = Menu.objects.filter(pid__isnull=True).order_by('sort') # menus = Menu.objects.filter(pid__isnull=True).order_by('sort')
# permissions = Menu.objects.filter(type='button').order_by('sort').values_list('auth_code', flat=True) permissions = Menu.objects.filter(type='button').order_by('auth_code').values_list('auth_code', flat=True)
else: else:
roles = user.get_role_name roles = user.get_role_name
# menus = Menu.objects.filter(pid__isnull=True, role__users=user).order_by('sort').distinct() # menus = Menu.objects.filter(pid__isnull=True, role__users=user).order_by('sort').distinct()
# permissions = Menu.objects.filter(type='button', role__users=user).order_by('sort').distinct().values_list('auth_code', flat=True) permissions = Menu.objects.filter(type='button', role__users=user).order_by('auth_code').distinct().values_list('auth_code', flat=True)
# menus_data = MenuSerializer(menus, many=True).data # menus_data = MenuSerializer(menus, many=True).data
user_data['roles'] = roles user_data['roles'] = roles
user_data['permissions'] = permissions
return Response({ return Response({
"code": 0, "code": 0,
"data": user_data, "data": user_data,

View File

@@ -20,6 +20,38 @@ class CustomModelViewSet(viewsets.ModelViewSet):
# 是否支持软删除 # 是否支持软删除
enable_soft_delete = False enable_soft_delete = False
def get_required_permission(self):
# 约定system:menu:create
app_label = self.queryset.model._meta.app_label
model_name = self.queryset.model._meta.model_name
action = self.action # 'create', 'update', 'destroy', 'list', 'retrieve'
# 只对增删改查等操作做权限控制
action_map = {
'create': 'create',
'update': 'edit',
'partial_update': 'edit',
'destroy': 'delete',
'list': 'query',
'retrieve': 'query',
}
if action in action_map:
perm_action = action_map[action]
else:
perm_action = action # 如 sync、import、export
return f"{app_label}:{model_name}:{perm_action}"
def get_permissions(self):
permissions = super().get_permissions()
required_code = self.get_required_permission()
if required_code:
from utils.permissions import HasButtonPermission
perm = HasButtonPermission()
# 动态设置 required_permission
perm.required_permission = required_code
permissions.append(perm)
return permissions
def get_serializer_class(self): def get_serializer_class(self):
"""根据当前动作获取序列化器类""" """根据当前动作获取序列化器类"""
return self.action_serializers.get( return self.action_serializers.get(
@@ -27,13 +59,6 @@ class CustomModelViewSet(viewsets.ModelViewSet):
super().get_serializer_class() super().get_serializer_class()
) )
def get_permissions(self):
"""根据当前动作获取权限类"""
permissions = self.action_permissions.get(
self.action,
self.permission_classes
)
return [permission() for permission in permissions]
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
"""重写列表视图,支持软删除过滤""" """重写列表视图,支持软删除过滤"""

View File

@@ -1,8 +1,48 @@
from rest_framework import permissions from rest_framework import permissions
from rest_framework.permissions import BasePermission
from system.models import Menu
class IsSuperUserOrReadOnly(permissions.BasePermission): class IsSuperUserOrReadOnly(BasePermission):
"""超级用户可读写,普通用户只读""" """超级用户可读写,普通用户只读"""
def has_permission(self, request, view): def has_permission(self, request, view):
if request.method in permissions.SAFE_METHODS: if request.method in permissions.SAFE_METHODS:
return True return True
return request.user and request.user.is_superuser return request.user and request.user.is_superuser
class HasButtonPermission(BasePermission):
"""
通用按钮权限校验
用法:在视图中设置 required_permission = 'xxx:xxx:xxx'
"""
def has_permission(self, request, view):
required_code = getattr(view, 'required_permission', None)
if not required_code:
# 可自动推断权限编码逻辑
app_label = view.queryset.model._meta.app_label
model_name = view.queryset.model._meta.model_name
action = getattr(view, 'action', None)
action_map = {
'create': 'create',
'update': 'edit',
'partial_update': 'edit',
'destroy': 'delete',
'list': 'query',
'retrieve': 'query',
}
if action in action_map:
required_code = f"{app_label}:{model_name}:{action_map[action]}"
if not required_code:
return True # 不需要按钮权限
user = request.user
if not user.is_authenticated or user.is_anonymous:
return False
if user.is_superuser:
return True
role_ids = user.role.values_list('id', flat=True)
return Menu.objects.filter(
type='button',
role__id__in=role_ids,
auth_code=required_code
).exists()

View File

@@ -15,6 +15,7 @@ import { initComponentAdapter } from './adapter/component';
import { initSetupVbenForm } from './adapter/form'; import { initSetupVbenForm } from './adapter/form';
import App from './app.vue'; import App from './app.vue';
import { router } from './router'; import { router } from './router';
import { registerPermissionDirective } from './utils/permission';
async function bootstrap(namespace: string) { async function bootstrap(namespace: string) {
// 初始化组件适配器 // 初始化组件适配器
@@ -48,6 +49,8 @@ async function bootstrap(namespace: string) {
// 安装权限指令 // 安装权限指令
registerAccessDirective(app); registerAccessDirective(app);
// 注册自定义v-permission指令
registerPermissionDirective(app);
// 初始化 tippy // 初始化 tippy
const { initTippy } = await import('@vben/common-ui/es/tippy'); const { initTippy } = await import('@vben/common-ui/es/tippy');

View File

@@ -13,9 +13,12 @@ import { defineStore } from 'pinia';
import { getAccessCodesApi, getUserInfoApi, loginApi, logoutApi } from '#/api'; import { getAccessCodesApi, getUserInfoApi, loginApi, logoutApi } from '#/api';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { usePermissionStore } from './permission';
export const useAuthStore = defineStore('auth', () => { export const useAuthStore = defineStore('auth', () => {
const accessStore = useAccessStore(); const accessStore = useAccessStore();
const userStore = useUserStore(); const userStore = useUserStore();
const permissionStore = usePermissionStore();
const router = useRouter(); const router = useRouter();
const loginLoading = ref(false); const loginLoading = ref(false);
@@ -62,7 +65,7 @@ export const useAuthStore = defineStore('auth', () => {
if (userInfo?.realName) { if (userInfo?.realName) {
notification.success({ notification.success({
description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.realName}`, description: `${$t('authentication.loginSuccessDesc')}:${userInfo?.username}`,
duration: 3, duration: 3,
message: $t('authentication.loginSuccess'), message: $t('authentication.loginSuccess'),
}); });
@@ -101,6 +104,11 @@ export const useAuthStore = defineStore('auth', () => {
let userInfo: null | UserInfo = null; let userInfo: null | UserInfo = null;
userInfo = await getUserInfoApi(); userInfo = await getUserInfoApi();
userStore.setUserInfo(userInfo); userStore.setUserInfo(userInfo);
// 设置权限
if (userInfo && Array.isArray(userInfo.permissions)) {
permissionStore.setPermissions(userInfo.permissions);
}
return userInfo; return userInfo;
} }

View File

@@ -0,0 +1,30 @@
import { ref } from 'vue';
import { defineStore } from 'pinia';
export const usePermissionStore = defineStore('permission', () => {
// 权限码列表
const permissions = ref<string[]>([]);
// 设置权限码
function setPermissions(perms: string[]) {
permissions.value = perms || [];
}
// 判断是否有某个权限
function hasPermission(code: string): boolean {
return permissions.value.includes(code);
}
// 重置
function $reset() {
permissions.value = [];
}
return {
permissions,
setPermissions,
hasPermission,
$reset,
};
})

View File

@@ -0,0 +1,48 @@
import type { App, DirectiveBinding } from 'vue';
import { usePermissionStore } from '#/store/permission';
/**
* 权限按钮option生成工具
* @param code 权限码
* @param option 按钮option或字符串
* @returns 有权限返回option无权限返回false
*/
export function op(code: string, option: any) {
const permissionStore = usePermissionStore();
return permissionStore.hasPermission(code) ? option : false;
}
/**
* 全局权限判断函数适用于模板v-if
*/
export function hasPermission(code: string): boolean {
const permissionStore = usePermissionStore();
return permissionStore.hasPermission(code);
}
/**
* v-permission 自定义指令
* 用法v-permission="'system:user:create'"
* 或 v-permission="['system:user:create', 'system:user:update']"
*/
const permissionDirective = {
mounted(el: HTMLElement, binding: DirectiveBinding<string | string[]>) {
const codes = Array.isArray(binding.value)
? binding.value
: [binding.value];
const permissionStore = usePermissionStore();
const has = codes.some((code) => permissionStore.hasPermission(code));
if (!has) {
el.parentNode && el.remove();
}
},
};
/**
* 注册全局权限指令
* @param app Vue App 实例
*/
export function registerPermissionDirective(app: App) {
app.directive('permission', permissionDirective);
}

View File

@@ -8,6 +8,8 @@ import { z } from '#/adapter/form';
import { getDeptList } from '#/api/system/dept'; import { getDeptList } from '#/api/system/dept';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { format_datetime } from '#/utils/date'; import { format_datetime } from '#/utils/date';
import { op } from '#/utils/permission';
/** /**
* 获取编辑表单的字段配置。如果没有使用多语言可以直接export一个数组常量 * 获取编辑表单的字段配置。如果没有使用多语言可以直接export一个数组常量
*/ */
@@ -137,18 +139,15 @@ export function useColumns(
}, },
name: 'CellOperation', name: 'CellOperation',
options: [ options: [
{ op('system:dept:create', { code: 'append', text: '新增下级' }),
code: 'append', op('system:dept:edit', 'edit'),
text: '新增下级', op('system:dept:delete', {
}, code: 'delete',
'edit', // 默认的编辑按钮
{
code: 'delete', // 默认的删除按钮
disabled: (row: SystemDeptApi.SystemDept) => { disabled: (row: SystemDeptApi.SystemDept) => {
return !!(row.children && row.children.length > 0); return !!(row.children && row.children.length > 0);
}, },
}, }),
], ].filter(Boolean),
}, },
field: 'operation', field: 'operation',
fixed: 'right', fixed: 'right',

View File

@@ -133,7 +133,11 @@ function refreshGrid() {
<FormModal @success="refreshGrid" /> <FormModal @success="refreshGrid" />
<Grid table-title="部门列表"> <Grid table-title="部门列表">
<template #toolbar-tools> <template #toolbar-tools>
<Button type="primary" @click="onCreate"> <Button
type="primary"
@click="onCreate"
v-permission="'system:dept:create'"
>
<Plus class="size-5" /> <Plus class="size-5" />
{{ $t('ui.actionTitle.create', [$t('system.dept.name')]) }} {{ $t('ui.actionTitle.create', [$t('system.dept.name')]) }}
</Button> </Button>

View File

@@ -15,7 +15,8 @@ interface UserInfo extends BasicUserInfo {
* accessToken * accessToken
*/ */
token: string; token: string;
roles: []; roles?: string[];
permissions?: string[];
} }
export type { UserInfo }; export type { UserInfo };