django 基于角色的权限控制
2017-12-25
ENV: Python3.6 + django1.11
应用场景
有一种场景, 要求为用户赋予一个角色, 基于角色(比如后管理员,总编, 编辑), 用户拥有相应的权限(比如管理员拥有所有权限, 总编可以增删改查, 编辑只能增改, 有些页面的按钮也只有某些角色才能查看), 角色可以任意添加, 每个角色的权限也可以任意设置
django 的权限系统
django 默认的权限是基于Model
的add, change, delete来做权限判断的, 这种设计方式有一个明显的缺陷, 比如怎么控制对Model
的某个字段的修改的权限的控制呢
设计权限
大多数的系统, 都会给用户赋予某个角色, 假如能针对用户的角色, 做权限控制,这个权限控制并不局限于对Model
的修改, 可以是任意位置的权限控制, 只要有一个权限名, 即可根据用户角色名下是否拥有权限名判断是否拥有权限
User, Role => UserRole => RolePermissions
User 是用户对象
Role: 角色表
UserRole 是用户角色关系对象
RolePermissions 是角色权限关系对象
因此, 需要创建三个Model
: User
,Role
, UserRole
, RolePermission
User
可以使用django 默认的User
对象
其他Model 如下
class Role(models.Model):
"""角色表"""
# e.g add_user
role_code = models.CharField('role code', max_length=64, unique=True, help_text = '用户角色标识')
# e.g 新增用户
role_name = models.CharField('role name', max_length=64, help_text = '用户角色名')
class UserRole(models.Model):
"""用户角色关系表"""
user_id = models.IntegerField('user id', blank=False, help_text='用户id', unique=True)
role_codes = models.CharField('role codes', blank=True, default=None, max_length=256, help_text='用户的角色codes')
class RolePermission(models.Model):
"""角色权限关系表"""
role_code = models.CharField('role code', max_length=64, blank=False, help_text = '用户角色标识')
pm_code = models.CharField('permission code', blank=False, max_length=64, help_text='权限code')
class Meta:
unique_together = ('role_code', 'pms_code')
其中 Role
和 RolePermission
用于管理角色和对应的权限的关系
UserRole
用于管理用户和角色的映射关系
权限管理
用户角色拥有哪些权限是在代码里定义好的, 比如:
PMS_MAP = (
('PM_ADD_USER', '新增用户'),
('PM_SET_MAIL', '编辑邮箱'),
...
)
PM_ADD_USER
是权限code码, 新增用户
是权限名, 在这里, 权限名由我们定义, 后面在需要使用的地方做has_perm(<pm_coede>)
判断时, 用的就是这是这个code
角色管理
在定义好权限后, 我们就可以做角色管理了,
在这里, 我们可以创建任意的角色, 为其分配任意的权限, 当然, 最好创建有意义的角色
角色表单定义(forms.py
)
role_regex_validator = RegexValidator(r"[a-zA-Z0-9]", "角色标记只能包含字母,数字, 下划线")
class RoleForm(forms.Form):
role_row_code = forms.IntegerField(required=False, widget=forms.HiddenInput())
role_code = forms.CharField(label='角色标记', min_length=3, max_length=64, validators=[role_regex_validator])
role_name = forms.CharField(label='角色名', min_length=3, max_length=64)
OPTIONS = PMS_MAP
pms = forms.MultipleChoiceField(label='权限列表', widget=forms.SelectMultiple(choices=OPTIONS)
角色编辑views.py
def role_edit(request):
"""角色编辑"""
if request.method == 'POST':
role_row_id = request.POST.get('role_row_id', 0)
role_code = request.POST.get('role_code', '')
role_name = request.POST.get('role_name', '')
pms = request.POST.getlist('pms', [])
# 表单校验
role_form = RoleForm({
'role_row_id': role_row_id,
'role_code': role_code,
'role_name': role_name,
'pms': pms
})
# 表单校验
if not role_form.is_valid():
return render(request, 'role_form.html', {'form': role_form)
role_row_id = role_form.cleaned_data.get('role_row_id', None)
if role_row_id:
# 角色更新
return update_role(request, role_form, role_row_id=role_row_id, role_code=role_code,
role_name=role_name, pms=pms)
else:
# 角色创建
return add_role(request, role_form, role_code, role_name, pms=pms)
else:
# 角色编辑页面
role_row_id = request.GET.get('id')
try:
role_item = Role.objects.get(pk=role_row_id)
except Role.DoesNotExist as e:
role_item = None
if role_item:
# 编辑已有角色表单
# 获取角色权限列表
role_pms_rows = RolePermission.objects.filter(role_code=role_item.role_code)
pms_codes = [role_pms_row.pms_code for role_pms_row in role_pms_rows]
role_form = RoleForm({
'role_row_id': role_row_id,
'role_code': role_item.role_code,
'role_name': role_item.role_name,
'pms': pms_codes
})
else:
# 新增角色表单
role_form = RoleForm()
return render(request, 'role_form.html', {'form': role_form})
def add_role(request, role_form, role_code, role_name, pms=()):
"""新增角色"""
try:
with transaction.atomic():
role_item = Role.objects.create(role_code=role_code, role_name=role_name)
for pm_code in pms:
RolePermission.objects.update_or_create(role_code=role_code, pms_code=pm_code)
return redirect('{}?id={}'.format(reverse('user_role_edit'), role_item.pk))
except IntegrityError as e:
# 创建出错
role_form.add_error('role_code', '角色已经存在: {}'.format(role_code))
return render(request, 'role_form.html', {'form': role_form})
def update_role(request, role_form, role_row_id, role_code, role_name, pms=()):
"""更新角色"""
try:
with transaction.atomic():
role_item = Role.objects.get(pk=role_row_id)
# 校验合法性
if not role_item:
raise Http404('非法的role记录id')
role_item.role_name = role_name
role_item.save()
# 删除原角色权限设置
RolePermission.objects.filter(role_code=role_code).delete()
for pm_code in pms:
RolePermission.objects.update_or_create(role_code=role_code, pms_code=pm_code)
return redirect('{}?id={}'.format(reverse('user_role_edit'), role_row_id))
except IntegrityError as e:
# 更新出错
role_form.add_error('role_name', '更新角色名出错:{}'.format(role_name))
return render(request, 'role_form.html', {'form': role_form})
表单部分html
<form class='form-horizontal' action='/user/role/edit' method='POST'>
<p>用户角色编辑</p>
{{form.non_field_errors}}
{% csrf_token %}
{% for hidden_field in form.hidden_fields %}
{{ hidden_field }}
{% endfor %}
{% for field in form.visible_fields %}
<div class='form-group'>
<label class='col-lg-2 control-label'>{{field.label}}</label>
{% if field.errors %}
<div class='col-lg-3 has-error'>
{{field}}
{% for error in field.errors %}
<p><span class='help-block m-b-none'>{{error}}</span><p>
{% endfor %}
</div>
{% else %}
<div class='col-lg-3'>
{{field}}
</div>
{% endif%}
</div>
{% endfor %}
<div class="form-group">
<div class="col-lg-offset-2 col-lg-10">
<button class="btn btn-sm btn-white" type="submit">Save</button>
</div>
</div>
</form>
至此用户角色编辑就完成了
还有一部分是用户角色分配
说白了就是编辑用户时, 给用户选择一个角色, 将用户id和角色code 通过UserRole
存到数据库中, 这一部分请各位自己实现吧 :)
权限判断
如果我们有了一个用户, 并赋予了一个角色, 应该怎么判断其是否有某个权限呢
在django的认证体系配置里, 有一项配置是AUTHENTICATION_BACKENDS
, 比如, 我们希望对接sso 单点登录, 就可以在这里添加配置
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
'cas.backends.CASBackend', # 单点登录实现
)
这个配置项下每个字符串都对应一个类, 每个类继承并了一组接口实现中的全部或其中一部分
这组接口定义了用户认证和授权的相关内容, 如下
authenticate
get_user_permissions
get_group_permissions
has_perm
...
其中 has_perm 就是直接判断是否拥有某个权限的接口
在 AUTHENTICATION_BACKENDS
中, 只要有一个类的 has_perm 判定用户拥有某个权限即可认为用户拥有该权限
因此, 我们实现自己的has_perm
实现
class PermBackend(ModelBackend):
def has_perm(self, user_obj, pms_code, obj):
if not user_obj.is_active:
return False
# 超级管理员拥有所有权限
if user_obj.is_superuser:
return True
try:
user_roles_record = UserRole.objects.get(user_id=user_obj.pk)
except UserRole.DoesNotExist as e:
return False
# 获取用户的角色(暂时是单个角色)
user_roles = user_roles_record.role_codes.split(',')
# 角色对应的权限集合
role_pms_rows = RolePermission.objects.filter(role_code__in=user_roles)
pms_codes = [role_pms_row.pms_code for role_pms_row in role_pms_rows]
# pms_code 是否在用户的权限code集合中
return pms_code in pms_codes
当然, 别忘了把PermBackend
放到 AUTHENTICATION_BACKENDS
最后
现在, 我们在views funcion的实现前添加 @permission_required(<pms_code>)
装饰器就能根据当前用户的角色, 判定是否拥有某个<pms_code>
权限啦
无论是任何地方, 只要定义唯一的pms_code
, 赋给角色, 并将角色分配给某个用户, 就可以实现粒度很深的权限控制, 在本文开始的地方的所说的对某个Model
单字段的修改权限也就不在话下了