文档章节

Django REST framework的各种技巧——3.权限

duoduo3_69
 duoduo3_69
发布于 2016/02/01 19:20
字数 2047
阅读 769
收藏 3

django内置强大的权限系统,restframework也完全支持,为什么不用呢?

Django REST framework的各种技巧【目录索引】

文档

django permission的文档 restframework permission的文档

权限的类型

  • 用户是否有访问某个api的权限
  • 用户对于相同的api不同权限看到不同的数据(其实一个filter)
  • 不同权限用户对于api的访问频次,其他限制等等
  • 假删除,各种级联假删除

基本讲解

首先在django中,group以及user都可以有很多的permission,一个user会有他自身permission+所有隶属group的permission。比如:user可能显示的有3个permission,但他隶属于3个组,每个组有2个不同的权限,那么他有3+2*3个权限。

permission会在api的models.py种统一建立,在Meta中添加对应的permission然后跑migrate数据库就会有新的权限。

由于django对每一个不同的Model都会建立几个基本的权限,我会在api/models.py里面单独建一个ModulePermission的Model,没有任何其他属性,就只有一个Meta class上面对应各种权限,这就是为了syncdb用的,另外还一个原因后面再说。

class ModulePermission(models.Model):
    class Meta:
        # 命名为cms设计稿里面对应 '菜单权限' 的地方, 例如用户管理
        permissions = ( 
            ("information.announcement", u"资讯管理-通知公告"),
            ("information.examinfo", u"资讯管理-考试信息"),
            ("information.memberschool", u"资讯管理-会员学校"),
            ("school.school", u"学校管理-学校管理"),
            ("course.course", u"课程管理-课程管理"),
            ("student.student", u"学生管理-学生管理"),
            ("exam.exam", u"考务管理-考试管理"),
            ("exam.room", u"考务管理-考场管理"),
            ...
        )

api访问权限的具体使用

permission_classes = (IsAuthenticated, ModulePermission)有一个ModulePermission,说明需要有对应module的权限才可以访问这个api,什么权限捏?module_perms = ['course_course'],拥有了这个api的访问权限后可以看到这个。 功能是跟学校权限有关的东西,所以需要过滤学校数据 filter_backends = [SchoolPermissionFilterBackend,],这个后面再表。

class CourseDetailView(UnActiveModelMixin, DeleteForeignObjectRelModelMixin, RetrieveUpdateDestroyAPIView):

    filter_backends = [SchoolPermissionFilterBackend,]
    serializer_class = CourseSerializer
    permission_classes = (IsAuthenticated, ModulePermission)
    queryset = Course.objects.filter(is_active=True).order_by('-id')
    module_perms = ['course.course']

    def get_serializer_class(self):
        if self.request.method in SAFE_METHODS:
            return CourseFullMessageSerializer
        else:
            return CourseSerializer

ModulePermission的实现

为毛把所有的权限都放在api里面呢?就是为了下面这个东西好写,因为直接从user.get_all_permissions拿到的permission会带模块名,放在api.models下的东东都会叫api.xxx,看下面实现就懂了

# -*- coding: utf-8 -*-
from rest_framework.permissions import BasePermission, SAFE_METHODS

class ModulePermission(BasePermission):
    '''
    ModulePermission, 检查一个用户是否有对应某些module的权限

    APIView需要实现module_perms属性:
        type: list
        example: ['information.information', 'school.school']

    权限说明:
        1. is_superuser有超级权限
        2. 权限列表请在api.models.Permission的class Meta中添加(请不要用数据库直接添加)
        3. 只要用户有module_perms的一条符合结果即认为有权限, 所以module_perms是or的意思
    '''

    authenticated_users_only = True

    def has_perms(self, user, perms):
        user_perms = user.get_all_permissions()
        for perm in perms:
            if perm in user_perms:
                return True
        return False

    def get_module_perms(self, view):
        return ['api.{}'.format(perm) for perm in view.module_perms]

    def has_permission(self, request, view):
        '''
        is_superuser用户有上帝权限,测试的时候注意账号
        '''
        # Workaround to ensure DjangoModelPermissions are not applied
        # to the root view when using DefaultRouter.
        # is_superuser用户有上帝权限
        if request.user.is_superuser:
            return True

        assert view.module_perms or not isinstance(view.module_perms, list), (
            u"view需要override module属性,例如['information.information', 'school.school']"
        )

        if getattr(view, '_ignore_model_permissions', False):
            return True

        if hasattr(view, 'get_queryset'):
            queryset = view.get_queryset()
        else:
            queryset = getattr(view, 'queryset', None)

        assert queryset is not None, (
            'Cannot apply DjangoModelPermissions on a view that '
            'does not set `.queryset` or have a `.get_queryset()` method.'
        )

        return (
            request.user and
            (request.user.is_authenticated() or not self.authenticated_users_only) and
            self.has_perms(request.user, self.get_module_perms(view))
        )


class ModulePermissionOrReadOnly(ModulePermission):
    """
    The request is authenticated with ModulePermission, or is a read-only request.
    """

    def has_permission(self, request, view):
        return (request.method in SAFE_METHODS or super(ModulePermissionOrReadOnly, self).has_permission(request, view))

用户对于相同的api不同权限看到不同的数据

这其实是一个filter

# -*- coding: utf-8 -*-
from django.db import models
from django_extensions.db.models import TimeStampedModel
from django.db.models.signals import post_save
from .signals import create_permisson
 
 
class School(TimeStampedModel):
    MIDDLE_SCHOOL = 1
    COLLEGE = 2
    school_choices = (
        (MIDDLE_SCHOOL, u"中学"),
        (COLLEGE, u"高校")
    )
    category = models.SmallIntegerField(
        choices=school_choices, db_index=True, default=MIDDLE_SCHOOL)
    name = models.CharField(max_length=255, db_index=True)
    ...
    is_active = models.BooleanField(default=True, db_index=True)
 
    class Meta:
        # 创建学校时会创建学校权限, 默认有所有学校权限
        permissions = (
            ("schoolpermission__all", u"全部学校"),
        )
 
    def __unicode__(self):
        return self.name
 
 
post_save.connect(create_permisson, sender=School)

上面的signal,当学校对象创建修改时会对应更新permission里面的记录,保持一致性。

# -*- coding: utf-8 -*-
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType


def create_permisson(sender, instance, created=False, *args, **kwargs):
    '''创建学校时会创建学校权限'''
    from .models import School
    school = instance
    content_type = ContentType.objects.get_for_model(School)
    codename = 'schoolpermission__{}'.format(school.id)
    name = school.name
    if created:
        permission = Permission.objects.create(codename=codename,
                                               name=school.name,
                                               content_type=content_type)
    else:
        if school.is_active:
            # 如果学校建在,有可能老师把学校名字改了,更新学学校名字
            permissions = Permission.objects.filter(codename=codename, content_type=content_type)
            if not permissions.exists():
                Permission.objects.create(codename=codename,
                                          name=school.name,
                                          content_type=content_type)
            else:
                permission = permissions[0]
                if permission.name != school.name:
                    permission.name = school.name
                    permission.save()
        else:
            Permission.objects.filter(codename=codename, content_type=content_type).delete()
    return instance

这样只要跟学校有关的东西都必须有个叫做school的外键字段,在view中添加filter_backends = [SchoolPermissionFilterBackend,],即可根据学校权限过滤

# -*- coding: utf-8 -*-
from django.db.models.fields.related import ReverseSingleRelatedObjectDescriptor
 
class SchoolPermissionFilterBackend(object):
 
    def get_perms(self, user):
        '''获取用户对于学校的权限, return (True, [])
 
            超级管理员有所有用户权限,
 
        '''
        ALL_SCHOOL_PERMISSION = (True, [])
        if user.is_superuser:
            has_all_school_permission = True
            return ALL_SCHOOL_PERMISSION
 
        permissions = user.get_all_permissions()
        perms = []
        for permission in permissions:
            if permission.startswith('school.schoolpermission__'):
                perm = permission.split('school.schoolpermission__')[-1]
                if perm == 'all':
                    return ALL_SCHOOL_PERMISSION
                perms.append(perm)
        return (False, perms)
 
    def filter_queryset(self, request, queryset, view):
        user = request.user
        model_cls = queryset.model
        has_all_school_permission, perms = self.get_perms(user)
        # 如果有所有学校权限则不过滤数据
        if has_all_school_permission:
            return queryset
        # 否则根据用户拥有的学校权限过来学校有关数据
        if hasattr(model_cls, 'school') and isinstance(model_cls.school, ReverseSingleRelatedObjectDescriptor):
            queryset = queryset.filter(school__in=perms, school__is_active=True)
        return queryset

不同权限用户对于api的访问频次,其他限制

官方文档

然而这个我没什么可以说的,继承下重写allow_request即可。

细节

由于对restful规范的理解,我们知道如果定义了一个/terms/接口,那么get请求是获得list,post是新建一个term,然而如果这两个的权限不一样GenericView应该怎么写呢?答案是重写get_permissions方法(你应该怎么知道的呢?看源码)

class TermsView(ListCreateAPIView):

    serializer_class = TermSerializer
    permission_classes = (IsAuthenticated, ModulePermission)
    queryset = Term.objects.filter(is_active=True).order_by('-id')
    module_perms = ['sysadmin.term']

    def get_permissions(self):
        if self.request.method in SAFE_METHODS:
            return [IsAuthenticated()]
        else:
            return [permission() for permission in self.permission_classes]

删除权限

需求是这样的:

  • 假删除
  • 如果没有其他东西对他有外键,则直接可以删
  • 如果其他东西对他有了外键,则需要给提示(用户确定后可以删除)

首先看实现 注意继承顺序一定不能错!!! UnActiveModelMixin对应假删除 DeleteForeignObjectRelModelMixin对应外键检查 RetrieveUpdateDestroyAPIView是默认的rest的view,提供delete支持,主要是需要xxxDestroyAPIView

class UnActiveModelMixin(object):
    """ 
    删除一个对象,并不真删除,级联将对应外键对象的is_active设置为false,需要外键对象都有is_active字段.
    """
    def perform_destroy(self, instance):
        rel_fileds = [f for f in instance._meta.get_fields() if isinstance(f, ForeignObjectRel)]
        links = [f.get_accessor_name() for f in rel_fileds]

        for link in links:
            manager = getattr(instance, link, None)
            if not manager:
                continue
            if isinstance(manager, models.Model):
                if hasattr(manager, 'is_active') and manager.is_active:
                    manager.is_active = False
                    manager.save()
                    raise ForeignObjectRelDeleteError(u'{} 上有关联数据'.format(link))
            else:
                if not manager.count():
                    continue
                try:
                    manager.model._meta.get_field('is_active')
                    manager.filter(is_active=True).update(is_active=False)
                except FieldDoesNotExist as ex: 
                    # 理论上,级联删除的model上面应该也有is_active字段,否则代码逻辑应该有问题
                    logger.warn(ex)
                    raise ModelDontHaveIsActiveFiled(
                            '{}.{} 没有is_active字段, 请检查程序逻辑'.format(
                                manager.model.__module__,
                                manager.model.__class__.__name__
                    ))
        instance.is_active = False
        instance.save()



class DeleteForeignObjectRelModelMixin(object):
    '''删除一个对象,如果他已有外键关联则抛出异常'''
    @POST_OR_GET('force_delete', type='bool', default=False)
    def destroy(self, request, force_delete, *args, **kwargs):
        instance = self.get_object()
        if not force_delete:
            rel_fileds = [f for f in instance._meta.get_fields() if isinstance(f, ForeignObjectRel)]
            links = [f.get_accessor_name() for f in rel_fileds]
            for link in links:
                manager = getattr(instance, link, None)
                if not manager:
                    continue      
                # one to one
                if isinstance(manager, models.Model):
                    if hasattr(manager, 'is_active') and manager.is_active:
                        raise ForeignObjectRelDeleteError(u'{} 上有关联数据'.format(link))
                else:
                    try:
                        manager.model._meta.get_field('is_active')
                        if manager.filter(is_active=True).count():
                            raise ForeignObjectRelDeleteError(u'{} 上有关联数据'.format(link))
                    except FieldDoesNotExist as ex:
                        if manager.count():
                            raise ForeignObjectRelDeleteError(u'{} 上有关联数据'.format(link))
        self.perform_destroy(instance)
        return Response(status=status.HTTP_204_NO_CONTENT)                                  

哦怎么用呢

例如你在admin里面先新建一个group,然后把group上面勾上对应的权限,然后用户的group那儿对应的把组加上即可(当然你可以自己写对应的代码而不用admin)。 不太建议直接在user上面加上对应的权限,虽然完全没有问题。

© 著作权归作者所有

duoduo3_69
粉丝 89
博文 57
码字总数 83746
作品 0
青岛
程序员
私信 提问
Django REST framework的各种技巧【目录索引】

为了防止各位看同学不好找,在这边丢了个目录过来,因为以后可能继续写 目录 [Django REST framework的各种技巧——1.基础讲解][1] [Django REST framework的各种技巧——2.serializer][2] ...

duoduo3_69
2016/02/01
612
1
Django REST framework的各种技巧【目录索引】

为了防止各位看同学不好找,在这边丢了个目录过来,因为以后可能继续写 目录 Django REST framework的各种技巧——1.基础讲解 Django REST framework的各种技巧——2.serializer Django RES...

D咄咄
2017/11/29
0
0
Django REST framework的各种技巧——3.权限

django内置强大的权限系统,restframework也完全支持,为什么不用呢? Django REST framework的各种技巧【目录索引】 文档 django permission的文档 restframework permission的文档 权限的类...

D咄咄
2017/11/29
0
0
Django REST framework的各种技巧——1.基础讲解

写在最上面的话 django是一个神奇的框架,而restframework又是遵循了这个框架的另一个神奇的框架,然而由于restframework的文档稀烂无比,很多时候你必须看源码才能写出科学的代码,这挡住了...

D咄咄
2017/11/29
0
0
Django REST framework的各种技巧——1.基础讲解

写在最上面的话 django是一个神奇的框架,而restframework又是遵循了这个框架的另一个神奇的框架,然而由于restframework的文档稀烂无比,很多时候你必须看源码才能写出科学的代码,这挡住了...

duoduo3_69
2016/02/01
549
0

没有更多内容

加载失败,请刷新页面

加载更多

每天AC系列(六):有效的括号

1 题目 LeetCode第20题,这题比较简单,匹配括号. 2 栈 这是栈的典型应用,括号匹配,当然不需要直接使用栈,使用一个StringBuilder即可: if(s.isEmpty()) return true;char a = s.charAt(0);...

Blueeeeeee
今天
27
0
Spring AOP-06-切入点类型

切入点是匹配连接点的拦截规则。之前使用的是注解@Pointcut,该注解是AspectJ中的。除了这个注解之外,Spring也提供了其他一些切入点类型: • 静态方法切入点StaticMethodMatcherPointcut •...

moon888
昨天
90
0
Class Loaders in Java

1. Introduction to Class Loaders Class loaders are responsible for loading Java classes during runtime dynamically to the JVM (Java Virtual Machine). Also, they are part of the ......

Ciet
昨天
96
0
以Lazada为例,看电商系统架构演进

什么是Lazada? Lazada 2012年成立于新加坡,是东南亚第一电商,2016年阿里投资10亿美金,2017年完成对lazada的收购。 业务模式上Lazada更偏重自营,类似于亚马逊,自建仓储和为商家提供服务...

春哥大魔王的博客
昨天
62
0
【自用】 Flutter Timer 简单用法

dart: void _startTime() async { _timer = Timer(Duration(seconds: sec), () { fun(xxx,yyy,zzz); }); } @override void dispose() { _timer.cancel()......

Tensor丨思悟
昨天
65
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部