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') 

其中 RoleRolePermission 用于管理角色和对应的权限的关系

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单字段的修改权限也就不在话下了