添加动态权限控制
This commit is contained in:
@@ -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(
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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):
|
||||||
"""重写列表视图,支持软删除过滤"""
|
"""重写列表视图,支持软删除过滤"""
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
30
web/apps/web-antd/src/store/permission.ts
Normal file
30
web/apps/web-antd/src/store/permission.ts
Normal 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,
|
||||||
|
};
|
||||||
|
})
|
||||||
48
web/apps/web-antd/src/utils/permission.ts
Normal file
48
web/apps/web-antd/src/utils/permission.ts
Normal 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);
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
Reference in New Issue
Block a user