前言

利用Django+Rest Framework 可以很轻松的专注于API开发,而不必重复造一些轮子。针对API的权限设计是整个系统核心且无比重要的步骤,DRF框架内置的一些权限如下。

权限名称 描述
AllowAny 允许全部请求访问
IsAuthenticated 允许认证账户访问
IsAdminUser 允许管理员访问
IsAuthenticatedOrReadOnly 允许认证账户访问全部的基础上,允许匿名用户执行GET, HEAD 或 OPTIONS
DjangoModelPermissions 基于DjangoModel的权限访问
DjangoModelPermissionsOrAnonReadOnly 基于DjangoModel的权限访问的基础上,允许匿名用户执行GET, HEAD 或 OPTIONS
DjangoObjectPermissions Django的标准对象权限框架相关联,对象级权限

可以看出,除了一些基本的权限外,DRF精细化权限设计基本都是基于model来做的,这样不好的地方是每次权限的变化都需要发生代码层面的修改。

更重要的是在RBAC中,我们更希望权限是绑定在角色上,脱离数据。

设计思路

我们希望改动能兼顾到以下几个点:

  1. 授权,修改,增删改等完全脱离于代码,全部动态可控。
  2. 权限精细到url的某个方法。如:get, post, delete等。
  3. 配置方便,代码入侵小,很方便的迁移。

很优秀的地方是,Django Rest Framework允许我们自定义权限。加上一些配合性的改动,可以很轻松的实现上述的要求。

最佳实现

1.权限体系梳理

在RBAC体系下,我们针对的是角色授权。举例,在我自行实现的框架中,权限是以权限码(PermissionCode)的形式关联到角色上。

前端在登录后获取用户token,并请求加载菜单,权限码等必要信息。根据权限码来决定某些菜单或者按钮是否要展示。

1
2
3
4
5
6
7
// 获取权限码
export function getPermCodeByUserId(params: GetUserInfoByUserIdParams) {
return defHttp.get<string[]>({
url: Api.GetPermCodeByUserId + params.userId + '/',
params,
});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!--决定某些按钮是否显示-->
<template>
<a-button v-if="hasPermission(['20000', '2000010'])" color="error" class="mx-4">
拥有[20000,2000010]code可见
</a-button>
</template>
<script lang="ts">
import { usePermission } from '/@/hooks/web/usePermission';
import { RoleEnum } from '/@/enums/roleEnum';

export default defineComponent({
setup() {
const { hasPermission } = usePermission();
return { hasPermission };
},
});
</script>

所以关键点是如何将现有的权限码关联到我们的方法上

2.后端实现

以权限码为基础,后端的改造大致分为两个部分:

  1. ApiView 的改造,基于原有的方法拓展自定义的权限属性.
  2. 增加DRF自定义权限,针对我们自定义的权限属性进行验证。

针对ApiView 的改动较为简单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from rest_framework.views import APIView
class PermissionAPIView(APIView):
"""
基于ApiView封装的权限视图,支持设置方法级权限。

:permission_code 权限码(基于角色的权限Code), 格式为数字或数字组成的列表
:permission_code_by_action 方法级权限码设置,格式为字典嵌套方法名,如{'get': [100, 200]}
"""
permission_code = []
permission_code_by_method = {}

def __init_subclass__(cls, **kwargs):
# 预检查配置及部分易出错场景
PermissionAPIView.pre_check(cls)
# 初始化类并注册
super().__init_subclass__(**kwargs)

@staticmethod
def pre_check(cls):
"""针对传入的cls检查permission_code等参数是否符合格式要求"""
# AnonymousUser无法使用PermissionCode进行检查
assert not cls.permission_code and not cls.permission_code_by_method or cls.authentication_classes, \
'PermissionAPIView cannot be used in AnonymousUser, please check class: %s' % cls.__name__

这里我们封装了两个属性,permission_codepermission_code_by_action

权限支持Int和List格式,分别对应单个和多个。基于method的权限是字典格式,支持设置get, post等key,value和单个权限格式一致,支持Int和List格式。

自定义权限设置较为复杂,因为其包含了格式检查等多个操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
from rest_framework import permissions
class CodePermission(permissions.BasePermission):
REQUEST_METHOD_MAP = {
'list': 'GET',
'create': 'POST',
'destroy': 'DELETE',
'delete': 'DELETE',
'retrieve': 'GET',
'put': 'PUT',
'get': 'GET',
'post': 'POST',
'patch': 'PATCH'
}

def check_permission_format(self, permission_code, permission_code_by_method, view):
"""检查配置的权限是否合法,不满足格式要求会抛异常"""
module_path = "{}.{}".format(view.__module__, view.__name__)
if permission_code and permission_code is not None and not isinstance(permission_code, (int, list)):
raise TypeError(
'<%s> permission_code type was not int or list' % module_path
)
else:
if isinstance(permission_code, list):
for action_code in permission_code:
if not isinstance(action_code, int):
raise TypeError(
'<%s> permission_code list value "%s" type was not int!' % (module_path, action_code)
)
if permission_code_by_method and permission_code_by_method is not None:
""""""
if not isinstance(permission_code_by_method, dict):
raise TypeError(
'<%s> permission_code_by_method type must dict!' % module_path
)
for action in permission_code_by_method:
if str(action).lower() not in self.REQUEST_METHOD_MAP:
raise TypeError(
'<%s> permission_code_by_method: action "%s" not support, allow:[%s]' % (
module_path, action, ",".join(self.REQUEST_METHOD_MAP.keys())
)
)
if not hasattr(view, action):
raise AttributeError(
'<%s> permission_code_by_method: method "%s" is undefined in this class.' % (
module_path, action
)
)
if not isinstance(permission_code_by_method.get(action, None), (int, list)):
raise TypeError(
'<%s> permission_code_by_method: "%s" permission_code type must int or list, not %s.'
% (module_path, action, type(permission_code_by_method.get(action, None)).__name__)
)
else:
if isinstance(action_list := permission_code_by_method.get(action, None), list):
for action_code in action_list:
if not isinstance(action_code, int):
raise TypeError(
'<%s> permission_code_by_method: "%s" code list value "%s" type was not int' % (
module_path, action, action_code
)
)

def get_permission(self, view) -> tuple:
"""获取配置的权限"""
return getattr(view, 'permission_code') if hasattr(view, 'permission_code') else None, \
getattr(view, 'permission_code_by_method') if hasattr(view, 'permission_code_by_method') else None

def is_require_permission(self, view) -> bool:
"""检查类是否需要权限访问"""
permission_code, permission_code_by_method = self.get_permission(view)

if permission_code or permission_code_by_method:
return True
return False

@staticmethod
def query_permission(role_id, code) -> bool:
"""查询权限code,True为满足所有,False为任一不满足。None视为不需要权限,返回True"""
if isinstance(code, int):
code = [code]
elif role_id is None:
return True

from apps.user.models import PermissionCodeRoleRelation
for c in code:
if isinstance(c, list):
if not CodePermission.query_permission(role_id=role_id, code=c):
return False
continue
if PermissionCodeRoleRelation.objects.filter(role_id=role_id, code=c).count() == 0:
return False
return True

def has_permission(self, request, view):
if self.is_require_permission(view):
if not request.user.role_id:
return False

permission_code, permission_code_by_method = self.get_permission(view)
self.check_permission_format(permission_code, permission_code_by_method, view)
if permission_code_by_method and isinstance(permission_code_by_method, dict):
permission_action = list(
set(permission_code_by_method.keys()) &
set([r for r in self.REQUEST_METHOD_MAP if self.REQUEST_METHOD_MAP[r] == request.method])
)
permission_action_code = [permission_code_by_method[k] for k in permission_code_by_method if
k in permission_action and permission_code_by_method[k]]
else:
permission_action_code = []
return self.query_permission(role_id=request.user.role_id, code=permission_code) & self.query_permission(
role_id=request.user.role_id, code=permission_action_code
)
else:
return True

大体流程为:1.检查PermissionAPIView是否设置了权限,如设置继续判断 -> 2.获取permission_code等权限信息,检查格式配置是否正确 -> 3.查询权限配置表,判断用户角色是否存在权限。 -> 4.返回最终结果

最后修改配置,将此权限设置为drf默认权限即可。

使用

使用时非常简单且灵活,继承PermissionAPIView后声明权限码即可。

1
2
3
4
5
6
7
class UserProfile(PermissionAPIView):
permission_code = 1000 # 整体要求:角色拥有权限码为 1000
permission_code_by_method = {
'get': [1001], # 整体要求基础上,get方法额外要求1001权限码
'post': [1002, 1004], # 整体要求基础上,post方法额外要求1002, 1004权限码
'delete': 1003 # 整体要求基础上,删除方法要求1003权限码
}

后续

后面将会讨论再此体系下,类似于阿里云RAM子账号的实现方式。

核心功能是:

  1. 允许父账号生成子账号,并支持基于父账号的权限,全部或部分权限授权给子账号,支持随时吊销或修改。
  2. 权限码的自动注册,管理。