用Django和SendGrid建立电子邮件的激活流程

512 阅读15分钟

任何包括认证过程的网络应用都应该有某种电子邮件激活流程。如果你没有,你就减少了与用户联系的可能性,并为垃圾邮件发送者打开了一扇门,让他们创建虚假账户并影响你的网站。

本教程将教你如何使用DjangoSendGrid建立确认邮件来验证用户的注册。

Django有一个内置的邮件发送接口,通过本教程,我们将使用SendGrid来发送邮件。

在我们完成后,你将拥有一个认证系统,要求用户用确认邮件来验证他们的账户。

我们将从头开始创建一个完整的项目,但如果你已经有了一个项目,就不要犹豫了,跟着这个教程走吧。

每当你想快速参考代码时,你可以访问这个 GitHub 仓库**.

前提条件

要完成本教程,你需要以下条件。

创建一个Python虚拟环境

为你开始的每个Django项目建立一个Python虚拟环境被认为是一个好的做法。这允许你管理你的每个项目的依赖性。

如果你跟随的是基于Unix的操作系统,如Linux或macOS,打开终端,运行以下命令,创建一个名为venv 的虚拟环境并将其源化。

python -m venv venv
source venv/bin/activate

如果激活命令不起作用,请查看这个激活脚本表

如果你是一个Windows用户,运行下面的命令。

python -m venv venv
venv\Scripts\activate

现在我们的虚拟环境已经激活,让我们来安装所有我们需要的软件包。

要安装所有这些东西,请运行这个pip命令。

pip install django django-environ django-crispy-forms crispy-bootstrap5

创建一个Django项目

现在,创建一个名为email-verification的文件夹,这将是你的项目所处的目录。之后,在新目录下启动一个名为config的Django项目(被认为是最佳做法)。

mkdir email-verification
cd email-verification/
django-admin startproject config .

这样做之后,你应该有以下的文件结构。

.
── email-verification
│   ├── config
│   │   ├── asgi.py
│   │   ├── __init__.py
│   │   ├── settings.py
│   │   ├── urls.py
│   │   └── wsgi.py
│   └── manage.py
── venv
    ├── bin
    ├── include
    ├── lib
    ├── lib64 -> lib

用以下命令运行服务器。

python manage.py runserver

如果你运行服务器并访问你的本地主机,你应该看到Django的欢迎页面。

Django welcome page

用SendGrid和Django发送电子邮件

我们有一个专门的教程,解释如何通过Django shell用Django和SendGrid发送电子邮件。如果你想获得更详细的说明,请务必查看它。

1.获得一个SendGrid的API密钥

进入SendGrid的API密钥页面,为这个项目获取一个API密钥。

SendGrid API keys page

点击创建API密钥按钮,这将打开以下面板。

Create API key screen

为你的API密钥输入一个名字,赋予它完全访问权限,然后点击创建和查看按钮。

创建后,你会得到一个私人API密钥。复制它并保存在安全的地方,因为我们以后会需要它。

2.创建*.env*文件

你不应该在代码中直接包含像API密钥这样的敏感信息。这就是为什么我们要使用环境变量。

config文件夹内创建一个空的*.env*文件,它将存储你的SendGrid API密钥。你可以通过下面的命令从终端进行操作。

pwd
# /home/daniel/Documents/email-verification/config
touch .env

打开该*.env*文件并设置以下键值对。

SENDGRID_API_KEY=<your-api-key>
FROM_EMAIL=<your-email-address>

用你在第一步得到的私人SendGrid API密钥替换<your-api-key> ,并对<your-email-address> 值做同样的处理。

我们将在django-environ的帮助下在我们的设置文件中使用这两个环境变量。

如果你使用像git这样的版本控制系统(VCS),确保在你的*.gitignore文件中包括.env*。在你的某个提交中包含私人API密钥是一个 可怕的安全错误**。

3.配置config/settings.py以使用SendGrid发送邮件

位于config/settings.py的设置文件存储了我们项目的所有配置,其中当然包括电子邮件发送配置。

打开settings.py文件并导入django-environ包。

# config/settings.py

# After importing Path
import environ

env = environ.Env()
environ.Env.read_env() # Reads the .env file

上面的代码导入了django-environ,并创建了一个env ,该变量包含了存储在*.env*文件中的环境变量。

现在,前往设置文件的末尾,编写以下代码。

# Bottom of settings.py 
# Twilio SendGrid
EMAIL_HOST = 'smtp.sendgrid.net'
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = 'apikey' # Name for all the SenGrid accounts
EMAIL_HOST_PASSWORD = env('SENDGRID_API_KEY')

# The email you'll be sending emails from
DEFAULT_FROM_EMAIL = env('FROM_EMAIL', default='noreply@gmail.com')
LOGIN_REDIRECT_URL = 'success'

在这里,我们使用env 变量来返回*.env*文件中的值。让我们看看我们将要使用的三个关键设置。

  • EMAIL_HOST_PASSWORD:Django将尝试使用密码来连接到EMAIL_HOST (SendGrid),在这种情况下,你的私人API密钥。
  • DEFAULT_FROM_EMAIL:你的收件人将从哪个电子邮件地址收到邮件
  • LOGIN_REDIRECT_URL:用户通过验证邮件激活账户后将被重定向到的URL命名空间。

4.从Django Shell中发送一封测试邮件

最后,让我们测试一下我们是否可以用Django和SendGrid来发送邮件。要做到这一点,打开一个新的终端窗口,确保你的虚拟环境被激活,然后进入Django shell。

(venv) $ python manage.py shell

由于我们是用manage.py ,我们所有的设置都会被导入。现在你在shell中,用下面的代码发送一封电子邮件,将占位符 "to@receiver.org "的地址替换成你能访问的电子邮件地址。

from django.core.mail import send_mail
from django.conf import settings
send_mail('Testing mail', 'A cool message :)', settings.DEFAULT_FROM_EMAIL, ['to@receiver.org'])

我们使用 [send_mail](https://docs.djangoproject.com/en/dev/topics/email/#send-mail)函数来发送这个消息,我们把subject,message,from_email, 和recipient_list 参数传给它。

正如你所看到的,我们使用设置对象来获得DEFAULT_FROM_EMAIL ,并使用它来发送电子邮件。

如果你可以访问你所发送的电子邮件地址,你应该看到类似这样的信息出现在你的收件箱中。

Received test email in inbox

如果你在这一步遇到了错误,请确保你已经在SendGrid控制台验证了你的发件人身份

创建注册和电子邮件激活流程

现在你可以用Django和SendGrid发送电子邮件了,是时候建立电子邮件激活流程了。

启动一个users 应用程序

从你的项目根部,启动一个名为users 的应用,它将让我们为Django的内置认证系统引入自定义功能。

python manage.py startapp users

这个应用并不是要取代Django内置的用户模型,所以我们要用它来创建自定义的令牌表单视图URLS文件。

继续在你的config/settings.py文件中安装该应用。

# config/settings.py
INSTALLED_APPS = [
    # More apps ...
    'django.contrib.staticfiles',

    # Users app
    'users',

]

最后,打开config/urls.py,修改你的项目URL模式,包括users 应用程序的URL配置。

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

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

当然,users.urls 文件还不存在,所以让我们为users 应用程序创建一个空的urlpatterns 列表。在应用程序内创建一个users/urls.py文件,并添加一个空列表。

urlpatterns = []

这将防止Django在下面的步骤中引发错误。

编码一个令牌生成器

我们需要使用一个令牌生成器来为我们的用户创建一次性使用的令牌来激活他们的账户。你可以在lyricstraining等网站上看到这个确切的过程。幸运的是,Django已经有一些这样的功能,所以我们唯一要做的就是对它进行一些定制。

要建立一个一次性令牌生成器,请进入你在上一步骤中创建的users 应用程序,并创建一个名为token.py的文件。然后添加下面的代码。

# users/token.py

from django.contrib.auth.tokens import PasswordResetTokenGenerator

class AccountActivationTokenGenerator(PasswordResetTokenGenerator):
    def _make_hash_value(self, user, timestamp):
        return (
            str(user.is_active) + str(user.pk) + str(timestamp)
        )

token_generator = AccountActivationTokenGenerator()

这段代码可能看起来很复杂,所以让我们看看每段代码的含义。

首先,我们导入PasswordResetTokenGenerator类,该类生成并检查用于密码重置的令牌。我们利用它的make_token()check_token() 方法来创建令牌发生器。

[make_token](https://github.com/django/django/blob/main/django/contrib/auth/tokens.py#L28)()方法会生成一个包含用户相关数据的哈希值,比如他们的ID、密码(哈希值,Django不存储原始密码)、日志时间戳和电子邮件。一旦令牌被使用,这些信息就会改变,这意味着令牌不会再有效。

这个类的默认_make_hash_value() ,返回如下。

    def _make_hash_value(self, user, timestamp):
        login_timestamp = '' if user.last_login is None else user.last_login.replace(microsecond=0, tzinfo=None)
        email_field = user.get_email_field_name()
        email = getattr(user, email_field, '') or ''
        return f'{user.pk}{user.password}{login_timestamp}{timestamp}{email}'

所以,我们只是修改了这个方法,并创建了一个变量token_generator 来实例化它。

为了检查这个新类的运行情况,首先,通过在终端窗口运行以下命令来迁移你的数据库(默认为SQLite)。

python manage.py migrate

在上面的命令中,我们正在为用户创建数据库表,接下来我们将使用这些表。现在,打开Django shell。

python manage.py shell

创建一个名为user1 的新用户,并将其传递给token_generator.make_token()方法。

from django.contrib.auth import get_user_model
from users.token import token_generator
users = get_user_model()
user1 = users.objects.create(username="test", password="acoolpass124")
token_generator.make_token(user1)

如果你运行上面的代码,你应该得到一个像这样的令牌。

'ark0dy-97b7b1bccb1367eac4b81634d5d0bf6a'

现在我们有了为每个用户生成一次性令牌的方法,是时候继续开发我们的Django项目了。

报名表格

我们需要创建一个SignUpForm ,它继承了默认的 [UserCreationForm](https://docs.djangoproject.com/en/dev/topics/auth/default/#django.contrib.auth.forms.UserCreationForm) 并添加一个电子邮件字段,因为我们需要它来向用户发送激活链接。

我们还需要一个自定义的send_activation_email() 方法,顾名思义,就是向用户发送激活链接。

users 应用程序中创建一个文件users/forms.py,并在其中粘贴以下代码。

# users/forms.py
from django import forms
from django.contrib.auth.forms import UserCreationForm

from django.contrib.sites.shortcuts import get_current_site
from django.contrib.auth import get_user_model
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode

from django.template.loader import render_to_string

from .token import token_generator

user_model = get_user_model()

# Sign Up Form
class SignUpForm(UserCreationForm):

    email = forms.EmailField(
        max_length=254, help_text='Enter a valid email address')

    class Meta:
        model = user_model
        fields = [
            'username',
            'email',
            'password1',
            'password2',
        ]

    # We need the user object, so it's an additional parameter
    def send_activation_email(self, request, user):
        current_site = get_current_site(request)
        subject = 'Activate Your Account'
        message = render_to_string(
            'users/activate_account.html',
            {
                'user': user,
                'domain': current_site.domain,
                'uid': urlsafe_base64_encode(force_bytes(user.pk)),
                'token': token_generator.make_token(user),
            }
        )

        user.email_user(subject, message, html_message=message)

这是一个相当大的表格,但让我们把重点放在send_activation_email() 。这个方法接受两个参数:请求和一个User 对象。我们用请求来获取当前网站的域名(在我们的例子中是localhost:8000),用user 来获取它的base64编码的ID和一个一次性的token。

所有这些信息都作为上下文传递给模板*users/activate_account.html,*我们将在后面创建。

最后,我们向用户发送一封电子邮件,并附上相应的主题信息html信息

我们在表单内创建了这个方法,因为视图 应该有尽可能少的逻辑

视图

我们将创建多个视图,所以如果在任何时候你需要一个指南,请查看项目的源代码

这些是我们要建立的视图。

  • SignUpView (注册视图)
  • ActivateView (激活用户的账户)
  • CheckEmailView (指出用户检查他们的电子邮件)
  • SuccessView (显示成功模板)
  • CustomLoginView (定制的日志视图)

打开users 应用程序里面的users/views.py文件,导入以下内容。

# users/views.py
from django.shortcuts import render
from django.views.generic import CreateView, TemplateView, RedirectView
from django.contrib.auth import login
from django.contrib.auth.views import LoginView

from django.urls import reverse_lazy
from django.utils.http import urlsafe_base64_decode
from django.utils.encoding import force_str # force_text on older versions of Django

from .forms import SignUpForm, token_generator, user_model

我们要导入一些Django的通用视图,一些实用工具,当然还有我们之前构建的SignUpFormtoken_generator

现在,创建一个继承自generic的SignUpView[CreateView](https://docs.djangoproject.com/en/dev/ref/class-based-views/generic-editing/#django.views.generic.edit.CreateView),自定义form_valid() 方法,将用户状态设置为非活动,并发送激活邮件。

class SignUpView(CreateView):
    form_class = SignUpForm 
    template_name = 'users/signup.html'
    success_url = reverse_lazy('check_email')

    def form_valid(self, form):
        to_return = super().form_valid(form)
        
        user = form.save()
        user.is_active = False # Turns the user status to inactive
        user.save()

        form.send_activation_email(self.request, user)

        return to_return

考虑到我们使用的是默认的User 模型,我们将这个 [is_active](https://docs.djangoproject.com/en/dev/ref/contrib/auth/#django.contrib.auth.models.User.is_active)字段为false,但如果你有一个自定义的用户模型或与一个配置文件模型有一对一的关系,你可以修改上面的视图,将相应的字段设置为false。

如果你有一个自定义的用户模型,你应该创建一个模型管理器,在创建的时候将所有用户的状态设置为False

然后,写一个继承自通用的激活视图 [RedirectView](https://docs.djangoproject.com/en/3.2/ref/class-based-views/base/#redirectview),因为如果get() 方法运行成功,我们将重定向到success 命名空间,我们将在后面创建。

class ActivateView(RedirectView):

    url = reverse_lazy('success')

    # Custom get method
    def get(self, request, uidb64, token):

        try:
            uid = force_str(urlsafe_base64_decode(uidb64))
            user = user_model.objects.get(pk=uid)
        except (TypeError, ValueError, OverflowError, user_model.DoesNotExist):
            user = None

        if user is not None and token_generator.check_token(user, token):
            user.is_active = True
            user.save()
            login(request, user)
            return super().get(request, uidb64, token)
        else:
            return render(request, 'users/activate_account_invalid.html')

随着Django 4.0新版本的发布,一些功能被删除或重新命名。force_str 函数就是这种情况,在旧版本(Django 3.x)中,它被命名为force_text

最后,写一个简单的CheckEmailViewSuccessView ,它们只不过是 [TemplateViews](https://docs.djangoproject.com/en/dev/ref/class-based-views/base/#templateview):

class CheckEmailView(TemplateView):
    template_name = 'users/check_email.html'

class SuccessView(TemplateView):
    template_name = 'users/success.html'

不要担心模板的问题,因为我们以后会创建模板。

URLs

接下来,我们要配置users 应用程序的URLs。在users/urls.py 中,导入你之前创建的所有视图,并编写各自的URL模式,替换掉前面步骤中的空列表。

from django.urls import path

from .views import (
    SignUpView,
    ActivateView,
    CheckEmailView,
    SuccessView,
)
# https://docs.djangoproject.com/en/4.0/ref/templates/language/#id1
urlpatterns = [
    path('signup/', SignUpView.as_view(), name="signup"),
    path('activate/<uidb64>/<token>/', ActivateView.as_view(), name="activate"),
    path('check-email/', CheckEmailView.as_view(), name="check_email"),
    path('success/', SuccessView.as_view(), name="success"),
]

我们就快完成了,但在测试这个项目的结果之前,我们需要编写所有的模板。

构建模板

在建立了所有的后台功能(用户看不到的)之后,是时候建立模板了,模板是普通的HTML文件,让我们使用模板继承来节省时间,并动态地显示数据。

为此,我们将使用Django模板语言(DTL)和Bootstrap 5。使用Bootstrap 5意味着我们将不使用静态文件。

我们已经安装了Django Crispy Forms,所以唯一剩下的就是在设置文件中安装它。

# config/settings.py
INSTALLED_APPS = [
    ....
    'django.contrib.staticfiles',

    # Forms
    'crispy_forms',
    'crispy_bootstrap5',

    # Users
    'users',
]

CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"

CRISPY_TEMPLATE_PACK = "bootstrap5"

现在,进入你的终端,为users 应用程序创建典型的Django模板双结构

pwd
# /home/daniel/Documents/email-verification/users
mkdir -p templates/users

我们要建立以下模板。

  • activate_account.html (用于发送邮件的模板)
  • base.html (所有其他模板的基础模板)
  • signup.html (注册的模板形式)
  • check_email.html (简单的检查邮件信息)
  • success.html (如果账户激活顺利就显示)

所有下列模板将位于users/templates/users 目录下。

电子邮件激活模板

现在,哪个自动化信息是以纯文本形式发送的?

我们要向用户发送一条带有自定义信息的HTML消息。事实上,我们已经在SignUpForm 中实现了这个功能,所以现在是时候创建相应的模板了。

templates文件夹内创建一个文件activate_account.html,并粘贴以下代码。

<div>
    <h2>Hey {{ user.username }}</h2>

    <h3>We noticed you just sign up on our website<h3>
    <a href="http://{{ domain }}{% url 'activate' uidb64=uid token=token %}">Please <strong>confirm your email address</strong> and activate your account</a>
</div>

这是一个简单的HTML模板,它使用DTL变量来获取通过上下文字典(我们在SignUpForm )传递的信息,将其显示为标题二(h2 ),并创建一个类似于以下的链接。

http://localhost:8000/activate/MTE/arlr3y-7fb92ddbceb2f3efee7581f45a044135/

我们将看到,当我们完成后的电子邮件被发送时,它看起来如何。

基础模板

在使用Django模板时,建议创建一个基础模板,其中包含基本的HTML骨架和CDN链接。这可以让你节省代码,当然还有时间,因为你不必为所有的模板复制粘贴相同的HTML结构。

让我们在这个应用程序中这样做。创建一个base.html文件并粘贴以下模板。

<!DOCTYPE html>
<html lang="en">

    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Django Email Confirmation</title>
        <!-- CSS only -->
        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet"
            integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
    </head>

    <body>
        <div class="container">
            {% block body %}

            {% endblock body %}
        </div>
        <!-- JavaScript Bundle with Popper -->
        <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"
            integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous">
        </script>
    </body>

</html>

是的,我们包括Bootstrap 5的链接,并创建一个块body ,我们以后会用到。基本上,所有扩展基础模板的模板将使用这个body 块来 "插入 "HTML片段,同时使用基本HTML骨架。

报名模板

用户应该能够从我们的网站上用一个表单注册,所以让我们创建注册模板,使用crispy过滤器

templates文件夹中创建一个模板signup.html,并使用下面的代码。

{% extends 'users/base.html' %}
{% load crispy_forms_tags %}
{% block body %}
<div class="mx-auto">
    <div class="form-group">
        <form action="" method="post">
            {% csrf_token %}
            {{ form|crispy }}
            <button type="submit" class="btn btn-danger w-100 my-3">Create account</button>
        </form>
    </div>
</div>
{% endblock body %}

注意我们是如何在从基础模板扩展后加载crispy表单标签的。此外,重要的是要记住,每次我们在模板中设置一个表单时,它应该有一个CSRF标记在里面。

检查电子邮件模板

创建一个check_email.html模板并实现以下代码。

{% extends 'users/base.html' %}

{% block body %}
<div class="mx-auto text-center">
    <h1>Activate your account</h1>
    <p>Please check your inbox and verify your email address</p>
</div>

{% endblock body %}

再一次,我们扩展了基础模板并使用body块来显示一个相当简约的信息。

成功模板

我们最后的模板将向用户显示一条成功信息,确认他们的账户已经被激活。

创建一个success.html模板并粘贴以下代码。

{% extends 'users/base.html' %}

{% block body %}
<div class="mx-auto text-center">
    <h1>Congrats! you just verified your account</h1>
</div>

{% endblock body %}

测试结果

这是用户试图在这个网络应用中注册时的互动。你可以按照这些步骤来测试该应用程序。

首先,用户进入注册页面(localhost:8000/signup/)并输入他们的信息以创建一个账户。

Signup page with form

然后他们被重定向到CheckEmailView ,显示这个简单的信息。

Check email view

如果他们检查他们的收件箱,应该会出现与下面类似的信息。

Message received in inbox

他们点击电子邮件验证链接,并被重定向到ActivateView 。如果令牌正确,他们立即被重定向到成功页面。

Message in ActivateView

你可以在本地服务器的日志中看到幕后发生的情况。

Screenshot of server logs

结论

Django是目前最常用的网络框架之一,它有大量的工具来解决日常的程序员任务,如电子邮件激活。

此外,你还可以将它与强大的服务集成,如SendGrid、Twilio可编程短信,或几乎任何现有的网络API。

现在你已经完成了这个教程,你知道如何。

  • 使用SendGrid SMTP服务,用Django发送电子邮件
  • 用Django创建一次性链接
  • 用Django和SendGrid注册用户并通过电子邮件激活他们的账户