Python后端部署-Part2——Django登录验证

[TOC]

登录验证的实现思路

参考:

  • 采用 token 认证方式,使用 rest_framework_simplejwt 库配置权限认证。
  • Simple JWT 为 Django REST Framework 框架提供了一个 JSON Web 令牌认证后端。
  • 注意:使用 rest_framework_simplejwt 进行身份认证时并不需要去对数据库进行查询校验,所以并不会将 token 保存在数据库中。

安装使用 rest_framework

  1. 安装

    1
    pip install djangorestframework
  2. 添加 rest_frameworkINSTALLED_APPS 设置中

    1
    2
    3
    4
    5
    6
    # -> backend/WebService/settings.py

    INSTALLED_APPS = [
    ...
    'rest_framework', # DRF
    ]
  3. 若使用 DRF 的可浏览 API,则修改路由 urls.py(在生产环境中是不需要的)

    1
    2
    3
    4
    5
    6
    # -> backend/WebService/urls.py

    urlpatterns = [
    ...
    path('api-auth/', include('rest_framework.urls')),
    ]

    安装使用 rest_framework_simplejwt

  4. 安装

    1
    pip install djangorestframework_simplejwt
  5. 添加 rest_framework_simplejwtINSTALLED_APPS 设置中

    1
    2
    3
    4
    5
    6
    7
    # -> backend/WebService/settings.py

    INSTALLED_APPS = [
    ...
    'rest_framework.authtoken',
    'rest_framework_simplejwt', # jwt
    ]
  6. 将 Simple JWT 的 JSON Web 令牌认证添加到身份验证类列表中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    # -> backend/WebService/settings.py

    REST_FRAMEWORK = {
    # 全局的权限认证,只有通过认证后才赋予用户权限
    'DEFAULT_PERMISSION_CLASSES': (
    'rest_framework.permissions.IsAuthenticated',
    ),
    # 身份验证类列表,可以设定多个身份验证
    'DEFAULT_AUTHENTICATION_CLASSES': (
    'rest_framework_simplejwt.authentication.JWTAuthentication',
    )
    }
  7. 配置 JWT 相关参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # -> backend/WebService/settings.py

    from datetime import timedelta

    SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=1), # token 的过期时间
    'REFRESH_TOKEN_LIFETIME': timedelta(days=15), # 刷新 token 的过期时间
    # 'AUTH_HEADER_TYPES': ('Bearer', 'JWT'), # token 的请求头类型
    }
  8. 在任意的路由配置中添加 Simple JWT 提供的视图(这里将登录验证模块集成到 login 应用下)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # -> backend/login/urls.py

    from django.urls import path
    from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView, TokenVerifyView

    urlpatterns = [
    path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), # POST 登录接口
    path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), # POST 刷新token接口
    path('token/verify/', TokenVerifyView.as_view(), name='token_verify'), # POST 验证token接口 用于vue前端写路由守卫
    ...
    ]
  9. 如果自定义用户表

    1
    2
    3
    4
    # -> backend/WebService/settings.py

    # AUTH_USER_MODEL 配置默认的校验用户表
    AUTH_USER_MODEL = 'login.UserInfo'

    安装使用 django-simple-captcha

  10. 安装

    1
    pip install django-simple-captcha
  11. 添加 captchaINSTALLED_APPS

    1
    2
    3
    4
    5
    6
    # -> backend/WebService/settings.py

    INSTALLED_APPS = [
    ...
    'captcha', # 生成验证码
    ]
  12. 需要更新数据库

    1
    2
    python manage.py makemigrations
    python manage.py migrate

    无验证码的登录验证

  13. 其实配置完 simplejwt 时,即可直接使用登录验证。

    • 创建超级用户,命令行输入:python3 manage.py createsuperuser

    • 使用 post 请求获取 token 令牌

      1
      2
      headers:Content-Type:application/json
      body:{"username": "admin", "password": "admin"}
    • 携带 token 令牌请求访问后端

      1
      body: {"token": "token"}
  14. 之后编写的其他视图函数,都要继承 from rest_framework.views import APIView ,否则将无法进行身份验证。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # -> backend/login/views.py

    from rest_framework.views import APIView

    class test(APIView): # 使用 token 之后,应当继承 APIView 类
    def get(self, request):
    return JsonResponse({"msg": "ok"}, json_dumps_params={"ensure_ascii": False})

    class test_no_login(APIView): # 不需要进行登录验证的逻辑则添加 permission_classes = []
    permission_classes = []

    def get(self, request):
    return JsonResponse({"msg": "ok"}, json_dumps_params={"ensure_ascii": False})

    带验证码的登录验证

构造返回验证码的视图

  1. 返回验证码的视图接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    # -> backend/login/views.py

    import base64
    import json
    from django.http import HttpResponse
    from captcha.views import CaptchaStore, captcha_image
    from django.views import View

    class CaptchaAPIView(View):

    def get(self, request):
    hash_key = CaptchaStore.generate_key()
    try:
    # 获取图片id
    id_ = CaptchaStore.objects.filter(hashkey=hash_key).first().id
    image = captcha_image(request, hash_key)
    # 将图片转换为base64
    image_base = 'data:image/png;base64,%s' % base64.b64encode(image.content).decode('utf-8')
    json_data = json.dumps({"id": id_, "image_base": image_base})
    # 批量删除过期验证码
    CaptchaStore.remove_expired()
    except:
    json_data = None
    return HttpResponse(json_data, content_type="application/json")
  2. 添加验证码视图的接口路由

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # -> backend/login/urls.py

    from django.urls import path

    from login.views import CaptchaAPIView

    urlpatterns = [
    ...
    path('captcha/', CaptchaAPIView.as_view(), name='captcha_api'), # GET 返回验证码接口
    ]

    重写带验证码的登录视图

  3. 构造一个手动返回令牌的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    # -> backend/login/utils/get_token.py

    from rest_framework_simplejwt.tokens import RefreshToken

    def get_tokens_for_user(user):
    # 手动返回令牌
    refresh = RefreshToken.for_user(user)

    return {
    'refresh': str(refresh),
    'access': str(refresh.access_token),
    }
  4. 在扩展 TokenObtainPairView 视图前先扩展该序列化类 TokenObtainPairSerializer

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    # -> backend/login/serializer.py

    from django.utils import timezone
    from rest_framework import serializers
    from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
    from django.contrib.auth import authenticate

    from captcha.fields import CaptchaStore
    from login.utils.get_token import get_tokens_for_user


    class DmallTokenObtainPairSerializer(TokenObtainPairSerializer):
    captcha = serializers.CharField(max_length=4, required=True,
    trim_whitespace=True, min_length=4,
    error_messages={
    "max_length": "图片验证码仅允许4位",
    "min_length": "图片验证码仅允许4位",
    "required": "请输入图片验证码"
    }, help_text="图片验证码")
    imgcode_id = serializers.CharField(required=True, write_only=True,
    help_text="图片验证码id")

    @classmethod
    def get_token(cls, user):
    token = super().get_token(user)
    token['captcha'] = user.captcha
    token['imgcode_id'] = user.imgcode_id
    return token

    def validate_captcha(self, captcha):
    # 验证码验证
    try:
    captcha = captcha.lower()
    except:
    raise serializers.ValidationError("验证码错误")
    img_code = CaptchaStore.objects.filter(
    id=int(self.initial_data['imgcode_id'])
    ).first()
    if img_code and timezone.now() > img_code.expiration:
    raise serializers.ValidationError("图片验证码过期")
    else:
    if img_code and img_code.response == captcha:
    pass
    else:
    raise serializers.ValidationError("验证码错误")

    def validate(self, attrs):
    # 删除验证码
    del attrs['captcha']
    del attrs['imgcode_id']
    authenticate_kwargs = {
    'username': attrs['username'],
    'password': attrs['password'],
    }
    # 验证当前登录用户
    self.user = authenticate(**authenticate_kwargs)
    if self.user is None:
    raise serializers.ValidationError('账号或密码不正确')
    # 登录成功返回token信息
    token = get_tokens_for_user(self.user)
    return token
  5. 重写 TokenObtainPairView 视图函数

    1
    2
    3
    4
    5
    6
    7
    8
    # -> backend/login/views.py

    from rest_framework_simplejwt.views import TokenObtainPairView
    from login.serializer import DmallTokenObtainPairSerializer

    class DmallTokenObtainPairView(TokenObtainPairView):
    # 登录成功返回token
    serializer_class = DmallTokenObtainPairSerializer
  6. 添加登录视图的接口路由

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # -> backend/login/urls.py

    from django.urls import path

    from login.views import DmallTokenObtainPairView

    urlpatterns = [
    ...
    path('captcha/token/', DmallTokenObtainPairView.as_view(), name='mytoken'),
    ]
  7. 测试

    image-20211126124446649