暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Django:用DJoser模块解决「用户注册、登录」相关的需求

Nephilim 2024-05-16
343

Tips:一些记录,一些笔记



2024/05/14

TUESDAY

Life is a kind of art, in under the premise of not enough sufficient conclusions.

生活是一种艺术,要在不充足的前提下得出充足的结论。




01

Django Djoser


官方GitHub:

https://github.com/sunscrapers/djoser


官方PyPi:

https://pypi.org/project/djoser/


官方文档:

https://djoser.readthedocs.io/en/latest/getting_started.html


Django认证系统的REST实现;

Djoser库提供了一组「Django Rest Framework」视图,用于处理注册、登录、注销、密码重置和账户激活等基本操作;

它们适用于自定义用户模型。


Djoser并没有重写Django的代码,而是重新实现了一些东西,以便于更好的适应单页面应用程序的体系结构。


它支持的身份验证后端有两种:

  • 基于DRF的身份认证Token

  • 基于djangorestframework_simplejwt的JWT认证


02

安装


通过Python的「PIP」去安装:

「pip install -U djoser」

    (venv) (base) adamhuan@Leviathan django_daily_media % pip list
    Package Version
    ------------------- -------
    asgiref 3.8.1
    cffi 1.16.0
    cryptography 42.0.7
    Django 5.0.6
    django-cors-headers 4.3.1
    django-filter 24.2
    djangorestframework 3.15.1
    pip 24.0
    pycparser 2.22
    PyMySQL 1.1.0
    setuptools 69.2.0
    sqlparse 0.5.0
    typing_extensions 4.11.0
    wheel 0.43.0
    (venv) (base) adamhuan@Leviathan django_daily_media %
    (venv) (base) adamhuan@Leviathan django_daily_media % pip install -U djoser
    Looking in indexes: https://mirrors.aliyun.com/pypi/simple/
    Collecting djoser
    Downloading https://mirrors.aliyun.com/pypi/packages/82/12/f7351496223339be99cd70f79bd1641fe2ea47abfe42a3da168e8d9344ce/djoser-2.2.2-py3-none-any.whl (42 kB)
    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 42.6/42.6 kB 2.6 MB/s eta 0:00:00
    Requirement already satisfied: django>=3.0.0 in ./venv/lib/python3.10/site-packages (from djoser) (5.0.6)
    Collecting django-templated-mail<2.0.0,>=1.1.1 (from djoser)
    Downloading https://mirrors.aliyun.com/pypi/packages/f3/ec/0f42b730e17ca087aa79a7aadecff7957a867709f04bd0354e72120e9f68/django_templated_mail-1.1.1-py3-none-any.whl (4.7 kB)
    Collecting djangorestframework-simplejwt<6.0,>=5.0 (from djoser)
    Downloading https://mirrors.aliyun.com/pypi/packages/f2/ab/88f73cf08d2ad3fb9f71b956dceca5680a57f121e5ce9a604f365877d57e/djangorestframework_simplejwt-5.3.1-py3-none-any.whl (101 kB)
    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 101.3/101.3 kB 2.9 MB/s eta 0:00:00
    Collecting social-auth-app-django<6.0.0,>=5.0.0 (from djoser)
    Downloading https://mirrors.aliyun.com/pypi/packages/61/e3/60318a13a5d58daa0b37eb2b8b0bada5a9d5725efb407358cebedbcc0ada/social_auth_app_django-5.4.1-py3-none-any.whl (26 kB)
    Requirement already satisfied: asgiref<4,>=3.7.0 in ./venv/lib/python3.10/site-packages (from django>=3.0.0->djoser) (3.8.1)
    Requirement already satisfied: sqlparse>=0.3.1 in ./venv/lib/python3.10/site-packages (from django>=3.0.0->djoser) (0.5.0)
    Requirement already satisfied: djangorestframework>=3.12 in ./venv/lib/python3.10/site-packages (from djangorestframework-simplejwt<6.0,>=5.0->djoser) (3.15.1)
    Collecting pyjwt<3,>=1.7.1 (from djangorestframework-simplejwt<6.0,>=5.0->djoser)
    Downloading https://mirrors.aliyun.com/pypi/packages/2b/4f/e04a8067c7c96c364cef7ef73906504e2f40d690811c021e1a1901473a19/PyJWT-2.8.0-py3-none-any.whl (22 kB)
    Collecting social-auth-core>=4.4.1 (from social-auth-app-django<6.0.0,>=5.0.0->djoser)
    Downloading https://mirrors.aliyun.com/pypi/packages/85/9e/c4b21c66ff73bcbd605ba06b7c48260559ec50b5a4216ce2736053fe1bc1/social_auth_core-4.5.4-py3-none-any.whl (410 kB)
    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 410.2/410.2 kB 5.7 MB/s eta 0:00:00
    Requirement already satisfied: typing-extensions>=4 in ./venv/lib/python3.10/site-packages (from asgiref<4,>=3.7.0->django>=3.0.0->djoser) (4.11.0)
    Collecting requests>=2.9.1 (from social-auth-core>=4.4.1->social-auth-app-django<6.0.0,>=5.0.0->djoser)
    Downloading https://mirrors.aliyun.com/pypi/packages/70/8e/0e2d847013cb52cd35b38c009bb167a1a26b2ce6cd6965bf26b47bc0bf44/requests-2.31.0-py3-none-any.whl (62 kB)
    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 62.6/62.6 kB 5.2 MB/s eta 0:00:00
    Collecting oauthlib>=1.0.3 (from social-auth-core>=4.4.1->social-auth-app-django<6.0.0,>=5.0.0->djoser)
    Downloading https://mirrors.aliyun.com/pypi/packages/7e/80/cab10959dc1faead58dc8384a781dfbf93cb4d33d50988f7a69f1b7c9bbe/oauthlib-3.2.2-py3-none-any.whl (151 kB)
    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 151.7/151.7 kB 7.6 MB/s eta 0:00:00
    Collecting requests-oauthlib>=0.6.1 (from social-auth-core>=4.4.1->social-auth-app-django<6.0.0,>=5.0.0->djoser)
    Downloading https://mirrors.aliyun.com/pypi/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl (24 kB)
    Requirement already satisfied: cryptography>=1.4 in ./venv/lib/python3.10/site-packages (from social-auth-core>=4.4.1->social-auth-app-django<6.0.0,>=5.0.0->djoser) (42.0.7)
    Collecting defusedxml>=0.5.0rc1 (from social-auth-core>=4.4.1->social-auth-app-django<6.0.0,>=5.0.0->djoser)
    Downloading https://mirrors.aliyun.com/pypi/packages/5d/c7/6b4ad89ca6f7732ff97ce5e9caa6fe739600d26c5d53c20d0bf9abb79ec5/defusedxml-0.8.0rc2-py2.py3-none-any.whl (25 kB)
    Collecting python3-openid>=3.0.10 (from social-auth-core>=4.4.1->social-auth-app-django<6.0.0,>=5.0.0->djoser)
    Downloading https://mirrors.aliyun.com/pypi/packages/e0/a5/c6ba13860bdf5525f1ab01e01cc667578d6f1efc8a1dba355700fb04c29b/python3_openid-3.2.0-py3-none-any.whl (133 kB)
    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 133.7/133.7 kB 7.2 MB/s eta 0:00:00
    Requirement already satisfied: cffi>=1.12 in ./venv/lib/python3.10/site-packages (from cryptography>=1.4->social-auth-core>=4.4.1->social-auth-app-django<6.0.0,>=5.0.0->djoser) (1.16.0)
    Collecting charset-normalizer<4,>=2 (from requests>=2.9.1->social-auth-core>=4.4.1->social-auth-app-django<6.0.0,>=5.0.0->djoser)
    Downloading https://mirrors.aliyun.com/pypi/packages/46/6a/d5c26c41c49b546860cc1acabdddf48b0b3fb2685f4f5617ac59261b44ae/charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl (120 kB)
    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 120.4/120.4 kB 6.4 MB/s eta 0:00:00
    Collecting idna<4,>=2.5 (from requests>=2.9.1->social-auth-core>=4.4.1->social-auth-app-django<6.0.0,>=5.0.0->djoser)
    Downloading https://mirrors.aliyun.com/pypi/packages/e5/3e/741d8c82801c347547f8a2a06aa57dbb1992be9e948df2ea0eda2c8b79e8/idna-3.7-py3-none-any.whl (66 kB)
    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 66.8/66.8 kB 4.6 MB/s eta 0:00:00
    Collecting urllib3<3,>=1.21.1 (from requests>=2.9.1->social-auth-core>=4.4.1->social-auth-app-django<6.0.0,>=5.0.0->djoser)
    Downloading https://mirrors.aliyun.com/pypi/packages/a2/73/a68704750a7679d0b6d3ad7aa8d4da8e14e151ae82e6fee774e6e0d05ec8/urllib3-2.2.1-py3-none-any.whl (121 kB)
    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 121.1/121.1 kB 6.6 MB/s eta 0:00:00
    Collecting certifi>=2017.4.17 (from requests>=2.9.1->social-auth-core>=4.4.1->social-auth-app-django<6.0.0,>=5.0.0->djoser)
    Downloading https://mirrors.aliyun.com/pypi/packages/ba/06/a07f096c664aeb9f01624f858c3add0a4e913d6c96257acb4fce61e7de14/certifi-2024.2.2-py3-none-any.whl (163 kB)
    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 163.8/163.8 kB 7.2 MB/s eta 0:00:00
    Requirement already satisfied: pycparser in ./venv/lib/python3.10/site-packages (from cffi>=1.12->cryptography>=1.4->social-auth-core>=4.4.1->social-auth-app-django<6.0.0,>=5.0.0->djoser) (2.22)
    Installing collected packages: django-templated-mail, urllib3, pyjwt, oauthlib, idna, defusedxml, charset-normalizer, certifi, requests, python3-openid, requests-oauthlib, social-auth-core, djangorestframework-simplejwt, social-auth-app-django, djoser
    Successfully installed certifi-2024.2.2 charset-normalizer-3.3.2 defusedxml-0.8.0rc2 django-templated-mail-1.1.1 djangorestframework-simplejwt-5.3.1 djoser-2.2.2 idna-3.7 oauthlib-3.2.2 pyjwt-2.8.0 python3-openid-3.2.0 requests-2.31.0 requests-oauthlib-2.0.0 social-auth-app-django-5.4.1 social-auth-core-4.5.4 urllib3-2.2.1
    (venv) (base) adamhuan@Leviathan django_daily_media %
    (venv) (base) adamhuan@Leviathan django_daily_media % pip list
    Package Version
    ----------------------------- --------
    asgiref 3.8.1
    certifi 2024.2.2
    cffi 1.16.0
    charset-normalizer 3.3.2
    cryptography 42.0.7
    defusedxml 0.8.0rc2
    Django 5.0.6
    django-cors-headers 4.3.1
    django-filter 24.2
    django-templated-mail 1.1.1
    djangorestframework 3.15.1
    djangorestframework-simplejwt 5.3.1
    djoser 2.2.2
    idna 3.7
    oauthlib 3.2.2
    pip 24.0
    pycparser 2.22
    PyJWT 2.8.0
    PyMySQL 1.1.0
    python3-openid 3.2.0
    requests 2.31.0
    requests-oauthlib 2.0.0
    setuptools 69.2.0
    social-auth-app-django 5.4.1
    social-auth-core 4.5.4
    sqlparse 0.5.0
    typing_extensions 4.11.0
    urllib3 2.2.1
    wheel 0.43.0
    (venv) (base) adamhuan@Leviathan django_daily_media %


    如果打算使用JWT的认证方式,你还需要安装:

      pip install -U djangorestframework_simplejwt


      如果打算使用基于第三方的身份认证,则需要安装:

        pip install -U social-auth-app-django


        03

        配置


        全局配置文件「settings.py」

          INSTALLED_APPS = (
          'django.contrib.auth',
          (...),
          'rest_framework',
          'djoser',
          (...),
          )


          全局路由「urls.py」

            """
            URL configuration for django_daily_media project.


            The `urlpatterns` list routes URLs to views. For more information please see:
            https://docs.djangoproject.com/en/5.0/topics/http/urls/
            Examples:
            Function views
            1. Add an import: from my_app import views
            2. Add a URL to urlpatterns: path('', views.home, name='home')
            Class-based views
            1. Add an import: from other_app.views import Home
            2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
            Including another URLconf
            1. Import the include() function: from django.urls import include, path
            2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
            """
            from django.contrib import admin
            from django.urls import path, include


            from rest_framework.routers import DefaultRouter


            from movie.views import *


            router = DefaultRouter()
            router.register(
            'movie', MovieViewSet
            )
            router.register(
            'category', CategoryViewSet
            )


            urlpatterns = [


            # Django Admin
            path("admin/", admin.site.urls),


            # Django Rest Framework
            path("api/", include(router.urls)),


            # Django Djoser
                path("auth/", include("djoser.urls")),
            ]



            04

            页面访问


            http://127.0.0.1:8000/auth/


            http://127.0.0.1:8000/auth/users/


            Djoser支持的端点:


            05

            通过「User Craete」Endpoints创建用户

            官方文档:

            https://djoser.readthedocs.io/en/latest/base_endpoints.html

            通过Postman发送请求:

            结果:

            可以看到,会对密码做基本的校验。


            当密码强度通过校验后,可以看到,用户创建就成功了:

            然后,在Django的后台「认证和授权 用户」中,就可以看到新增的用户了:


            以上,是最简单的用户注册的演示。


            06

            Djoser「配置」


            其实,除了上面演示的基础功能,Djoser还有更高级的用法。

            你可以通过配置的方式去定义Djoser模块的行为,下面来具体展示下这方面的知识点。


            有关「配置」的官方文档:

            https://djoser.readthedocs.io/en/latest/settings.html


            文件「settings.py」

              # 邮箱
              EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
              EMAIL_HOST = "smtp.163.com"
              EMAIL_PORT = 25
              EMAIL_HOST_USER = "leviathan_litan@163.com"
              EMAIL_HOST_PASSWORD = "通过网易邮箱获取的随机「授权密码」"
              DEFAULT_FROM_EMAIL = "leviathan_litan@163.com"


              # Djoser
              DJOSER = {
              "USER_ID_FIELD": "username",
              "LOGIN_FIELD": "email",
              'ACTIVATION_URL': 'activate/{uid}/{token}',
              'SEND_ACTIVATION_EMAIL': True,
              'SEND_CONFIRMATION_EMAIL': True,
              }


              然后,通过Postman注册:

              可以看到,注册成功。


              看看后端数据库中的状态:

              可以看到,当前该用户并没有激活「is_active」为「0」。


              去刚刚我们注册的时候的邮箱中收取「注册邮件」

              可以从激活的链接:

              http://127.0.0.1:8000/activate/Ng/c71gbe-0e4eebd2fbe9fc840a73666c06b55174

              看到:

              • uid「Ng

              • token「c71gbe-0e4eebd2fbe9fc840a73666c06b55174


              从Postman激活:

              http://127.0.0.1:8000/auth/users/activation/

              可以看到,激活成功了。


              在DB中查看一下:


              当你激活成功后,根据DJOSER的设置,你会收到一封激活成功的邮件:


              不过通常情况下,一般用户不会通过Postman的方式去操作,而是更习惯直接点击链接。


              但默认情况下,你是没有单独配置「activate」的路由的,因此,你直接点击发送给你的邮件中的激活链接的时候,你会遇到「Page Not Found」的错误:

              要解决这个问题,你需要去单独定义「activate」的路由。


              07

              为DJOSER的「激活」配置路由


              创建应用:

                (venv) (base) adamhuan@Leviathan django_daily_media % python manage.py startapp account
                (venv) (base) adamhuan@Leviathan django_daily_media %


                添加到全局配置「settings.py」中:

                  INSTALLED_APPS = [
                  "django.contrib.admin",
                  "django.contrib.auth",
                  "django.contrib.contenttypes",
                  "django.contrib.sessions",
                  "django.contrib.messages",
                  "django.contrib.staticfiles",


                  # Django Filter
                  "django_filters",


                  # Django Rest Framework
                  "rest_framework",


                  # Django Djoser
                  "djoser",


                  # 电影
                  "movie",


                  # 用户
                  "account",
                  ]


                  在应用「account」中配置视图「views.py」:

                    from django.shortcuts import render
                    from django.http import HttpResponse


                    import requests


                    # Create your views here.


                    def account_activate(request, uid, token):


                    res = requests.post(
                    url="http://127.0.0.1:8000/auth/users/activation/",
                    data={
                    "uid": uid,
                    "token": token,
                    }
                    )


                    if res.status_code == 204:
                    return HttpResponse("账户「成功激活」")


                    return HttpResponse("请确认「激活链接」的正确性")


                    然后,在全局路由中配置「active」的路由记录:

                      """
                      URL configuration for django_daily_media project.


                      The `urlpatterns` list routes URLs to views. For more information please see:
                      https://docs.djangoproject.com/en/5.0/topics/http/urls/
                      Examples:
                      Function views
                      1. Add an import: from my_app import views
                      2. Add a URL to urlpatterns: path('', views.home, name='home')
                      Class-based views
                      1. Add an import: from other_app.views import Home
                      2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
                      Including another URLconf
                      1. Import the include() function: from django.urls import include, path
                      2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
                      """
                      from django.contrib import admin
                      from django.urls import path, include


                      from rest_framework.routers import DefaultRouter


                      from movie.views import *
                      from account.views import *


                      router = DefaultRouter()
                      router.register(
                      'movie', MovieViewSet
                      )
                      router.register(
                      'category', CategoryViewSet
                      )


                      urlpatterns = [


                      # Django Admin
                      path("admin/", admin.site.urls),


                      # Django Rest Framework
                      path("api/", include(router.urls)),


                      # Django Djoser
                      path("auth/", include("djoser.urls")),


                      # Account: Activation
                      path("activate/<str:uid>/<str:token>/", account_activate),


                      ]



                      这时候,当你再次注册了一个新用户之后,点击邮件中的激活链接的时候,就不会再是「404」了:

                      只有当第一次激活的时候,才会成功。

                      当你第二次访问已经激活过的链接的时候,你的响应代码就不再是「204」,因此将会显示错误信息:

                      ————————————————

                      HTTP状态码「204」的意思:


                      HTTP状态
                      204 (No Content)
                      表示服务器已成功完成请求,并且在响应有效负载正文中没有要发送的内容。


                      服务器可能希望以
                      entity-headers的形式返回更新的元信息,如果存在,应该将其应用于当前文档的活动视图(如果有的话)。


                      204响应绝不能包含消息体,因此总是在头字段之后的第一个空行终止。

                      默认情况下,204 (No Content)
                      响应是可缓存的。如果需要覆盖缓存,则响应必须包括缓存各自的缓存头。

                      例如,您可能希望204 (No Content)
                      在UPDATE操作中返回状态,其中请求有效负载足够大而不能来回传输。用户代理将有效负载发送到服务器以更新资源,如果操作成功,服务器将响应204
                      以指示成功,以便客户端应用程序可以更新其UI以通知用户操作的成功。

                      它还经常与期望自动数据传输普遍存在的接口一起使用,例如在分布式版本控制系统中。


                      08

                      用自定义的「序列化器」替代Djoser默认的「序列化器」


                      全局配置文件「settings.py」

                        # Djoser
                        DJOSER = {
                        "USER_ID_FIELD": "username",
                        "LOGIN_FIELD": "email",
                        'ACTIVATION_URL': 'activate/{uid}/{token}',
                        'SEND_ACTIVATION_EMAIL': True,
                        'SEND_CONFIRMATION_EMAIL': True,
                        'SERIALIZERS': {
                        'user_create': 'account.serializers.CustomUserCreateSerializer',
                        },
                        }

                        然后,在应用「account」中创建「serializers.py」

                          #!/usr/bin/env python
                          # -*- coding: UTF-8 -*-


                          # ——————————————————
                          # 脚本说明:
                          # xxxxxxxxx
                          # ——————————————————


                          # ========================================
                          # 开始
                          # ))))))))) 模块包导入
                          from rest_framework import serializers
                          from rest_framework.validators import UniqueValidator


                          from djoser.serializers import UserCreateSerializer


                          from django.contrib.auth.models import User


                          # ))))))))) 类名


                          class CustomUserCreateSerializer(UserCreateSerializer):
                          email = serializers.EmailField(
                          validators=[UniqueValidator(queryset=User.objects.all()),]
                          )


                          class Meta:
                          model = User
                          fields = ('id', 'username', 'email', 'password')


                          # ))))))))) 执行阶段


                          # ))))))))) 结束


                          # ========================================
                          # 结束


                          如上,在自定义的序列化器中,添加了对邮箱唯一性的校验。


                          在Postman中看看效果:

                          可以看到,邮箱的唯一性校验已经生效了。


                          ————————————

                          在上面,错误的信息是「该字段必须唯一」,这个错误信息其实是很模糊的;

                          关于错误信息的输出,我们一样可以自定义。


                          还是修改文件「serializers.py」

                            #!/usr/bin/env python
                            # -*- coding: UTF-8 -*-


                            # ——————————————————
                            # 脚本说明:
                            # xxxxxxxxx
                            # ——————————————————


                            # ========================================
                            # 开始
                            # ))))))))) 模块包导入
                            from rest_framework import serializers
                            from rest_framework.validators import UniqueValidator


                            from djoser.serializers import UserCreateSerializer


                            from django.contrib.auth.models import User


                            # ))))))))) 类名


                            class CustomUniqueValidator(UniqueValidator):
                            def __call__(self, value, serializer_field):
                            self.message = "邮箱「%s」已存在" % value
                            return super().__call__(value=value, serializer_field=serializer_field)


                            class CustomUserCreateSerializer(UserCreateSerializer):
                            email = serializers.EmailField(
                            validators=[CustomUniqueValidator(queryset=User.objects.all()),]
                            )


                            class Meta:
                            model = User
                            fields = ('id', 'username', 'email', 'password')


                            # ))))))))) 执行阶段


                            # ))))))))) 结束


                            # ========================================
                            # 结束



                            再看看Postman中的效果:


                            通过上面的步骤,我们实现了后端注册接口的开发,但真实的用户使用的时候,不会通过Postman使用接口的方式去注册网站。


                            因此,下面我们将设计一个注册页面,并对接到前面开发的这个接口。


                            09

                            Vue.JS


                            首先,配置路由「src/router/index.js」

                              import { createRouter, createWebHistory } from 'vue-router'


                              import HomeView from '../views/HomeView.vue'
                              import MovieDetail from "@/views/MovieDetail.vue"
                              import RegisterView from "@/views/RegisterView.vue"


                              const router = createRouter({
                              history: createWebHistory(import.meta.env.BASE_URL),
                              routes: [
                              {
                              path: '/',
                              name: 'home',
                              component: HomeView
                              },
                              {
                              path: '/register',
                              name: "register",
                              component: RegisterView
                              },
                              {
                              path: "/movie/:id",
                              name: "MovieDetail",
                              component: MovieDetail
                              },
                              ]
                              })


                              export default router


                              可以看到,添加了一个「register」的路由。


                              实现Vue的视图「src/views/RegisterView.vue」

                                <template>
                                <div id="main-body" class="bg-primary-100 flex justify-center h-screen">
                                <section class="container flex justify-center items-center rounded ">
                                <div class="hidden md:block">
                                <img src="../assets/images/bg.png" alt="" class="rounded-l max-h-[500px]">
                                </div>
                                <div class="rounded-r w-80 h-[500px] my-8 px-2 py-4 bg-white shadow shadow-gray-300">
                                <div class="text-center text-lg py-6">用户注册</div>
                                <form id="register_form" class="px-4">
                                <div class="flex justify-left items-center relative h-10 px-4 my-2 rounded border-solid border border-gray-400">
                                <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-gray-300 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
                                <path stroke-linecap="round" stroke-linejoin="round" d="M5.121 17.804A13.937 13.937 0 0112 16c2.5 0 4.847.655 6.879 1.804M15 10a3 3 0 11-6 0 3 3 0 016 0zm6 2a9 9 0 11-18 0 9 9 0 0118 0z" >
                                </svg>
                                <input v-model="username" type="text" placeholder="请输入用户名" class="outline-0 text-sm">
                                </div>
                                <div class="flex justify-left items-center relative h-10 px-4 my-2 rounded border-solid border border-gray-400">
                                <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-gray-300 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
                                <path stroke-linecap="round" stroke-linejoin="round" d="m19 2-8.4 7.05a1 1 0 0 1-1.2 0L1 2m18 0a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1m18 0v12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2" >
                                </svg>
                                <input v-model="email" type="text" placeholder="请输入邮箱" class="outline-0 text-sm">
                                </div>
                                <div class="flex justify-left items-center relative h-10 px-4 my-2 rounded border-solid border border-gray-400">


                                <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-gray-300 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
                                <path stroke-linecap="round" stroke-linejoin="round" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" >
                                </svg>
                                <input v-model="password" type="password" placeholder="请输入密码" class="outline-0 text-sm">
                                </div>
                                <div class="flex justify-left items-center relative h-10 px-4 my-2 rounded border-solid border border-gray-400">
                                <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-gray-300 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
                                <path stroke-linecap="round" stroke-linejoin="round" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" >
                                </svg>
                                <input v-model="password2" type="password" placeholder="确认密码" class="outline-0 text-sm">
                                </div>
                                <div class="flex items-center">
                                <div class="flex items-center relative h-10 w-32 px-4 my-2 rounded border-solid border border-gray-400">
                                <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-gray-300 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
                                <path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" >
                                </svg>
                                <input type="text" id="vcode" name="vcode" placeholder="请输入验证码" class="outline-0 text-sm w-24">
                                </div>
                                <img id="signInVcodeImg" src="" onclick="refreshVcode()" >
                                </div>
                                <div class="pt-6">
                                <button v-on:click.prevent="submitForm" id="login" class="rounded bg-green-500 w-full h-8 text-white">注册</button>
                                </div>
                                <div class="text-center text-sm my-2">
                                已有账号
                                <a class="text-blue-500" href="/login">去登录</a>
                                </div>
                                </form>
                                </div>
                                </section>
                                </div>


                                </template>


                                <script>
                                import axios from 'axios'
                                import showMessage from '@/utils/message.js'


                                export default {
                                name: 'Register',
                                data: function(){
                                return {
                                username: '',
                                password: '',
                                password2: '',
                                email: '',
                                }
                                },
                                methods: {
                                submitForm() {
                                if (this.username === '') {
                                showMessage('用户名不能为空')
                                return
                                }


                                if (this.email === '') {
                                showMessage('邮箱不能为空')
                                return
                                }


                                if (this.password === '') {
                                showMessage('密码不能为空')
                                return
                                }


                                if (this.password !== this.password2) {
                                showMessage('2次密码不一致')
                                return
                                }
                                // 没有错误,提交表单


                                const formData = {
                                username: this.username,
                                email: this.email,
                                password: this.password
                                }
                                // const that = this;
                                axios
                                .post('/auth/users/', formData)
                                .then(response => {
                                // 登录成功后回到登录页面
                                showMessage('注册完成,请到邮箱中激活账号')
                                that.$router.push({name: 'Login'});
                                })
                                .catch( error => {
                                // 遍历所有错误信息
                                const errorData = error.response.data
                                const errorMessages = Object.values(errorData).flat();
                                for (let i = 0; i < errorMessages.length; i++) {
                                showMessage(errorMessages[i])
                                }
                                })
                                }
                                }
                                }
                                </script>


                                其中,引入了一个JS「'@/utils/message.js'

                                  import Toastify from 'toastify-js'
                                  import "toastify-js/src/toastify.css"


                                  export default function showMessage(message, state='error', callback_func) {
                                  let background_color;
                                  if (state == 'error') {
                                  background_color = 'linear-gradient(to right, #ff5f6d, #ffc371)'
                                  } else {
                                  background_color = 'linear-gradient(to right, #00b09b, #96c93d)'
                                  }


                                  Toastify({
                                  text: message,
                                  duration: 2000,
                                  close: true,
                                  gravity: "top", // `top` or `bottom`
                                  position: "center", // `left`, `center` or `right`
                                  stopOnFocus: true, // Prevents dismissing of toast on hover
                                  style: {
                                  background: background_color,
                                  },
                                  callback: function(){
                                  if (!callback_func) return ;
                                  if (callback_func) {
                                  callback_func()
                                  }
                                  },
                                  onClick: function(){
                                  alert('debug onclick')
                                  } // Callback after click
                                  }).showToast();
                                  }

                                  这里,需要用NPM进行Toastify的安装:

                                    npm install toastify-js


                                    页面看看效果:


                                    当你成功注册后,你会看到这样的画面:

                                    然后,去看看注册时提供的邮箱的收件箱:

                                    点击激活链接后:

                                    最后,在数据库中可以看到,已经激活成功了:


                                    10

                                    账户激活


                                    前面的激活是通过Django激活的,界面并不好看;

                                    这里,用前端的VUE实现一下。


                                    为了样式好看,先安装「Ant Design」:

                                      (base) adamhuan@Leviathan django_daily_media_frontend % npm install ant-design-vue


                                      added 29 packages, and audited 312 packages in 5s


                                      66 packages are looking for funding
                                      run `npm fund` for details


                                      found 0 vulnerabilities
                                      (base) adamhuan@Leviathan django_daily_media_frontend %
                                      (base) adamhuan@Leviathan django_daily_media_frontend % npm install unplugin-vue-components -D


                                      added 10 packages, and audited 322 packages in 3s


                                      68 packages are looking for funding
                                      run `npm fund` for details


                                      found 0 vulnerabilities
                                      (base) adamhuan@Leviathan django_daily_media_frontend %


                                      然后,做出相关的配置:

                                      文件「src/main.ts」

                                        // import './assets/main.css'
                                        import '@/assets/css/tailwind.css'
                                        import 'ant-design-vue/dist/reset.css'
                                        // import '@surely-vue/table/dist/index.less';


                                        import { createApp } from 'vue'
                                        import {VueCookies} from "vue-cookies";


                                        import { createPinia } from 'pinia'


                                        import Antd from "ant-design-vue";
                                        // import STable from '@surely-vue/table';


                                        import App from './App.vue'
                                        import router from './router'


                                        const app = createApp(App)


                                        app.use(VueCookies)
                                        app.use(createPinia())
                                        app.use(router)
                                        app.use(Antd)
                                        // app.use(STable)


                                        app.mount('#app')



                                        文件「vite.config.ts」

                                          import { fileURLToPath, URL } from 'node:url'
                                          import path from 'node:path'


                                          import { defineConfig, loadEnv } from 'vite'
                                          import vue from '@vitejs/plugin-vue'
                                          import VueDevTools from 'vite-plugin-vue-devtools'


                                          import Components from 'unplugin-vue-components/vite'
                                          import {AntDesignVueResolver, AntDesignVueResolverOptions} from "unplugin-vue-components/dist/resolvers";


                                          // https://vitejs.dev/config/
                                          export default defineConfig( (config) => {


                                          const { command, mode } = config;
                                          const env = loadEnv(mode, process.cwd(), '')


                                          // 代理记录
                                          const proxies = {
                                          // 第一条代理
                                          [env.VITE_APP_BASE_API]: {
                                          target: env.VITE_APP_SERVICE_API,
                                          changeOrigin: true,
                                          rewrite:(path:any) => path.replace(
                                          new RegExp("^" + env.VITE_APP_BASE_API),
                                          ""
                                          ),
                                          },
                                          // 其他代理
                                          "/auth": {
                                          target: "http://localhost:8000/auth",
                                          changeOrigin: true,
                                          rewrite:(path:any) => path.replace(
                                          new RegExp("^/auth"),
                                          ""
                                          ),
                                          },
                                          }


                                          return {


                                          plugins: [
                                          vue(),
                                          VueDevTools(),


                                          Components({
                                          resolvers: [
                                          AntDesignVueResolver({
                                          importStyle: false,
                                          }),
                                          ],
                                          }),


                                          ],


                                          // css: {}


                                          resolve: {
                                          alias: {
                                          '@': fileURLToPath(new URL('./src', import.meta.url))
                                          }
                                          },


                                          server: {
                                          host: "0.0.0.0",
                                          port: Number(env.VITE_APP_PORT),
                                          open: true,
                                          proxy: proxies,
                                          }


                                          }


                                          })



                                          路由配置「src/router/index.ts」

                                            import { createRouter, createWebHistory } from 'vue-router'


                                            import HomeView from '../views/HomeView.vue'
                                            import MovieDetail from "@/views/MovieDetail.vue"
                                            import RegisterView from "@/views/RegisterView.vue"
                                            import ActivateEmail from '@/views/ActivateEmail.vue'


                                            const router = createRouter({
                                            history: createWebHistory(import.meta.env.BASE_URL),
                                            routes: [
                                            {
                                            path: '/',
                                            name: 'home',
                                            component: HomeView
                                            },
                                            {
                                            path: '/register',
                                            name: "register",
                                            component: RegisterView
                                            },
                                            {
                                            path: "/activate/:uid/:token",
                                            name: "ActivateEmail",
                                            component: ActivateEmail
                                            },
                                            {
                                            path: "/movie/:id",
                                            name: "MovieDetail",
                                            component: MovieDetail
                                            },
                                            ]
                                            })


                                            export default router



                                            文件「src/views/ActivateEmail.vue」

                                              <template>
                                              <div id="container" class="text-white text-sm bg-primary-300 min-h-screen pb-4">
                                              <Header/>


                                              <div class="text-center text-2xl p-8">
                                              激活你的账号
                                              </div>
                                              <div class="flex items-center justify-center">
                                              <a-card title="请点击下方按钮,激活账号" :bordered="false" :headStyle="{ 'text-align': 'center' }" style="width: 300px">
                                              <div class="flex justify-center">
                                              <button v-on:click="activate" class="bg-green-500 text-white px-4 py-2 rounded">激活账号</button>
                                              </div>
                                              </a-card>
                                                  </div>

                                              <Footer/>
                                              </div>
                                              </template>


                                              <script>
                                              import Header from '@/components/Header.vue'
                                              import Footer from '@/components/Footer.vue'


                                              import axios from 'axios'
                                              import showMessage from '@/utils/message.js'
                                              import Cookies from "vue-cookies";


                                              export default {
                                              name: 'ActivateEmail',
                                              components: { Header, Footer},
                                              methods: {
                                              activate() {
                                              // 获取参数
                                              const uid = this.$route.params.uid
                                              const token = this.$route.params.token
                                              const formData = {
                                              'uid': uid,
                                              'token': token
                                              }
                                              // 激活用户接口
                                              axios
                                              .post('/auth/users/activation/', formData,
                                              {
                                              headers:{
                                              'X-CSRFToken': Cookies.get('csrftoken')
                                              }
                                              },
                                              )
                                              .then(response => {
                                              // 登录成功后回到登录页面
                                              showMessage('账号激活成功,可以登录了', "info")
                                              this.$router.push({name: 'Login'});
                                              })
                                              .catch( function(error) {
                                              // 遍历所有错误信息
                                              console.log(error)
                                              const errorData = error.response.data
                                              console.log(errorData)
                                              const errorMessages = Object.values(errorData).flat();
                                              for (let i = 0; i < errorMessages.length; i++) {
                                              showMessage(errorMessages[i])
                                              }
                                              })
                                              }
                                              }
                                              }
                                              </script>


                                              <style scoped>
                                              </style>


                                              然后看看页面效果:

                                              不过需要注意的是,收到的邮件的地址是Django的地址,端口「8000」:

                                              http://localhost:8000/activate/MjA/c73287-daa9b034262553ab783f5e37f9d7db82


                                              如果需要用前端Vue的项目打开,你需要将端口改为前端项目的端口「3000」:

                                              http://localhost:3000/activate/MjA/c73287-daa9b034262553ab783f5e37f9d7db82


                                              11

                                              Djoser模块中的认证方式


                                              官方文档:

                                              https://djoser.readthedocs.io/en/latest/authentication_backends.html


                                              官方提供了两种认证方式:

                                              • Token Based Authentication

                                              • JSON Web Token Authentication


                                              这里使用JWT。


                                              如果你需要使用第三方的社交网络的验证:

                                                pip install -U social-auth-app-django

                                                12

                                                Djoser配置JWT


                                                这是在后端Django中进行配置的。


                                                安装:

                                                  pip install -U djangorestframework_simplejwt


                                                  官方的配置相关的文档:

                                                  https://djoser.readthedocs.io/en/latest/authentication_backends.html#json-web-token-authentication


                                                  全局配置文件「settings.py」

                                                  REST_FRAMEWORK

                                                    REST_FRAMEWORK = {
                                                    # 分页
                                                    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
                                                    # 'PAGE_SIZE': 12,


                                                    # 过滤器
                                                    "DEFAULT_FILTER_BACKENDS": {
                                                    'django_filters.rest_framework.DjangoFilterBackend',
                                                    },


                                                    # 验证
                                                    "DEFAULT_AUTHENTICATION_CLASSES": {
                                                    # 'rest_framework.authentication.BasicAuthentication',
                                                    # 'rest_framework.authentication.TokenAuthentication',
                                                    # 'rest_framework_simplejwt.authentication.JWTAuthentication',
                                                    },
                                                    }

                                                    SIMPLE_JWT

                                                      SIMPLE_JWT = {
                                                      'AUTH_HEADER_TYPES': ('JWT',),
                                                      }


                                                      这个时候,全局配置文件「settings.py」的完整内容:

                                                        """
                                                        Django settings for django_daily_media project.


                                                        Generated by 'django-admin startproject' using Django 5.0.6.


                                                        For more information on this file, see
                                                        https://docs.djangoproject.com/en/5.0/topics/settings/


                                                        For the full list of settings and their values, see
                                                        https://docs.djangoproject.com/en/5.0/ref/settings/
                                                        """


                                                        from pathlib import Path


                                                        # Build paths inside the project like this: BASE_DIR 'subdir'.
                                                        BASE_DIR = Path(__file__).resolve().parent.parent




                                                        # Quick-start development settings - unsuitable for production
                                                        # See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/


                                                        # SECURITY WARNING: keep the secret key used in production secret!
                                                        SECRET_KEY = "django-insecure-9sqc)8l_#!8ntww9j7ehj!-()re%mp*n(!t$k&9)4dl^eegy*p"


                                                        # SECURITY WARNING: don't run with debug turned on in production!
                                                        DEBUG = True


                                                        ALLOWED_HOSTS = [
                                                        '127.0.0.1',
                                                        'localhost',
                                                        ]




                                                        # Application definition


                                                        INSTALLED_APPS = [
                                                        "django.contrib.admin",
                                                        "django.contrib.auth",
                                                        "django.contrib.contenttypes",
                                                        "django.contrib.sessions",
                                                        "django.contrib.messages",
                                                        "django.contrib.staticfiles",


                                                        # Django Filter
                                                        "django_filters",


                                                        # Django Rest Framework
                                                        "rest_framework",


                                                        # Django Djoser
                                                        "djoser",


                                                        # pip install django-cors-headers
                                                        "corsheaders",


                                                        # 电影
                                                        "movie",


                                                        # 用户
                                                        "account",
                                                        ]


                                                        MIDDLEWARE = [
                                                        # Django Default
                                                        "django.middleware.security.SecurityMiddleware",
                                                        "django.contrib.sessions.middleware.SessionMiddleware",


                                                        # pip install django-cors-headers
                                                        "corsheaders.middleware.CorsMiddleware",


                                                        "django.middleware.common.CommonMiddleware",
                                                        "django.middleware.csrf.CsrfViewMiddleware",
                                                        "django.contrib.auth.middleware.AuthenticationMiddleware",
                                                        "django.contrib.messages.middleware.MessageMiddleware",
                                                        "django.middleware.clickjacking.XFrameOptionsMiddleware",


                                                        ]


                                                        ROOT_URLCONF = "django_daily_media.urls"


                                                        TEMPLATES = [
                                                        {
                                                        "BACKEND": "django.template.backends.django.DjangoTemplates",
                                                        "DIRS": [BASE_DIR 'templates'],
                                                        "APP_DIRS": True,
                                                        "OPTIONS": {
                                                        "context_processors": [
                                                        "django.template.context_processors.debug",
                                                        "django.template.context_processors.request",
                                                        "django.contrib.auth.context_processors.auth",
                                                        "django.contrib.messages.context_processors.messages",
                                                        ],
                                                        },
                                                        },
                                                        ]


                                                        WSGI_APPLICATION = "django_daily_media.wsgi.application"




                                                        # Database
                                                        # https://docs.djangoproject.com/en/5.0/ref/settings/#databases


                                                        DATABASES = {
                                                        # SQLite
                                                        # "default": {
                                                        # "ENGINE": "django.db.backends.sqlite3",
                                                        # "NAME": BASE_DIR "db.sqlite3",
                                                        # }


                                                        # MySQL
                                                        'default': {
                                                        "ENGINE": "django.db.backends.mysql",
                                                        "HOST": "127.0.0.1",
                                                        "USER": "root",
                                                        "PASSWORD": "xxxxx",
                                                        "NAME": "xxxxx",
                                                        }
                                                        }




                                                        # Password validation
                                                        # https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators


                                                        AUTH_PASSWORD_VALIDATORS = [
                                                        {
                                                        "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
                                                        },
                                                        {
                                                        "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
                                                        },
                                                        {
                                                        "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
                                                        },
                                                        {
                                                        "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
                                                        },
                                                        ]




                                                        # Internationalization
                                                        # https://docs.djangoproject.com/en/5.0/topics/i18n/


                                                        LANGUAGE_CODE = "zh-hans"


                                                        TIME_ZONE = "Asia/Shanghai"


                                                        USE_I18N = True


                                                        USE_TZ = True




                                                        # Static files (CSS, JavaScript, Images)
                                                        # https://docs.djangoproject.com/en/5.0/howto/static-files/


                                                        STATIC_URL = "static/"


                                                        # Default primary key field type
                                                        # https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field


                                                        DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"


                                                        REST_FRAMEWORK = {
                                                        # 分页
                                                        'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
                                                        # 'PAGE_SIZE': 12,


                                                        # 过滤器
                                                        "DEFAULT_FILTER_BACKENDS": {
                                                        'django_filters.rest_framework.DjangoFilterBackend',
                                                        },


                                                        # 验证
                                                        "DEFAULT_AUTHENTICATION_CLASSES": {
                                                        # 'rest_framework.authentication.BasicAuthentication',
                                                        # 'rest_framework.authentication.TokenAuthentication',
                                                        # 'rest_framework_simplejwt.authentication.JWTAuthentication',
                                                        },
                                                        }


                                                        SIMPLE_JWT = {
                                                        'AUTH_HEADER_TYPES': ('JWT',),
                                                        }


                                                        # 邮箱
                                                        EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
                                                        EMAIL_HOST = "smtp.163.com"
                                                        EMAIL_PORT = 25
                                                        EMAIL_HOST_USER = "leviathan_litan@163.com"
                                                        EMAIL_HOST_PASSWORD = "IEXXWXSVGNOQDTBN"
                                                        DEFAULT_FROM_EMAIL = "leviathan_litan@163.com"
                                                        # EMAIL_USE_TLS = True


                                                        # Djoser
                                                        DJOSER = {
                                                        "USER_ID_FIELD": "username",
                                                        "LOGIN_FIELD": "email",
                                                        'ACTIVATION_URL': 'activate/{uid}/{token}',
                                                        'SEND_ACTIVATION_EMAIL': True,
                                                        'SEND_CONFIRMATION_EMAIL': True,
                                                        'SERIALIZERS': {
                                                        'user_create': 'account.serializers.CustomUserCreateSerializer',
                                                        },
                                                        }


                                                        # 跨域 pip install django-cors-headers
                                                        # CORS_ALLOW_ALL_ORIGINS = True
                                                        # CORS_ALLOW_CREDENTIALS = True


                                                        # CORS_ALLOW_HEADERS = (
                                                        # "accept",
                                                        # "authorization",
                                                        # "content-type",
                                                        # "user-agent",
                                                        # "x-csrftoken",
                                                        # "x-requested-with",
                                                        # )


                                                        # CORS_ALLOW_METHODS = [
                                                        # "DELETE",
                                                        # "GET",
                                                        # "OPTIONS",
                                                        # "PATCH",
                                                        # "POST",
                                                        # "PUT",
                                                        # ]


                                                        # CORS_ALLOWED_ORIGINS = [
                                                        # "https://example.com",
                                                        # "https://sub.example.com",
                                                        # "http://localhost:8000",
                                                        # "http://127.0.0.1:8000",
                                                        # ]
                                                        # CORS_ALLOWED_ORIGIN_REGEXES = [
                                                        # r"^https://\w+\.example\.com$",
                                                        # ]


                                                        CSRF_TRUSTED_ORIGINS = [
                                                        "http://localhost:3000",
                                                        ]


                                                        # 导入「local_settings.py」
                                                        # ——————————————————————————————
                                                        # 「敏感的、本地化的」配置需要在「local_settings.py」中进行设置
                                                        # ——————————————————————————————
                                                        try:
                                                        from .local_settings import *
                                                        except ImportError:
                                                        # 仅仅捕获「导入异常」相关的错误
                                                        pass



                                                        然后,配置全局路由「urls.py」

                                                          urlpatterns = [


                                                          # Django Admin
                                                          path("admin/", admin.site.urls),


                                                          # Django Rest Framework
                                                          path("api/", include(router.urls)),


                                                          # Django Djoser
                                                          path('auth/', include('djoser.urls')),


                                                          # Django Token Based Authentication
                                                          # path('auth/', include('djoser.urls.authtoken')),


                                                          # Django Djoser SWT
                                                              path("jwt/"include("djoser.urls.jwt")),


                                                          # Account: Activation
                                                          path("activate/<str:uid>/<str:token>/", account_activate),


                                                          ]

                                                          这时候,该文件的完整内容:

                                                            """
                                                            URL configuration for django_daily_media project.


                                                            The `urlpatterns` list routes URLs to views. For more information please see:
                                                            https://docs.djangoproject.com/en/5.0/topics/http/urls/
                                                            Examples:
                                                            Function views
                                                            1. Add an import: from my_app import views
                                                            2. Add a URL to urlpatterns: path('', views.home, name='home')
                                                            Class-based views
                                                            1. Add an import: from other_app.views import Home
                                                            2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
                                                            Including another URLconf
                                                            1. Import the include() function: from django.urls import include, path
                                                            2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
                                                            """
                                                            from django.contrib import admin
                                                            from django.urls import path, include


                                                            from rest_framework.routers import DefaultRouter


                                                            from movie.views import *
                                                            from account.views import *


                                                            router = DefaultRouter()
                                                            router.register(
                                                            'movie', MovieViewSet
                                                            )
                                                            router.register(
                                                            'category', CategoryViewSet
                                                            )


                                                            urlpatterns = [


                                                            # Django Admin
                                                            path("admin/", admin.site.urls),


                                                            # Django Rest Framework
                                                            path("api/", include(router.urls)),


                                                            # Django Djoser
                                                            path('auth/', include('djoser.urls')),


                                                            # Django Token Based Authentication
                                                            # path('auth/', include('djoser.urls.authtoken')),


                                                            # Django Djoser SWT
                                                            path("jwt/", include("djoser.urls.jwt")),


                                                            # Account: Activation
                                                            path("activate/<str:uid>/<str:token>/", account_activate),


                                                            ]



                                                            这样,JWT的配置就完成了。


                                                            与JWT相关的端口,官方文档:

                                                            https://djoser.readthedocs.io/en/latest/jwt_endpoints.html

                                                            JWT接口:

                                                            登录
                                                            JWT Create
                                                            刷新
                                                            JWT Refresh
                                                            验证
                                                            JWT Verify


                                                            JWT Create:

                                                            先用Postman试试:

                                                            http://127.0.0.1:8000/jwt/jwt/create

                                                            可以看到,当请求成功后,就会返回两个令牌:

                                                            • refresh:用于刷新令牌的

                                                            • access:令牌


                                                            这样后端的接口就配置好了。


                                                            13

                                                            Vue前端「登录界面」


                                                            安装「vuex@next」

                                                              (base) adamhuan@Leviathan django_daily_media_frontend % npm install vuex@next


                                                              added 1 package, and audited 344 packages in 2s


                                                              69 packages are looking for funding
                                                              run `npm fund` for details


                                                              found 0 vulnerabilities
                                                              (base) adamhuan@Leviathan django_daily_media_frontend %

                                                              文件「src/main.ts」

                                                                // import './assets/main.css'
                                                                import '@/assets/css/tailwind.css'
                                                                import 'ant-design-vue/dist/reset.css'
                                                                // import '@surely-vue/table/dist/index.less';


                                                                import { createApp } from 'vue'
                                                                import {VueCookies} from "vue-cookies";


                                                                import { createPinia } from 'pinia'


                                                                import Antd from "ant-design-vue";
                                                                // import STable from '@surely-vue/table';


                                                                import App from './App.vue'
                                                                import router from './router'


                                                                import store from './stores'


                                                                const app = createApp(App)


                                                                app.use(VueCookies)
                                                                app.use(createPinia())
                                                                app.use(router)
                                                                app.use(Antd)
                                                                app.use(store)
                                                                // app.use(STable)


                                                                app.mount('#app')


                                                                文件「src/stores/index.ts」

                                                                  import { createStore } from 'vuex'


                                                                  export default createStore({
                                                                  state: {
                                                                  isLogin: false,
                                                                  token: ''
                                                                  },
                                                                  getters: {
                                                                  },
                                                                  mutations: {
                                                                  initializeStore(state) {
                                                                  if (localStorage.getItem('token')) {
                                                                  state.isLogin = true
                                                                  state.token = localStorage.getItem('token')
                                                                  }
                                                                  else {
                                                                  state.isLogin = false
                                                                  state.token = ''
                                                                  }
                                                                  },
                                                                  setLoginStatus(state, status) {
                                                                  state.isLogin = status
                                                                  },
                                                                  setToken(state, token) {
                                                                  state.token = token
                                                                  },
                                                                  removeToken(state, token) {
                                                                  state.token = ''
                                                                  },


                                                                  },
                                                                  actions: {
                                                                  },
                                                                  modules: {
                                                                  }
                                                                  })



                                                                  ————————————

                                                                  由于前面新增了一个接口,因此Vue的「vite.config.ts」中需要添加代理:

                                                                    import { fileURLToPath, URL } from 'node:url'
                                                                    import path from 'node:path'


                                                                    import { defineConfig, loadEnv } from 'vite'
                                                                    import vue from '@vitejs/plugin-vue'
                                                                    import VueDevTools from 'vite-plugin-vue-devtools'


                                                                    import Components from 'unplugin-vue-components/vite'
                                                                    import {AntDesignVueResolver, AntDesignVueResolverOptions} from "unplugin-vue-components/dist/resolvers";


                                                                    // https://vitejs.dev/config/
                                                                    export default defineConfig( (config) => {


                                                                    const { command, mode } = config;
                                                                    const env = loadEnv(mode, process.cwd(), '')


                                                                    // 代理记录
                                                                    const proxies = {
                                                                    // 第一条代理
                                                                    [env.VITE_APP_BASE_API]: {
                                                                    target: env.VITE_APP_SERVICE_API,
                                                                    changeOrigin: true,
                                                                    rewrite:(path:any) => path.replace(
                                                                    new RegExp("^" + env.VITE_APP_BASE_API),
                                                                    ""
                                                                    ),
                                                                    },
                                                                    // 其他代理
                                                                    "/auth": {
                                                                    target: "http://localhost:8000/auth",
                                                                    changeOrigin: true,
                                                                    rewrite:(path:any) => path.replace(
                                                                    new RegExp("^/auth"),
                                                                    ""
                                                                    ),
                                                                    },
                                                                    // 其他代理
                                                                    "/jwt": {
                                                                    target: "http://localhost:8000/jwt",
                                                                    changeOrigin: true,
                                                                    rewrite:(path:any) => path.replace(
                                                                    new RegExp("^/jwt"),
                                                                    ""
                                                                    ),
                                                                    },
                                                                    }


                                                                    return {


                                                                    plugins: [
                                                                    vue(),
                                                                    VueDevTools(),


                                                                    Components({
                                                                    resolvers: [
                                                                    AntDesignVueResolver({
                                                                    importStyle: false,
                                                                    }),
                                                                    ],
                                                                    }),


                                                                    ],


                                                                    // css: {}


                                                                    resolve: {
                                                                    alias: {
                                                                    '@': fileURLToPath(new URL('./src', import.meta.url))
                                                                    }
                                                                    },


                                                                    server: {
                                                                    host: "0.0.0.0",
                                                                    port: Number(env.VITE_APP_PORT),
                                                                    open: true,
                                                                    proxy: proxies,
                                                                    }


                                                                    }


                                                                    })



                                                                    添加路由「src/router/index.ts」

                                                                      import { createRouter, createWebHistory } from 'vue-router'


                                                                      import HomeView from '../views/HomeView.vue'
                                                                      import MovieDetail from "@/views/MovieDetail.vue"
                                                                      import RegisterView from "@/views/RegisterView.vue"
                                                                      import ActivateEmail from '@/views/ActivateEmail.vue'
                                                                      import Login from "@/views/LoginView.vue"


                                                                      const router = createRouter({
                                                                      history: createWebHistory(import.meta.env.BASE_URL),
                                                                      routes: [
                                                                      {
                                                                      path: '/',
                                                                      name: 'home',
                                                                      component: HomeView
                                                                      },
                                                                      {
                                                                      path: '/register',
                                                                      name: "register",
                                                                      component: RegisterView
                                                                      },
                                                                      {
                                                                      path: "/login",
                                                                      name: "login",
                                                                      component: Login
                                                                      },
                                                                      {
                                                                      path: "/activate/:uid/:token",
                                                                      name: "ActivateEmail",
                                                                      component: ActivateEmail
                                                                      },
                                                                      {
                                                                      path: "/movie/:id",
                                                                      name: "MovieDetail",
                                                                      component: MovieDetail
                                                                      },
                                                                      ]
                                                                      })


                                                                      export default router



                                                                      文件「src/views/LoginView.vue」

                                                                        <template>
                                                                        <div id="main-body" class="bg-primary-100 flex justify-center h-screen">
                                                                        <section class="container flex justify-center items-center rounded ">
                                                                        <div class="hidden md:block">
                                                                        <img src="../assets/images/bg.png" alt="" class="rounded-l max-h-[500px]">
                                                                        </div>
                                                                        <div class="rounded-r w-80 h-[500px] my-8 px-2 py-4 bg-white shadow shadow-gray-300">
                                                                        <div class="text-center text-lg py-6">用户登录</div>
                                                                        <form id="register_form" class="px-4">
                                                                        <div class="flex justify-left items-center relative h-10 px-4 my-2 rounded border-solid border border-gray-400">
                                                                        <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-gray-300 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
                                                                        <path stroke-linecap="round" stroke-linejoin="round" d="M5.121 17.804A13.937 13.937 0 0112 16c2.5 0 4.847.655 6.879 1.804M15 10a3 3 0 11-6 0 3 3 0 016 0zm6 2a9 9 0 11-18 0 9 9 0 0118 0z" >
                                                                        </svg>
                                                                        <input v-model="username" type="text" placeholder="请输入用户名" class="outline-0 text-sm">
                                                                        </div>
                                                                        <div class="flex justify-left items-center relative h-10 px-4 my-2 rounded border-solid border border-gray-400">
                                                                        <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-gray-300 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
                                                                        <path stroke-linecap="round" stroke-linejoin="round" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" >
                                                                        </svg>
                                                                        <input v-model="password" type="password" placeholder="请输入密码" class="outline-0 text-sm">
                                                                        </div>
                                                                        <div class="pt-6">
                                                                        <button v-on:click.prevent="submitForm" id="login" class="rounded bg-green-500 w-full h-8 text-white">登录</button>
                                                                        </div>
                                                                        <div class="text-center text-sm my-2">
                                                                        <span class="text-left text-green-500">
                                                                        <router-link to="/reset_password">忘记密码?</router-link>
                                                                        </span>
                                                                        没有账号
                                                                        <a class="text-blue-500" href="/register">去注册</a>
                                                                        </div>
                                                                        </form>
                                                                        </div>
                                                                        </section>
                                                                        </div>


                                                                        </template>


                                                                        <script>
                                                                        import showMessage from "@/utils/message.js";
                                                                        import axios from "axios";


                                                                        export default {
                                                                        name: "Login",
                                                                        data: function () {
                                                                        return {
                                                                        username: "",
                                                                        password: "",
                                                                        };
                                                                        },
                                                                        methods: {
                                                                        submitForm(){
                                                                        const username = this.username;
                                                                        const password = this.password;
                                                                        if (username === '') {
                                                                        showMessage('用户名不能为空')
                                                                        return
                                                                        }
                                                                        if (password === '') {
                                                                        showMessage('密码不能为空')
                                                                        return
                                                                        }
                                                                        // 先清空
                                                                        axios.defaults.headers.common["Authorization"] = ""
                                                                        localStorage.removeItem("token")


                                                                        const formData = {
                                                                        username: username,
                                                                        password: password,
                                                                        }
                                                                        axios
                                                                        .post('/jwt/jwt/create/', formData)
                                                                        .then(response => {
                                                                        const token = response.data.access;
                                                                        const refreshToken = response.data.refresh;
                                                                        const username = this.username;
                                                                        localStorage.setItem("token", token)
                                                                        localStorage.setItem("refreshToken", refreshToken)
                                                                        localStorage.setItem("username", username)
                                                                        localStorage.setItem("expiredTime", Date.now() + 5 * 60 * 1000 )
                                                                        this.$store.commit('setToken', token)
                                                                        showMessage('登录成功','info',()=>{
                                                                        this.$router.push({
                                                                        name: 'home'
                                                                        })
                                                                        })
                                                                        })
                                                                        .catch(error => {
                                                                        console.log(error);
                                                                        const errorData = error.response.data;
                                                                        const errorMessage = Object.values(errorData).flat();
                                                                        for (let i = 0; i < errorMessage.length; i++) {
                                                                        showMessage(errorMessage[i]);
                                                                        }
                                                                        })
                                                                        }
                                                                        }


                                                                        }
                                                                        </script>


                                                                        文件「src/components/Header.vue」

                                                                          <template>
                                                                          <div id="header" class="h-12 py-1 bg-primary-100 flex items-center justify-center">
                                                                          <div class="w-full px-4" style="max-width:1440px;">
                                                                          <div class="flex justify-between" >
                                                                          <div class="flex items-center">
                                                                          <a href="/">
                                                                          <img src="../assets/images/logo.png" style="height:39px" >
                                                                          </a>
                                                                          <!-- 分类导航 -->
                                                                          <div id="nav" class="px-4">
                                                                          <category/>
                                                                          </div>
                                                                          </div>


                                                                          <!-- 最右侧 -->
                                                                          <div class="flex items-center space-x-2">


                                                                          <!--输入框 -->
                                                                          <div class="relative shrink">
                                                                          <form>
                                                                          <input v-model="keyword" type="text" name="keyword" class="
                                                                          outline-0
                                                                          h-9
                                                                          rounded
                                                                          bg-primary-700
                                                                          border
                                                                          border-gray-600
                                                                          placeholder-gray-400
                                                                          w-64
                                                                          px-2
                                                                          py-1
                                                                          max-w-[180px]
                                                                          " placeholder="请输入关键词" >
                                                                          <div class="
                                                                          absolute
                                                                          top-0
                                                                          right-0
                                                                          flex
                                                                          items-center
                                                                          h-full
                                                                          ">
                                                                          <div class=" rounded text-xs text-gray-400 px-2 mr-2">
                                                                          <button v-on:click.prevent="searchMovies">
                                                                          <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
                                                                          <path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" >
                                                                          </svg>
                                                                          </button>
                                                                          </div>
                                                                          </div>
                                                                          </form>
                                                                          </div>


                                                                          <!-- 最右侧:用户信息 -->
                                                                          <div v-if="$store.state.isLogin" @click="toggleMenu" id="userinfo" class="flex relative hover: cursor-pointer items-center justify-center rounded border border-solid text-white text-lg h-9 text-center">
                                                                          <div id="username" class="px-2">{{ username }}</div>
                                                                          <div class="pr-2">
                                                                          <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
                                                                          <path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" >
                                                                          </svg>
                                                                          </div>
                                                                          <div :class="{hidden: !showMenu}" id="userinfo_menu" class="absolute top-[40px] w-32 transition ease-in-out delay-150 z-50">
                                                                          <ul class="bg-primary-700 py-2 px-4 text-sm">
                                                                          <li class="plx-2 py-2">
                                                                          <a href="/personal">个人中心</a>
                                                                          </li>
                                                                          <li class="plx-2 py-2">
                                                                          <a href="/collect">我的收藏</a>
                                                                          </li>
                                                                          <li class="plx-2 py-2">
                                                                          <router-link to="/" v-on:click.prevent="logout()">退出</router-link>
                                                                          </li>
                                                                          </ul>
                                                                          </div>
                                                                          </div>


                                                                          <!-- 如果没有登录 -->
                                                                          <div v-else class="text-white flex-shrink-0 pr-2">
                                                                          <a href="/login/">登录</a>
                                                                          <a href="/register/">注册</a>
                                                                          </div>


                                                                          </div>
                                                                          </div>
                                                                          </div>
                                                                          </div>
                                                                          </template>


                                                                          <script>
                                                                          import Category from "@/components/Category.vue";


                                                                          export default {
                                                                          name: "Header",
                                                                          components: {
                                                                          Category,
                                                                          },
                                                                          data: function() {
                                                                          return {
                                                                          keyword: '',
                                                                          username: '',
                                                                          showMenu: false,
                                                                          }
                                                                          },
                                                                          mounted() {
                                                                          this.username = localStorage.getItem('username')
                                                                          // 判断登录状态
                                                                          const currentTime = Date.now()
                                                                          const expiredTime = localStorage.getItem('expiredTime')
                                                                          const refreshToken = localStorage.getItem('refreshToken')
                                                                          console.log(currentTime)
                                                                          console.log(expiredTime)


                                                                          if (expiredTime > currentTime) {
                                                                          this.$store.commit('setLoginStatus', true)
                                                                          }
                                                                          else if (refreshToken) {
                                                                          axios
                                                                          .post('/api/jwt/refresh/', {refresh: refreshToken})
                                                                          .then(response => {
                                                                          console.log('执行refresh')
                                                                          const token = response.data.access
                                                                          localStorage.setItem("token", token)
                                                                          // 更新过期时间
                                                                          const expiredTime = Date.now() + 5 * 60 * 1000
                                                                          localStorage.setItem('expiredTime', expiredTime)
                                                                          this.$store.commit('setLoginStatus', true)
                                                                          this.$store.commit('setToken', token)
                                                                          })
                                                                          .catch( error => {
                                                                          console.log(error)
                                                                          this.$store.commit('setLoginStatus', false)
                                                                          this.$store.commit('removeToken')
                                                                          localStorage.clear()
                                                                          })


                                                                          }
                                                                          else {
                                                                          this.$store.commit('setLoginStatus', false)
                                                                          this.$store.commit('removeToken')
                                                                          localStorage.clear()
                                                                          }
                                                                          },
                                                                          methods: {
                                                                          searchMovies() {
                                                                          const keyword = this.keyword.trim()
                                                                          this.$router.push({
                                                                          name: 'home',
                                                                          query: {
                                                                          search: keyword
                                                                          }
                                                                          })
                                                                          },
                                                                          // 切换展示和隐藏
                                                                          toggleMenu() {
                                                                          this.showMenu = !this.showMenu
                                                                          },
                                                                          // 退出
                                                                          logout() {
                                                                          localStorage.clear() // 情况本地数据
                                                                          this.$store.commit('setLoginStatus', false)
                                                                          this.$store.commit('setToken')
                                                                          }
                                                                          },
                                                                          }
                                                                          </script>


                                                                          <style scoped>
                                                                          </style>


                                                                          来看看页面效果:

                                                                          来看看浏览器缓存的数据:

                                                                          最后,成功登录后,会通过「message.js」的回调函数转到「home」主页:

                                                                          可以看到,右上角有登陆成功的账号状态。





                                                                          END




                                                                          温馨提示



                                                                          如果你喜欢本文,请分享到朋友圈,想要获得更多信息,请关注我。


                                                                          文章转载自Nephilim,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                                                                          评论