django之认证与权限

认证(Authentication)与权限(Permission)不是一回事。

认证是通过用户提供的用户ID/密码组合或者Token来验证用户的身份。认证本身不会允许或拒绝传入的请求,它只是简单识别请求携带的凭证。

权限(Permission)的校验发生验证用户身份以后,决定是否应该接收请求或拒绝访问。

django的认证

对于django,认证模块就是把用户信息绑定到request.user,如果验证通过,则request.user绑定的就是通过验证的用户对象(auth_user表该用户对应的数据object),如果验证不通过,则request.user绑定的是AnonymousUser(匿名用户)。

代码部分

认证相关的代码主要包含三个部分 复制代码
# 相关的代码主要包含三个部分
django/contrib/auth
django/contrib/session
django/contrib/contenttypes

# 相关的配置(例如settings)
INSTALLED_APPS = [
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
]
MIDDLEWARE = [
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
]

# 以下两个非必需,需要覆盖默认值的时候使用
# 用户模型:当默认提供的用户表无法满足需求,需要自定义用户表时使用,用于覆盖Django默认的User模型,指向您自定义的用户模型。格式为 '应用名.模型名'
AUTH_USER_MODEL = "system.Users"
# 认证后端:用于指定Django如何进行用户认证。可以添加自定义的后端以实现如邮箱登录、第三方OAuth登录等复杂认证逻辑。
AUTHENTICATION_BACKENDS = ['django.contrib.auth.backends.ModelBackend']

模型部分

主要涉及三张表,主要字段和关联关系如下:

python 复制代码
# django/contrib/auth/models.py
auth_user表(class User):
   username
   user_permissions(多对多权限表Permission)

auth_permission表(class Permission):
   name
   codename
   content_type(外键ContentType)

# django/contrib/contenttypes/models.py
django_content_type表(class ContentType):
   app_label
   model

表字段样例

django_content_type

所有应用的所有model都会生成一条记录。

id app_lable model
1 auth group
2 auth permission

auth_permission

权限是绑定到model级别的,每个content_type_id对应一个model,对应的code_name就是权限名称的编码。也是在代码中设置要求的权限时要用到的。

id name content_type_id code_name
1 Can add permission 1 add_permission
2 Can change permission 1 change_permission

auth_user

id username is_active
1 张三 1
2 李四 1

auth_user_user_permissions

用户表和权限表多对多关系生成的用户权限表。

id user_id permission_id
1 1 2
2 1 3

处理流程

请求处理的流程如下:
中间件
session中间件
auth中间件
request请求
API视图

session中间件:初始化session对象绑定到request.session。

python 复制代码
# django/contrib/session/middleware.py
class SessionMiddleware(MiddlewareMixin):
    def process_request(self, request):
        session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME)
        request.session = self.SessionStore(session_key)
# 初次访问时,因未携带Cookies数据,获取session_key为None,则初始化一个空对象绑定到request.session
# 若访问携带Cookies,则通过Cookies中的sessionid获取数据表中存储的session_data,
# 解析后绑定到request.session,此时则完成了用户的绑定和认证

auth中间件:初始化一个空对象(延迟计算属性)绑定到request.user。

python 复制代码
# django/contrib/auth/middleware.py
class AuthenticationMiddleware(MiddlewareMixin):
    def process_request(self, request):
        request.user = SimpleLazyObject(lambda: get_user(request))

可以看出中间件的处理仅仅是初始化了两个空对象,没有实质性的动作。

真正是否要开启用户权限校验的开关是API视图来决定的。

开启认证

django框架提供了开启认证的装饰器函数,可以通过快速配置实现权限校验.

  • 1、基于函数的视图(FBV)

    使用样例

    通过在函数上加上装饰器即可开启认证校验。

    python 复制代码
    from django.contrib.auth.decorators import login_required
    
    # 方法一,最常用
    @login_required
    def my_view(request):
        pass
    
    # 方法二
    my_view = login_required(my_view)

    原理解析

    python 复制代码
    # django/contrib/auth/decorators.py
    
    # 装饰器工厂函数
    def user_passes_test(
        test_func,login_url=None,redirect_field_name=REDIRECT_FIELD_NAME
    ):
        def decorator(view_func):
            @wraps(view_func)
            def _wrapper_view(request, *args, **kwargs):
                # 若校验函数返回true,则进入视图处理
                if test_func(request.user):
                    return view_func(request, *args, **kwargs)
                # 若校验函数返回false,转到登陆页
                return redirect_to_login(path, resolved_login_url, redirect_field_name)
            return _wrapper_view
        return decorator
    
    # 认证装饰器
    # 此处function参数是要传入视图函数
    def login_required(
        function=None,redirect_field_name=REDIRECT_FIELD_NAME,login_url=None
    ):
        # 可以看出test_func(request.user)就是要校验request.user.is_authenticated是否为true
        actual_decorator = user_passes_test(
            lambda u: u.is_authenticated,
            login_url=login_url,
            redirect_field_name=redirect_field_name,
        )
        # 对应样例中的方法二,my_view = login_required(my_view)
        if function:
            return actual_decorator(function)
        # 对应样例中的方法一,返回实际的装饰器
        return actual_decorator
  • 2、基于类的视图(CBV)

    使用样例

    通过继承框架提供的混入类实现

    python 复制代码
    from django.contrib.auth.mixins import LoginRequiredMixin
    
    class MyView(LoginRequiredMixin, View):
        login_url = "/login/"
        redirect_field_name = "redirect_to"

    原理解析

    python 复制代码
    class LoginRequiredMixin(AccessMixin):
        def dispatch(self, request, *args, **kwargs):
            # request.user.is_authenticated为false时转到登陆页
            if not request.user.is_authenticated:
                return self.handle_no_permission()
            # request.user.is_authenticated为true时进入视图处理
            return super().dispatch(request, *args, **kwargs)
    
    class AccessMixin:
        def handle_no_permission(self):
            return redirect_to_login(
                path,
                resolved_login_url,
                self.get_redirect_field_name(),
            )
  • 3、自己编码校验

    就是不用框架提供的工具包,自己在视图里校验,不推荐。

    样例

    python 复制代码
    from django.conf import settings
    from django.shortcuts import redirect
    
    def my_view(request):
        # 自行检查 request.user.is_authenticated ,若未登陆则重定向到登录页面
        if not request.user.is_authenticated:
            return redirect(f"{settings.LOGIN_URL}?next={request.path}")
        # ...

    基于以上,可以看出认证需求的主题逻辑就是:
    检查request.user.is_authenticated,true就允许下一步处理,false就跳转登录页

认证内部逻辑解析

因为要检查request.user.is_authenticated,就需要真正执行request.user = SimpleLazyObject(lambda: get_user(request))了。

python 复制代码
# django/contrib/auth/__init__.py
def get_user(request):
  from .models import AnonymousUser
  user = None
  try:
      user_id = _get_user_session_key(request)
      backend_path = request.session[BACKEND_SESSION_KEY]
  except KeyError:
      pass
  else:
      if backend_path in settings.AUTHENTICATION_BACKENDS:
          backend = load_backend(backend_path)
          user = backend.get_user(user_id)
          ...
  # 校验session信息成功则返回用户对象
  # 否则返回匿名用户对象
  return user or AnonymousUser()

若返回AnonymousUser,则request.user.is_authenticated的值为false,则会转到登陆页处理。

登陆处理

django提供了关于登陆、认证的一系列组件,可以直接拿来使用。主要包括:

API view: 位于django/contrib/auth/views.py的LoginView、LogoutView等

工具函数:位于django/contrib/auth/init.py的authenticate(),login()等

但django没有提供登陆页的模板,因此很多时候,都是用户自行编写登陆页、登陆认证相关的视图API等。

不过还是介绍下django提供的登录页的基础逻辑。

python 复制代码
# django/contrib/auth/views.py
class LoginView(RedirectURLMixin, FormView):
  form_class = AuthenticationForm
  # 此处为登陆页的模板,框架并没有提供,需要自行编写
  # 主要就是用户名、密码的输入框,以Post方式提交
  template_name = "registration/login.html"

  def form_valid(self, form):
    auth_login(self.request, form.get_user())
    return HttpResponseRedirect(self.get_success_url())

# django/views/generic/edit.py
class ProcessFormView(View):
   # 提交用户名密码的请求处理入口
   def post(self, request, *args, **kwargs):
     # 获取到form_class = AuthenticationForm,调用AuthenticationForm的is_valid
     # is_valid函数会调用到clean()函数
     form = self.get_form()
     if form.is_valid():
       return self.form_valid(form)
     else:
       return self.form_invalid(form)

# django/contrib/auth/forms.py
class AuthenticationForm(forms.Form):
  def clean(self):
     # clean函数获取表单中的用户名/密码
     username = self.cleaned_data.get("username")
     password = self.cleaned_data.get("password")
     if username is not None and password:
       # 调用auth/__init__.py的authenticate()验证密码
       self.user_cache = authenticate(
          self.request, username=username, password=password)
     return self.cleaned_data


# django/contrib/auth/__init__.py
def authenticate(request=None, **credentials):
  # 对于配置多个认证后端的情况,按顺序任何一个通过即返回
  for backend, backend_path in _get_backends(return_tuples=True):
    try:
      user = backend.authenticate(request, **credentials)

# 前面POST方法中,认证通过后,开始调用self.form_valid(form),这个form_valid就是执行login函数。
# django/contrib/auth/__init__.py 
def login(request, user, backend=None):
    ......
# login这个函数主要是处理session信息,认证成功后,将用户的信息存放到request.session
# 这与惯性思维的login概念不一样,常规思维login是登陆入口,负责认证一些列的动作
# 而这里的login是个工具方法,只是做认证通过后的后续处理方法

自定义登陆

如果要使用django提供的方法,首先登陆页要自行编写,后端认证接口的基本流程如下:

python 复制代码
def post(self, request, *args, **kwargs):
    # 获取用户密码信息
    username = request.data.get('username')
    password = request.data.get('password')
    # 验证用户密码信息
    user = authenticate(username=username, password=password)
    # 验证成功后的处理
    login(request, user)
    return 

认证后端

认证后端可以再settings文件里通过AUTHENTICATION_BACKENDS进行配置。其默认值为('django.contrib.auth.backends.ModelBackend',)。除此之外,可以通过继承BaseBackend自定义认证后端。

自定义认证后端主要关注两个方法:

authenticate:自定义认证方法,主要有校验用户密码等,return True or False

get_user: 根据user_id获取用户对象。这个方法是登陆成功后,后续处理带着cookie信息的请求,由中间件层获取用户对象。

python 复制代码
# django/contrib/auth/backends.py
class BaseBackend:
    def authenticate(self, request, **kwargs):
        return None

    def get_user(self, user_id):
        return None

django的权限

权限是在认证完成后,验证用户是否具有相关视图有访问权限。

权限定义

权限名一般的格式为:{app名}.{权限动作}_{模型名}。

以blog应用为例,Django为Article模型自动创建的4个可选权限名分别为:

查看文章(view): blog.view_article

创建文章(add): blog.add_article

更改文章(change): blog.change_article

删除文章(delete): blog.delete_article

如何增加自定义权限

权限的清单是存储在auth_permission表。

增加自定义权限实质就是在auth_permission表中增加一条记录。

方法1. 在Model的meta属性中添加权限,在执行migrate时会维护权限数据

python 复制代码
class Article(models.Model):
 ...
   class Meta:
     permissions = (
        ("publish_article", "Can publish article"),
        ("comment_article", "Can comment article"),
     )

方法2. 使用ContentType程序化创建权限

比如可以在交互式的shell中,手工增加数据。

python 复制代码
python manage.py shell

>>> from blog.models import Article
>>> from django.contrib.auth.models import Permission
>>> from django.contrib.contenttypes.models import ContentType
>>> 
# 获取model对象的id
>>> content_type = ContentType.objects.get_for_model(article)
# 新增权限
>>> permission1 = Permission.objects.create(
    codename='publish_article',
    name='Can publish articles',
    content_type=content_type,
)
>>> 
>>> permission2 = Permission.objects.create(
    codename='comment_article',
    name='Can comment articles',
    content_type=content_type,
)
>>> 

绑定用户和权限

这个一般需要用户自行编写前端页面和API接口,实现权限管理。

校验权限

跟认证一样,默认情况下是不去校验权限的,如果需要校验权限,需要在接口级别做单独的配置。

  • 1、基于函数的视图(FBV)

    使用样例

    通过在函数上加上装饰器即可开启权限校验。

    python 复制代码
    from django.contrib.auth.decorators import permission_required
    
    # 验证一个权限
    @permission_required('polls.can_vote')
    def my_view(request):
        pass
    
    # 同时具有多个权限才可访问
    @permission_required(('polls.can_vote', 'polls.can_edit'))
    def my_view(request):
        pass    
  • 2、基于类的视图(CBV)

    使用样例

    python 复制代码
    from django.contrib.auth.mixins import PermissionRequiredMixin
    
    # 验证一个权限
    class MyView(PermissionRequiredMixin, View):
        permission_required = 'polls.can_vote'
    
    # 同时具有多个权限才可访问
    class MyView(PermissionRequiredMixin, View):
        permission_required = ('polls.can_open', 'polls.can_edit')

说明:

1、如果permission中指定了多个权限,则是and的关系,必须要同时满足才行。

2、对于CBV情况下,权限是针对所有方法的,比如MyView下有get()、post()方法,则权限校验是全局通用的,而不能针对特定方法。

3、'polls.can_vote'只是一个权限名称。这种权限名是全局性的,可以在任意位置使用,并不是只能用在polls 应用下或者vote的model中使用。

  • 3、针对单个方法的校验

    若需要针对不同的方法设置不同的权限,可以使用method_decorator装饰器。

    python 复制代码
    from django.contrib.auth.decorators import permission_required, login_required
    from django.utils.decorators import method_decorator
    from django.views.generic import View
    from django.http import JsonResponse
    
    class ArticleView(View):
    
        @method_decorator(permission_required('blog.add_article', raise_exception=True))
        def post(self, request, *args, **kwargs):
            """POST 方法需要 blog.add_article 权限"""
            return JsonResponse({'message': '文章创建成功'})
    
        # 可以多个组合
        @method_decorator(login_required)
        @method_decorator(permission_required('blog.view_article', raise_exception=True))
        def get(self, request, *args, **kwargs):
            """需要登录且具有 view_article 权限"""
            return JsonResponse({'message': '查看文章'})

如果还想要实现更复杂的校验机制,就得自己写校验逻辑了。

再回头看看权限校验的源码逻辑

python 复制代码
# django/contrib/auth/decorators.py
def permission_required(perm, login_url=None, raise_exception=False):
  def check_perms(user):
      # 调用用户的has_perms方法
      if user.has_perms(perms):
            return True
      return False
  return user_passes_test(check_perms, login_url=login_url)

# django/contrib/auth/model.py
class PermissionsMixin(models.Model):
  # 用户的has_perms方法,对perm_list里定义的权限进行校验
  def has_perms(self, perm_list, obj=None):
     # 使用all,必须所有的权限都满足才通过
     return all(self.has_perm(perm, obj) for perm in perm_list)

  # 对单个权限进行校验
  def has_perm(self, perm, obj=None):
     return _user_has_perm(self, perm, obj)

  # 具体校验逻辑
  def _user_has_perms(user, perm, obj):
     # 循环所有认证后端,只要有一个后端的校验通过即校验通过
     for backend in auth.get_backends():
       if backend.has_perm(user, perm, obj):
          return True
     return False


# 从以上验证方法可以看出,认证通过的backend和校验的backend没有强制要求是同一个。

django框架自带的权限校验确实存在较多的不足,如果要求不高,确实可以直接拿来开箱使用,如果项目要求较高,大概率要自行编码校验。

主要不足有:

1、不能实现行级别的校验,若用户有权限则对所有数据有权限。比如要限制用户只能编辑自己的文档,不能修改别人的文档。

2、不能实现多权限OR的校验,即若多个权限满足任何一个就通过。

DRF的认证和权限

DRF的认证权限流程
视图处理
中间件
session中间件
auth中间件
DRF认证
DRF权限
DRF视图
request请求

通过django认证的流程解析,我们知道中间件处理部分其实就是解析token,绑定到request.user,其并不会对用户请求进行权限校验和拦截。如果需要拦截,需要在django的视图函数上加上装饰器。

对于DRF,则复用了django的中间件部分,把真正校验的部分放在了视图里。

默认配置

默认配置需要在settings目录下

python 复制代码
REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": (
        "rest_framework_simplejwt.authentication.JWTAuthentication",
        "rest_framework.authentication.SessionAuthentication",
    ),
    "DEFAULT_PERMISSION_CLASSES": [
        "rest_framework.permissions.IsAuthenticated",  # 只有经过身份认证确定用户身份才能访问
    ],
}

视图级别配置

基于类的视图(CBV)

python 复制代码
class ExampleView(APIView):
    authentication_classes = (SessionAuthentication, BasicAuthentication)
    permission_classes = (IsAuthenticated,)

    def get():
        pass

以上在类级别设置的认证类和权限类,是对该类下的所有函数生效的。如果要为函数类的某个函数单独指定认证类或权限类。则可以参考以下例子

python 复制代码
## 方法一,重写get_permission方法
class MyView(APIView):
    # 类级别的默认设置
    authentication_classes = [SessionAuthentication]
    permission_classes = [IsAuthenticated]

    def get_permissions(self):
        """
        根据请求方法动态返回权限类
        """
        if self.request.method == 'GET':
            return [AllowAny()]  # GET 允许任何人
        elif self.request.method == 'POST':
            return [IsAuthenticated()]  # POST 需要登录
        else:
            return [IsAuthenticated()]  # 其他方法也需要登录


# 对于viewset,则可以在action中指定
class UserViewSet(ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer
    permission_classes = [IsAuthenticated]  # 默认需要认证

    @action(detail=False, methods=['post'], 
            authentication_classes=[SessionAuthentication], 
            permission_classes=[AllowAny])
    def register(self, request): 
        pass

基于函数的视图(FBV)

python 复制代码
@api_view(['GET'])
@authentication_classes((SessionAuthentication, BasicAuthentication))
@permission_classes((IsAuthenticated,))
def example_view(request, format=None):
    pass

使用api_view装饰器,即可将django的函数转成DRF下的函数。就可以使用DRF下默认的DEFAULT_AUTHENTICATION_CLASSES、DEFAULT_PERMISSION_CLASSES这些配置。

如果想要为函数单独设置认证类和权限类,则需要使用authentication_classes、permission_classes装饰器。

自定义认证和权限类

python 复制代码
# 自定义认证类
from rest_framework.authentication import BaseAuthentication

class MyAuthentication(BaseAuthentication):

    # 自定义逻辑,返回user对象和认证信息
    def authenticate(self, request):
         return (user, None)


# 自定义权限类
from rest_framework.permissions import BasePermission

class MyPermission(BasePermission):

    # 自定义逻辑,返回true or false
    def has_permission(self, request, view):
        return True or False

调用部分源码解析

以APIView为例,APIView视图的调用入口是该类的dispatch()方法。

python 复制代码
# rest_framework/view.py
class APIView(View):

    def dispatch(self, request, *args, **kwargs):
        request = self.initialize_request(request, *args, **kwargs)
        self.initial(request, *args, **kwargs)
        return self.response

    def initial(self, request, *args, **kwargs):
        # 认证
        self.perform_authentication(request)
        # 权限
        self.check_permissions(request)
        self.check_throttles(request)
相关推荐
青春不败 177-3266-05202 小时前
基于R语言lavaan结构方程模型(SEM)实践技术应用
python·r语言·贝叶斯·生态学·结构方程·sem
费弗里2 小时前
进阶技巧:在Dash应用中直接使用原生React组件
python·dash
Ashley_Amanda2 小时前
Python入门知识点梳理
开发语言·windows·python
tjjucheng2 小时前
小程序定制开发哪家有完整流程
python
海棠AI实验室2 小时前
第十二章 类型标注与可读性:让协作与复用更容易
python
羊村积极分子懒羊羊2 小时前
python课程三月二十九号粗略总结
开发语言·python
深圳蔓延科技2 小时前
Python算法学习分享
python
aloha_7892 小时前
langchain4j如何使用mcp
java·人工智能·python·langchain
yunhuibin2 小时前
CNN基础学习
人工智能·python·深度学习·神经网络