使用Djangochannels/Vue3&websocket实现一个即时聊天室—Part 1 后端用户功能的实现

2023年 8月 22日 168.3k 0

前言

最近在学习如何在Django中如何实现即时通信。正好Django提供了一个库叫channels,它可以实现我的需求,进而尝试了一下。这次给大家带来的demo是基于前后端分离的开发模式,通过Django-rest-framework构建后端接口,前端使用Vue3构建页面,将前后端连接好之后,使用channels+websocket实现即时通信。

实现效果

chat_room.gif

这次的教程分为四个部分:

  • 后端实现登录与注册
  • 前端页面设计和前后端交互
  • 构建聊天室接口及channels的使用
  • 聊天页面的构建以及设置websocket

本次教程是整个课程的第一部分,内容是构建使用Django-rest-framework编写的后端接口。

本次教程的待办事项如下:

  • 初始化项目
  • 初始化子app--用户
  • 创建自定义用户模型
  • 实现注册功能
  • 实现登录功能
  • 实现登出功能

初始化项目

设置开发环境

编写新项目之前,我建议开发者在后端文件夹内设置一个虚拟环境,避免项目之间的包版本冲突:

在后端文件夹server下运行以下命令:

// env是虚拟环境的名称,可自定义
python -m venv env
// 激活虚拟环境
envScriptsactivate
// 在虚拟环境下安装django
pip install django

注:如果安装较慢,可换用国内镜像下载,如:

pip install -i mirrors.aliyun.com/pypi/simple… django

创建新项目

环境设置完成后,通过命令创建一个新项目

django-admin startproject chat_api .

注:. 的作用是让django在当前目录下创建该项目,让manage.py处于当前目录

此时的目录结构:

现在开发前的准备基本完成,让我们尝试开启项目:

python manage.py runserver

您应该在终端中看到它在http://127.0.0.1:8000/上启动。现在您可以访问此链接并检查它是否有效。您应该在浏览器中看到以下窗口:

至此新项目的初始化已经完成。

初始化子app--用户

创建app

接下来,请在终端/命令行中运行以下命令来创建将驻留在我们本地库项目中的主应用程序。请务必在项目manage.py文件所在的同一文件夹中运行此命令:

python manage.py startapp accounts

您应该看到一个名为accounts的新目录已创建。现在,项目结构的目录大致如下:

📦 server
 ┣ 📂chat_api
 ┃ ┣ 📜asgi.py
 ┃ ┣ 📜settings.py
 ┃ ┣ 📜urls.py
 ┃ ┗ 📜wsgi.py
 ┃ 📦accounts
 ┃ ┣ 📂migrations
 ┃ ┃ ┗ 📜__init__.py
 ┃ ┣ 📜admin.py
 ┃ ┣ 📜apps.py
 ┃ ┣ 📜models.py
 ┃ ┣ 📜tests.py
 ┃ ┣ 📜views.py
 ┃ ┗ 📜__init__.py
 ┣ 📂env
 ┣ 📜.gitignore
 ┗ 📜manage.py

该命令创建一个新文件夹,并在其中设置不同程序部分的文件(如示例所示)。大多数文件根据其用途进行命名(例如,视图应存储在 view.py中,模型应存储在 model.py中,测试应存储在 test.py中,管理站点配置应存储在 admin.py中,应用程序注册应存储在 apps.py中),并且具有一些开始处理相关对象的标准代码。

此外,我们现在有一个migrations的迁移文件夹,这些文件允许您在自动修改模型时更新数据库。我们在 init.py创建了一个空文件来将 Django / Python 识别为 Python 包,并允许其对象在项目的其他部分中使用。(需要执行迁移命令后生成)

settings.py中注册

在 Django 中,所有应用程序都是通过将它们添加到项目设置中的 INSTALLED_APPS 列表中来注册的。当应用程序创建或准备使用时,我们必须在那里注册它。

打开项目设置文件 ,server/chat_api/settings.py并找到 INSTALLED_APPS 列表定义。然后,在列表末尾添加一个新行,如下所示:

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    # 添加新app
    "accounts",
]

指定数据库

这也是您需要指定项目数据库的地方。建议尽可能使用相同的数据库进行开发。

我们将在这个项目中使用 SQLite 数据库,因为我们不希望在开发数据库上有大量并发访问,尽管不需要额外的工作来设置它!您可以看到在 settings.py 中配置数据库是多么容易:

# server/chat_api/settings.py
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": BASE_DIR / "db.sqlite3",
    }
}

因为我们使用 SQLite,所以这里不需要其他设置。

其他项目设置

设置时区(在settings.py中)

# server/chat_api/settings.py
TIME_ZONE = "Asia/Shanghai"
# 如设置True则数据库存储的时间为UTC时间   
# False则为TIME_ZONE设置的时间
USE_TZ = False

创建自定义用户模型

编写用于管理用户的UserManager

我们编写的CustomUserManager继承于BaseUserManager,是一个自定义的用户管理器,重写了两个主要方法:

  • create_user:用于创建普通用户。它接收用户的电子邮箱、密码和其他可选字段,并将用户信息保存至数据库。
  • create_superuser:用于创建超级用户。该方法除了包含普通用户的字段外,还设置了is_staffis_superuser属性为True,然后调用create_user方法创建用户。

代码实现如下:

# server/accounts/models.py
from django.db import models
from django.contrib.auth.base_user import BaseUserManager

class CustomUserManager(BaseUserManager):
    def create_user(self, email, password, **extra_fields):
        email = self.normalize_email(email)

        user = self.model(email=email, **extra_fields)
        user.set_password(password)
        user.save()
        return user

    def create_superuser(self, email, password, **extra_fields):
        extra_fields.setdefault("is_staff", True)
        extra_fields.setdefault("is_superuser", True)

        if extra_fields.get("is_staff") is not True:
            return ValueError("超级用户的is_staff属性必须为True")

        if extra_fields.get("is_superuser") is not True:
            return ValueError("超级用户的is_superuser属性必须为True")

        return self.create_user(email=email, password=password, **extra_fields)

编写User类

现在我们要通过继承AbstractUser类编写自定义用户模型,并将CustomUserManager应用至自定义的User类中

# server/accounts/models.py
from django.contrib.auth.models import AbstractUser
from django.utils import timezone

class User(AbstractUser):
    email = models.CharField(max_length=80, unique=True)
    username = models.CharField(max_length=50)
    description = models.CharField(blank=True, max_length=512)
    phone = models.CharField(blank=True, max_length=20)
    date_joined = models.DateTimeField(default=timezone.now)
    last_login = models.DateTimeField(blank=True, null=True)
	
    # 指定使用CustomUserManager进行用户管理
    objects = CustomUserManager()
    # 将用户验证时使用字段修改为email
    USERNAME_FIELD = "email"
    # 设置在创建用户账号时用户需要提供的字段
    REQUIRED_FIELDS = ["username"]

    # 设置模型在管理界面中的显示名称。
    class Meta:
        verbose_name = "Account"

	def __str__(self):
        return self.username

默认情况下,AbstractUser没有电子邮箱、用户名称和描述等字段。它们将根据您的需求进行定制;稍后,为我们添加更多内容不会有任何问题。默认的 Django 用户模型在身份验证过程中使用用户名来唯一标识用户。要使用电子邮件地址而不是用户名,您必须通过将其拆分为 AbstractUser来创建自定义用户模型。

自定义User的使用

接下来,我们需要注册该app。我们使用以下代码来完成此操作:

# chat_api/accounts/admin.py
from django.contrib import admin
from .models import User

admin.site.register(User)

现在我们应该可以在管理面板中看到我们的注册用户。

在进行数据库迁移前,我们需要在settings.py中注册我们的用户模型,我们在settings.py中添加以下行:

# server/chat_api/settings.py
AUTH_USER_MODEL = "accounts.User"

完成这些以后,我们可以开始迁移数据库了。

python manage.py makemigrations
python manage.py migrate

注意,我们在项目开发的时候,应该先自定义用户模型,否则会遇到以下问题:

由于Django对可交换模型的动态依赖特性的限制,AUTH_USER_MODEL引用的模型必须在其应用程序的第一次迁移中创建(通常称为0001_initial);否则,您将遇到依赖性问题。

在下一步中,我们可以创建管理员用户:

python manage.py createsuperuser 

Email: alienware@app.com
Username: alienware
Password: 
Password (again):
Superuser created successfully.

现在我们完成了管理员的创建,运行以下命令以启动服务器:

python manage.py runserver

接下来,让我们登录管理页面:http://127.0.0.1:8000/admin

使用我们刚刚创建的用户登录,您应该看到如下页面:

您可以在accounts表内查看用户详细信息。

实现用户注册

在这之前,先安装rest-framework

pip install django-restframework

settings.py引用:

# server/chat_api/settings.py
INSTALLED_APPS = [
	...
    "rest_framework",
    "rest_framework.authtoken",
]

构建序列化器(serializer)

首先构建一个serializer负责创建用户。我们创建一个serializers.py用来存放用于实现功能的序列化器:

# server/accounts/serializers.py
from rest_framework import serializers
from .models import User

class SignUpSerializer(serializers.ModelSerializer):
    email = serializers.CharField(max_length=80)
    username = serializers.CharField(max_length=50)
    password = serializers.CharField(min_length=8, write_only=True)

    class Meta:
        model = User
        fields = ["email", "username", "password"]

添加验证方法,用于检查用户是否存在:

# server/accounts/serializers.py
from rest_framework.validators import ValidationError

class ...
	def validate(self, attrs):
        email_exists = User.objects.filter(email=attrs["email"]).exists()

        if email_exists:
            raise ValidationError("该邮箱已被使用!")

        return super().validate(attrs)

重写create方法,使密码加密而不是明文:

# server/accounts/serializers.py
from rest_framework_authtoken import Token

class ...
    def create(self, validated_data):
        password = validated_data.pop("password")
        user = super().create(validated_data)
        # 将用户的密码使用哈希算法处理
        user.set_password(password)
        user.save()
        Token.objects.create(user=user)
        return user

通过移除明文密码然后根据旧密码进行加密的方案对密码进行加密。

这里添加了token,是为了确认登录状态的,具体用法后面会提及,现在先创建并存储下来。

注:使用Token之前可能需要重新迁移数据库(migrations、migrate)

构建接口(views.py)

接下来在views.py编写一个类视图SignUpView,用于实现注册的接口:

# server/accounts/views.py
from rest_framework import generics, status
from rest_framework.response import Response

from .serializers import SignUpSerializer, UserSerializer
from .models import User

class SignUpView(generics.GenericAPIView):
    serializer_class = SignUpSerializer
    def post(self, request):
        data = request.data
        serializer = self.serializer_class(data=data)
        if serializer.is_valid():
            serializer.save()
            response = {"message": "用户创建成功", "data": serializer.data}
            return Response(data=response, status=status.HTTP_201_CREATED)
        return Response(data=serializer.errors,
                        status=status.HTTP_400_BAD_REQUEST)

这段代码通过重写post请求,对请求的数据进行接收以及验证,如果验证通过,我们将会保存这个新建的对象;如果验证失败将返回400错误。

准备路由(urls.py)

为了使路由生效,我们需要一个注册接口的控制器。简单来说,我们需要该接口的URL。让我们在server/accounts路径下创建一个urls.py,并编写以下内容:

# server/accounts/urls.py
from django.urls import path
from . import views
urlpatterns = [
    path("signup/", views.SignUpView(), name="signup"),
]

但这么做还不够,它并不会起作用。我们需要将accountsurls.py文件注册到chat_apiurls.py中,为此,我们应在server/chat_api/urls.py下添加以下内容:

# server/chat_api/urls.py
from django.urls import path, include
urlpatterns = [
    path("admin/", admin.site.urls),
    # 注册accounts应用
    path("auth/", include("accounts.urls")),
]

测试接口

完成以上步骤后,我们可以开始测试接口了。

这里我使用Postman来做接口测试,首先新建请求:

结果:

我们成功了!用户已经创建完成并正确返回结果,我们可以继续接下来的工作了。

接下来我们再进行一次测试,验证是否可以返回错误:

这里有一个小技巧,我们可以自定义错误字段的键。在settings.py中编写以下代码:

# server/chat_api/settings.py
REST_FRAMEWORK = {
    "NON_FIELD_ERRORS_KEY":"errors",
}

其他错误也可以自行设置,这里只举一例说明。

此时的返回结果出现了变化:

在admin中查看新建的用户,此时数据一切正常。(由于篇幅有限,大家自行验证)

实现用户登录

准备工作

rest_framework允许我们自定义身份验证类型,在settings.py添加以下配置:

# server/chat_api/settings.py
REST_FRAMEWORK = {
    "NON_FIELD_ERRORS_KEY":"errors",
    # 添加默认身份验证
    "DEFAULT_AUTHENTICATION_CLASSES": (
        "rest_framework.authentication.SessionAuthentication",
        "rest_framework.authentication.TokenAuthentication",
    ),
    # 为受保护的端点提供权限验证
    "DEFAULT_PERMISSION_CLASSES":(
      "rest_framework.permissions.IsAuthenticated"
    ),
}

构建接口

views.py中添加登录接口:

# server/accounts/views.py
from rest_framework.views import APIView
from rest_framework.authtoken.models import Token
from django.contrib.auth import authenticate

class LoginView(APIView):
    permission_classes = []
    def post(self, request):
        email = request.data.get("email")
        password = request.data.get("password")
        user = authenticate(email=email, password=password)
        current_user = User.objects.filter(email=email)

        if user is not None:
            Token.objects.filter(user=current_user[0]).delete()
            Token.objects.create(user=current_user[0])
            response = {
                "message": "登录成功!",
                "user": {
                    "user_id": user.id,
                    "name": user.username,
                    "email": user.email,
                    "token": user.auth_token.key,
                },
            }
            return Response(data=response, status=status.HTTP_200_OK)
        else:
            return Response(data={"message": "无效的邮箱或密码"})

    def get(self, request):
        content = {"user": str(request.user), "auth": str(request.auth)}
        return Response(data=content, status=status.HTTP_200_OK)

这段代码通过authenticate函数验证用户输入的信息,然后确认当前用户信息后,对用户的令牌进行刷新(通过对Token表的增删实现)。然后返回与当前用户相关的一些信息。

匿名登录(get)会返回一些消息,这个在测试的时候会讲到。

准备路由

urls.py中设置:

# server/accounts/urls.py
urlpatterns = [
        ...
        path("login/", views.LoginView.as_view(), name="login"),
]

测试接口

如果直接使用get方法,服务器会返回一个匿名对象,表示你当前没有权限,也不会有Token等信息返回:

使用用户的账号登录接口post

成功登录!接下来可以继续实现登出用户的操作。

实现用户登出

实现用户登出比较简单,步骤与登录类似,逻辑也比较简单。这里直接给出代码实现:

构建接口

# server/accounts/views.py
from rest_framework.decorators import api_view, permission_classes
@api_view(["POST"])
@permission_classes([])
def Logout(request):
    token = request.data["token"]
    # print(token)
    user_token = Token.objects.get(key=token)
    user_token.delete()
    response = {"message": "用户已成功登出"}
    return Response(data=response, status=status.HTTP_200_OK)

准备路由

# server/accounts/urls.py
urlpatterns = [
    path("signup/", views.SignUpView.as_view(), name="signup"),
    path("login/", views.LoginView.as_view(), name="login"),
    # 登出
    path("logout/", views.Logout, name="logout"),
]

测试接口

注:测试之前在accounts表和token表查看用户相关信息。

至此后端的用户模块已经成功完成。

写在最后

接下来来编写我们的前端部分,实现基础的页面的同时连通前后端。

在这周内完成课程的更新,我在这先提前谢谢大家的观看了!

本章节代码参考:github

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论