DRF:源码剖析 - 认证组件

引言

一、引入

在编程的世界中,我们认为,用户输入的所有数据都是不可靠的,不合法的,直接使用用户输入的数据是不安全的。

很久很久以前,Web站点只是作为浏览文档和其他资源的工具,甚少有什么用户交互之类的烦人的事情需要处理,所以,Web站点的开发这根本不关心什么人在什么时候访问了什么资源,不需要记录任何数据,有客户端请求,我即返回数据,简单方便,每一个http请求都是新的,响应之后立即断开连接。

而如今,不管是论坛类、商城类、社交类、门户类还是其他各类Web站点,大家都非常重视用户交互,只有跟用户交互了,才能进一步留住用户,只有留住了用户,才能知道用户需求,知道了用户需求,就是有商机,有了流量,才能够骗到…额…抱歉…是融到钱,有了资金企业才能继续发展,可见,用户交互是非常重要的,甚至可以说是至关重要的一个基础功能。

而谈到用户交互,必须要谈到我们知识点,认证、权限和频率

二、token令牌

我们知道cookie和session两种方式可以保存用户信息,这两种方式不同的是cookie保存在客户端浏览器中,而session保存在服务器中,他们各有优缺点,配合起来使用,可将重要的敏感的信息存储在session中,而在cookie中可以存储不太敏感的数据。

token称之为令牌。cookie、session和token都有其应用场景,没有谁好谁坏,不过我们开发数据接口类(视图中的类)的Web应用,目前用token还是比较多的。

token认证的大致步骤是这样的:

(1)用户登录,服务器端获取用户名密码,查询用户表,如果存在该用户且第一次登录(或者token过期),生成token,否则返回错误信息
(2)如果不是第一次登录,且token未过期,更新token值

因此,我们需要修改model.py,增加用户表、token表

models.py

 

认证组件

一、前提准备

使用认证组件,判断用户是否登录成功,认证登录身份,需要修改models.py,增加用户表和token表

1、settings中app注册 rest_framework

2、models.py

from django.db import models

# Create your models here.

# -------------------    用于用户认证表  -------------------

class User(models.Model):
    username = models.CharField(max_length=32)
    password = models.CharField(max_length=32)
    user_type_entry = (
        (1, 'Delux'),
        (2, 'SVIP'),
        (3, 'VVIP')
    )
    user_type = models.IntegerField(choices=user_type_entry)

class UserToken(models.Model):
    user = models.OneToOneField('User', on_delete=models.CASCADE)
    token = models.CharField(max_length=128)


# -------------------  图书管理相关表 -------------

class Author(models.Model):
    nid = models.AutoField(primary_key=True)
    name = models.CharField(max_length=32)
    age = models.IntegerField()

    def __str__(self):
        return self.name

class Publish(models.Model):
    nid = models.AutoField(primary_key=True)
    name = models.CharField(max_length=32)
    city = models.CharField(max_length=32)
    email = models.EmailField()

    def __str__(self):
        return self.name


class Book(models.Model):
    nid = models.AutoField(primary_key=True)
    title = models.CharField(max_length=32)
    price = models.DecimalField(max_digits=5, decimal_places=2)
    # 外键字段(一对多关系)
    publish = models.ForeignKey(to="Publish", related_name="book", related_query_name="book_query", on_delete=models.CASCADE)
    # 多对多字段
    authors = models.ManyToManyField(to="Author")
View Code

3、创建序列化类

# 序列化类
from rest_framework import serializers

class BookSerializer(serializers.ModelSerializer):
    class Meta:
        model = Book

        fields = ('title',
                  'price',
                  'publish',
                  'authors',
                  'author_list',
                  'publish_name',
                  'publish_city'
                  )  # 返回给前端,显示的字段
        extra_kwargs = {
            'publish': {'write_only': True},
            'authors': {'write_only': True}
        }  # 该字段只可写,不可读,不能够返回给前端

    publish_name = serializers.CharField(max_length=32, read_only=True,
                                         source='publish.name')  # 在field中,必须声明出该字段publish_name,否则会报错
    publish_city = serializers.CharField(max_length=32, read_only=True, source='publish.city')

    author_list = serializers.SerializerMethodField()  # 多对多的关系,需要SerializerMethodField,并且用"get_字段名"方法,取值

    def get_author_list(self, book_obj):
        # 拿到queryset开始循环 [{}, {}, {}, {}]
        authors = list()
        for author in book_obj.authors.all():
            authors.append(author.name)
        return authors
View Code

二、局部组件的认证

1、urls.py

from django.contrib import admin
from django.urls import path,include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('serializer/', include('serializer.urls')),
]

 

urlpatterns = [
    # re_path(r'books/$', views.BookView.as_view()),
    # re_path(r'books/(?P<pk>\d+)/$', views.BookFilterView.as_view()),

    re_path(r'books/$', views.BookView.as_view({
        'get': 'list',
        'post': 'create'
    })),
    re_path(r'books/(?P<pk>\d+)/$', views.BookView.as_view({
        'get': 'retrieve',
        'put': 'update',
        'delete': 'destroy'
    })),
    re_path(r'user/$',views.UserView.as_view())
]

2、生成 token

用户登录的视图函数中,生成 token,存储在 token 表中

from rest_framework.views import APIView
from .utils import get_token
class UserView(APIView): def post(self, request): response = {} # 定义返回的消息 fields = {'username', 'password'} # 定义需要的用户信息 user_info = {} # 定义一个用户信息字典 if fields.issubset(set(request.data.keys())): # 判断是否为子集合 # username = request.data.get("username") # password = request.data.get("password") for key in fields: user_info[key] = request.data[key] if user_info: user_instance = User.objects.filter(**user_info).first() # 使用filter需要判断参数是否不为空,因为为空会报错 if user_instance is not None: # 用户存在, access_token = get_token.generate_token() # 在表中写入token值 UserToken.objects.update_or_create(user=user_instance, defaults={ 'token': access_token }) response['status_code'] = 200 response['status_message'] = '登陆成功' response['access_token'] = access_token response['user_role'] = user_instance.get_user_type_display() else: response['status_code'] = 201 response['status_message'] = '登录失败,用户名或密码错误' return JsonResponse(response)

utils / get_token.py

  获取随机字符串

import uuid

#  获取token随机字符串
def generate_token():
    random_str = str(uuid.uuid4()).replace('-','')
    return random_str

3、创建认证类

utils / authentication_classes.py

方式一:(用该方法即可)

from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import APIException

from ..models import UserToken


class UserAuth(BaseAuthentication):     # 如果没有继承该类,就必须要写 authenticate_header(self,request)方法

    # 所有的逻辑都在这里面
    def authenticate(self, request):
        user_token = request.query_params.get('token')   # query_params是固定的

        try:
            token = UserToken.objects.get(token = user_token)  # 携带的token与表中的token值一样
            # 后面的权限会用到
            return token.user,token.token     # 这里返回,以后在request.user、request.auth 中接收,在权限认证的时候,直接使用request.user局能够获取到当前的登录用户
    
    except Exception: raise APIException('用户没有认证')

方式二:

from rest_framework.exceptions import APIException
class UserAuth():
def authenticate_header(self, request): pass
def authenticate(self,request): user_token = request.query_params.get("token") #获取的浏览器客户端的token try: token = UserToken.objects.get(token=user_token) #如果数据库中没有与客户端匹配的token就会 报错 return token.user.username,token.token except Exception: raise APIException("没有认证")

4、视图类中,添加认证类

class BookView(ModelViewSet):

    authentication_classes = [UserAuth]   # 指定认证的类,变量名必须是 authentication_classes

    queryset = Book.objects.all()
    serializer_class = BookSerializer

 

源码分析

 

三、多个认证类实现

我们还可以指定多个认证类,只是需要注意的是,如果需要返回什么数据,请在最后一个认证类中返回,因为如果在前面返回,在self._authentication()方法中会对返回值进行判断,如果不为空,认证的过程就会中止,多个认证类的实现方式如下:

class UserAuth2(object):

    def authenticate_header(self, request):
        pass
   def authenticate(self, request):
        raise APIException("认证失败")


class UserAuth(object):

    def authenticate_header(self, request):
        pass

    def authenticate(self, request):
        user_post_token = request.query_params.get('token')

        token_object = UserToken.objects.filter(token=user_post_token).first()
        if token_object:
            return token_object.user.username, token_object.token
        else:
            raise APIException("认证失败")


class BookView(ModelViewSet):

    authentication_classes = [UserAuth2, UserAuth]    # 必须最后一个认证类有返回值

如果不希望每次都写那个无用的authenticate_header方法,我们可以这样:

from rest_framework.authentication import BaseAuthentication

class UserAuth2(BaseAuthentication):

    def authenticate(self, request):
        raise APIException("认证失败")


class UserAuth(BaseAuthentication):

    def authenticate(self, request):
        user_post_token = request.query_params.get('token')

        token_object = UserToken.objects.filter(token=user_post_token).first()
        if token_object:
            return token_object.user.user_name, token_object.token
        else:
            raise APIException("认证失败")

四、全局认证组件

如果希望所有的数据接口都需要认证怎么办?很简单,还是根据之前的经验,就是这句代码:

authentication_classes=api_settings.DEFAULT_AUTHENTICATION_CLASSES

如果认证类自己没有authentication_classes,就会到settings中去找,通过这个机制,我们可以将认证类写入到settings文件中即可实现全局认证:

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'authenticator.utils.authentication.UserAuth',
        'authenticator.utils.authentication.UserAuth2',
    ),
}

 

posted @ 2018-12-11 08:41  葡萄想柠檬  Views(143)  Comments(0)    收藏  举报
目录代码