leokale-zz

参考博客:https://www.cnblogs.com/yuanchenqi/articles/8719520.html

一、实现登录验证

1.创建User和Token表

User表用作用户名密码认证,Token表用于存放用户每次成功登陆后的随机Token。

在models.py中添加以下两张表:

# 用户表
class User(models.Model):
    username = models.CharField(max_length=32)
    password = models.CharField(max_length=32)


# token表
class Token(models.Model):
    user = models.OneToOneField("User", on_delete=models.CASCADE)
    token = models.CharField(max_length=128)

执行命令,生成数据库表:

python manage.py makemigrations
python manage.py migrate

2.实现登录验证操作

添加路由条目:

urlpatterns = [
    path(\'admin/\', admin.site.urls),
    re_path(\'^publishes/$\', views.PublishView.as_view(), name="publish"),
    re_path(\'^publishes/(?P<pk>\d+)/$\', views.PublishDetailView.as_view(), name="publishdetail"),
    re_path(\'^books/$\', views.BookView.as_view(), name="book"),
    re_path(\'^books/(?P<pk>\d+)/$\', views.BookDetailView.as_view(), name="bookdetail"),
    re_path(\'^authors/$\', views.AuthorViewSet.as_view({"get": "list", "post": "create"}), name="author"),
    re_path(\'^authors/(?P<pk>\d+)/$\', views.AuthorViewSet.as_view(
        {"get": "retrieve", "put": "update", "patch": "partial_update", "delete": "destroy"}), name="authordetail"),
    re_path(\'^login/$\', views.LoginView.as_view(), name="login"),
]

实现视图类LoginView:

# 导入User和Token的model类
from .models import User
from .models import Token


# 生成一个随机token,username和ctime的MD5加密值
def get_random_str(user):
    import hashlib
    import time
    # 获取当前时间
    ctime = str(time.time())
    # username的md5
    md5 = hashlib.md5(bytes(user, encoding=\'utf-8\'))
    # 加上ctime
    md5.update(bytes(ctime, encoding=\'utf-8\'))
    return md5.hexdigest()


class LoginView(APIView):
    def post(self, request):
        res = {\'code\': 1000, "msg": None}
        try:
            # 从post请求中获取用户提交的用户名和密码
            username = request.data.get("username")
            password = request.data.get("password")
            # 判断数据库中的数据是否匹配
            user_obj = User.objects.filter(username=username, password=password).first()
            # 如果不匹配,返回登录失败
            if not user_obj:
                res[\'code\'] = 1001
                res[\'msg\'] = "用户名或密码错误"
            else:
                # 如果匹配,则生成一个随机token
                token = get_random_str(username)
                # 如果token已经存在,则更新,如果不存在,则创建
                Token.objects.update_or_create(user=user_obj, defaults={\'token\': token})
                res["token"] = token
        except Exception as e:
            res[\'code\'] = 1002
            res[\'msg\'] = e

        return HttpResponse(json.dumps(res))

二、实现token认证

1.实现token认证

要实现认证,只需要在需要认证的视图类中添加 authentication_classes 列表。restframe认证组件会自动去该列表中寻找认证使用的类(类由我们来定义)。例如BookView视图类中:

class BookView(APIView):
    authentication_classes = [TokenAuth,]
    pass

查看restframework调用authentication_classes中类的源码,可以看到TokenAuth中必须实现 authenticate方法,以及 authenticate_header 方法:

class TokenAuth(object):
    # 认证token过程
    def authenticate(self, request):
        token = request.GET.get("token")
        token_obj = Token.objects.filter(token=token).first()
        if not token_obj:
            raise exceptions.AuthenticationFailed("验证失败")
        return (token_obj.user, token_obj)

    def authenticate_header(self, request):
        return None

或者,继承 BaseAuthentication也可以:

from rest_framework.authentication import BaseAuthentication

class TokenAuth(BaseAuthentication):
    # 认证token过程
    def authenticate(self, request):
        token = request.GET.get("token")
        token_obj = Token.objects.filter(token=token).first()
        if not token_obj:
            raise exceptions.AuthenticationFailed("验证失败")
        return (token_obj.user, token_obj)

然后应用于BookView:

class BookView(APIView):
    authentication_classes = [TokenAuth]

    def get(self, request):
        book_list = Book.objects.all()
        bs = BookModelSerializers(book_list, many=True, context={\'request\': request})

        return Response(bs.data)

    def post(self, request):
        bs = BookModelSerializers(data=request.data)
        if bs.is_valid():
            bs.save()
            return Response(bs.data)
        else:
            return Response(bs.errors)

这样,我们想要通过GET请求获取book数据的时候,就需要先访问login页面,获取token,然后在GET请求中附带token,才能正确获取book数据:

 

2.测试

POST请求访问http://127.0.0.1:8000/login/,附带用户名和密码,进行登录验证:

 

获得返回值:

{"code": 1000, "msg": null, "token": "91dc33a308cd4e8b04e14bb3d23d492b"}

然后GET请求访问http://127.0.0.1:8000/books/?token=91dc33a308cd4e8b04e14bb3d23d492b:

获得返回结果:

[{"id":8,"publish":"http://127.0.0.1:8000/publishes/1/","title":"Python2标准库3","price":99,"pub_date":"2012-11-20T13:03:33Z","authors":[1,2]},{"id":9,"publish":"http://127.0.0.1:8000/publishes/1/","title":"Python2标准库4","price":99,"pub_date":"2012-11-20T13:03:33Z","authors":[1,2]},{"id":10,"publish":"http://127.0.0.1:8000/publishes/1/","title":"Python2标准库5","price":99,"pub_date":"2012-11-20T13:03:33Z","authors":[1,2]},{"id":11,"publish":"http://127.0.0.1:8000/publishes/1/","title":"Python2标准库6","price":99,"pub_date":"2012-11-20T13:03:33Z","authors":[1,2]},{"id":12,"publish":"http://127.0.0.1:8000/publishes/1/","title":"Python2标准库7","price":99,"pub_date":"2012-11-20T13:03:33Z","authors":[1,2]},{"id":13,"publish":"http://127.0.0.1:8000/publishes/1/","title":"Python2标准库","price":99,"pub_date":"2012-11-20T13:03:33Z","authors":[1,2]},{"id":14,"publish":"http://127.0.0.1:8000/publishes/1/","title":"Python3","price":99,"pub_date":"2020-01-20T13:03:04Z","authors":[3]},{"id":15,"publish":"http://127.0.0.1:8000/publishes/1/","title":"JAVA","price":99,"pub_date":"2020-01-20T13:03:04Z","authors":[3]},{"id":16,"publish":"http://127.0.0.1:8000/publishes/1/","title":"JAVA","price":99,"pub_date":"2020-01-20T13:03:04Z","authors":[3]},{"id":17,"publish":"http://127.0.0.1:8000/publishes/1/","title":"JAVA","price":99,"pub_date":"2020-01-20T13:03:04Z","authors":[3]},{"id":18,"publish":"http://127.0.0.1:8000/publishes/1/","title":"hello","price":99,"pub_date":"2020-01-20T13:03:04Z","authors":[3]}]

如果未携带token,或携带的token错误:

返回结果:

{"detail":"验证失败"}

三、restframework配置

1.引子

在第二节中,我们实现了token的生成和认证,在认证时,我们使用自定义的TokenAuth类来进行认证,但是如果在每个视图类中都加上 authentication_classes 列表,比较冗余。

我们观察restframe的源码,可以看到,当我们不添加 authentication_classes 列表变量时,APIView中 authentication_classes 变量会读取一个默认值:

class APIView(View):

    # The following policies may be set at either globally, or per-view.
    renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
    parser_classes = api_settings.DEFAULT_PARSER_CLASSES
    authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES
    throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES
    ...
    ...

继续查看api_settings所属类的源码:

class APISettings:
    def __init__(self, user_settings=None, defaults=None, import_strings=None):
        if user_settings:
            self._user_settings = self.__check_user_settings(user_settings)
        self.defaults = defaults or DEFAULTS
        self.import_strings = import_strings or IMPORT_STRINGS
        self._cached_attrs = set()
    ...
    ...

这里的DEFAULTS就是restframework的默认配置:

DEFAULTS = {
    # Base API policies
    \'DEFAULT_RENDERER_CLASSES\': [
        \'rest_framework.renderers.JSONRenderer\',
        \'rest_framework.renderers.BrowsableAPIRenderer\',
    ],
    \'DEFAULT_PARSER_CLASSES\': [
        \'rest_framework.parsers.JSONParser\',
        \'rest_framework.parsers.FormParser\',
        \'rest_framework.parsers.MultiPartParser\'
    ],
    \'DEFAULT_AUTHENTICATION_CLASSES\': [
        \'rest_framework.authentication.SessionAuthentication\',
        \'rest_framework.authentication.BasicAuthentication\'
    ],
    \'DEFAULT_PERMISSION_CLASSES\': [
        \'rest_framework.permissions.AllowAny\',
    ],
    \'DEFAULT_THROTTLE_CLASSES\': [],
    \'DEFAULT_CONTENT_NEGOTIATION_CLASS\': \'rest_framework.negotiation.DefaultContentNegotiation\',
    \'DEFAULT_METADATA_CLASS\': \'rest_framework.metadata.SimpleMetadata\',
    \'DEFAULT_VERSIONING_CLASS\': None,

    # Generic view behavior
    \'DEFAULT_PAGINATION_CLASS\': None,
    \'DEFAULT_FILTER_BACKENDS\': [],

    # Schema
    \'DEFAULT_SCHEMA_CLASS\': \'rest_framework.schemas.openapi.AutoSchema\',

    # Throttling
    \'DEFAULT_THROTTLE_RATES\': {
        \'user\': None,
        \'anon\': None,
    },
    \'NUM_PROXIES\': None,

    # Pagination
    \'PAGE_SIZE\': None,

    # Filtering
    \'SEARCH_PARAM\': \'search\',
    \'ORDERING_PARAM\': \'ordering\',

    # Versioning
    \'DEFAULT_VERSION\': None,
    \'ALLOWED_VERSIONS\': None,
    \'VERSION_PARAM\': \'version\',

    # Authentication
    \'UNAUTHENTICATED_USER\': \'django.contrib.auth.models.AnonymousUser\',
    \'UNAUTHENTICATED_TOKEN\': None,

    # View configuration
    \'VIEW_NAME_FUNCTION\': \'rest_framework.views.get_view_name\',
    \'VIEW_DESCRIPTION_FUNCTION\': \'rest_framework.views.get_view_description\',

    # Exception handling
    \'EXCEPTION_HANDLER\': \'rest_framework.views.exception_handler\',
    \'NON_FIELD_ERRORS_KEY\': \'non_field_errors\',

    # Testing
    \'TEST_REQUEST_RENDERER_CLASSES\': [
        \'rest_framework.renderers.MultiPartRenderer\',
        \'rest_framework.renderers.JSONRenderer\'
    ],
    \'TEST_REQUEST_DEFAULT_FORMAT\': \'multipart\',

    # Hyperlink settings
    \'URL_FORMAT_OVERRIDE\': \'format\',
    \'FORMAT_SUFFIX_KWARG\': \'format\',
    \'URL_FIELD_NAME\': \'url\',

    # Input and output formats
    \'DATE_FORMAT\': ISO_8601,
    \'DATE_INPUT_FORMATS\': [ISO_8601],

    \'DATETIME_FORMAT\': ISO_8601,
    \'DATETIME_INPUT_FORMATS\': [ISO_8601],

    \'TIME_FORMAT\': ISO_8601,
    \'TIME_INPUT_FORMATS\': [ISO_8601],

    # Encoding
    \'UNICODE_JSON\': True,
    \'COMPACT_JSON\': True,
    \'STRICT_JSON\': True,
    \'COERCE_DECIMAL_TO_STRING\': True,
    \'UPLOADED_FILES_USE_URL\': True,

    # Browseable API
    \'HTML_SELECT_CUTOFF\': 1000,
    \'HTML_SELECT_CUTOFF_TEXT\': "More than {count} items...",

    # Schemas
    \'SCHEMA_COERCE_PATH_PK\': True,
    \'SCHEMA_COERCE_METHOD_NAMES\': {
        \'retrieve\': \'read\',
        \'destroy\': \'delete\'
    },
}
View Code

 

前面代码中,使用 api_settings.DEFAULT_AUTHENTICATION_CLASSES ,api_settings没有这个属性,所以会自动调用 APISettings 的__getattr__()方法:

def __getattr__(self, attr):
    if attr not in self.defaults:
        raise AttributeError("Invalid API setting: \'%s\'" % attr)
    try:
        # Check if present in user settings
        val = self.user_settings[attr]
    except KeyError:
        # Fall back to defaults
        val = self.defaults[attr]
    # Coerce import strings into classes
    if attr in self.import_strings:
        val = perform_import(val, attr)
    # Cache the result
    self._cached_attrs.add(attr)
    setattr(self, attr, val)
    return val

__getattr__()方法先判断DEFAULTS中是否存在 DEFAULT_AUTHENTICATION_CLASSES ,如果不存在则报错。然后去user_settings中获取 DEFAULT_AUTHENTICATION_CLASSES 的值,user_settings是一个属性方法:

@property
def user_settings(self):
    if not hasattr(self, \'_user_settings\'):
        self._user_settings = getattr(settings, \'REST_FRAMEWORK\', {})
    return self._user_settings

这段代码会先去django的settings中查看是否存在名为"REST_FRAMEWORK"的配置项。所以我们要使用自定义的认证类,可以在django的settings中配置REST_FRAMEWORK来指定。

首先,将TokenAuth类从views.py移到单独的一个模块,例如utils.py:

# utils.py

from rest_framework import exceptions
from .models import Token
from rest_framework.authentication import BaseAuthentication

class TokenAuth(BaseAuthentication):
    # 认证token过程
    def authenticate(self, request):
        token = request.GET.get("token")
        token_obj = Token.objects.filter(token=token).first()
        if not token_obj:
            raise exceptions.AuthenticationFailed("验证失败")
        return (token_obj.user, token_obj)

然后在django的settings中添加配置:

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": ["demo.utils.TokenAuth"]
}

这样,我们的所有视图类在被访问时都会使用TokenAuth类来对token进行验证,但是在访问/login/页面时,由于还没有登录认证,所以不能进行token验证。

可以在LoginView视图类中,加上一个空的 authentication_classes 列表来处理:

class LoginView(APIView):
    authentication_classes = []
    ...
    ...

这样,访问/login/的时候不会验证token,而访问其他资源的时候会验证token。

 

ღ♋

分类:

技术点:

相关文章: