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上面加上对应的权限,虽然完全没有问题。