跳转至

第8章:用户认证

在上一章中,我们更新了 API 的权限,这也称为授权。在本章中,我们将实现认证,这是用户可以注册新帐户、登录和注销的过程。

在传统的、单体 Django 网站中,认证更简单,涉及基于会话的 cookie 模式,我们将在下面回顾。但对于 API,事情有点棘手。请记住,HTTP 是一种无状态协议,因此没有内置的方法来记住用户是否从一次请求到下一次请求都经过认证。每次用户请求受限资源时,它都必须验证自己。

解决方案是在每个 HTTP 请求中传递一个唯一的标识符。令人困惑的是,对于这种标识符的形式没有普遍同意的方法,它可以采取多种形式。Django REST Framework 附带了四种不同的内置认证选项:基本、会话、令牌和默认。还有更多的第三方包提供额外的功能,如 JSON Web Tokens(JWTs)。

在本章中,我们将彻底探索 API 认证如何工作,回顾每种方法的优缺点,然后为我们的博客 API 做出明智的选择。到最后,我们将创建用于注册、登录和注销的 API 端点。

基本认证

最常见的 HTTP 认证形式被称为"基本"认证。当客户端发出 HTTP 请求时,在授予访问权限之前,它被强制发送批准的认证凭据。

完整的请求/响应流如下所示:

  1. 客户端发出 HTTP 请求
  2. 服务器响应包含 401(未授权)状态码的 HTTP 响应,以及包含如何授权详细信息的 WWW-Authenticate HTTP 标头
  3. 客户端通过 Authorization HTTP 标头发回凭据
  4. 服务器检查凭据并响应 200 OK 或 403 Forbidden 状态码

一旦获得批准,客户端将在所有未来的请求中发送带有 Authorization HTTP 标头凭据。我们也可以如下所示可视化这种交换:

Diagram

Client                                      Server
------                                      ------
--------------------------------------->
GET / HTTP/1.1

<-------------------------------------
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic

--------------------------------------->
GET / HTTP/1.1
Authorization: Basic d3N2OnBhY3N3b3JkMTIz

<-------------------------------------
HTTP/1.1 200 OK

请注意,发送的认证凭据是 <username>:<password> 的未加密 base64 编码版本。所以在我的例子中,这是 wsv:password123,使用 base64 编码后是 d3N2OnBhY3N3b3JkMTIz

这种方法的主要优点是简单。但也有几个主要的缺点。首先,在每个请求中,服务器必须查找并验证用户名和密码,这是低效的。最好进行一次查找,然后传递某种令牌,说明此用户已批准。其次,用户凭据以明文形式传递——根本不加密——通过互联网。这是非常不安全的。任何未加密的互联网流量都很容易被捕获和重用。因此,基本认证应该只通过 HTTPS(HTTP 的安全版本)使用。

会话认证

单体网站(如传统的 Django)长期以来一直使用另一种认证方案,它是会话和 cookie 的组合。在高层,客户端使用其凭据(用户名/密码)进行认证,然后从服务器接收会话 ID,该 ID 作为 cookie 存储。然后在每个未来的 HTTP 请求的标头中传递此会话 ID。

当会话 ID 被传递时,服务器使用它来查找包含给定用户所有可用信息的会话对象,包括凭据。

这种方法是状态化的,因为必须在服务器(会话对象)和客户端(会话 ID)上保持和维护记录。

让我们回顾基本流程:

  1. 用户输入其登录凭据(通常是用户名/密码)
  2. 服务器验证凭据是否正确,并生成会话对象,然后存储在数据库中
  3. 服务器向客户端发送会话 ID(不是会话对象本身),该 ID 作为 cookie 存储在浏览器上
  4. 在所有未来的请求中,会话 ID 作为 HTTP 标头包含,如果数据库验证,则请求继续进行
  5. 一旦用户注销应用程序,客户端和服务器都会销毁会话 ID
  6. 如果用户稍后再次登录,会生成新的会话 ID 并作为 cookie 存储在客户端

Django REST Framework 中的默认设置实际上是基本认证和会话认证的组合。使用 Django 的传统基于会话的认证系统,会话 ID 在每个请求中通过基本认证在 HTTP 标头中传递。

这种方法的优点是更安全,因为用户凭据只发送一次,而不是像基本认证中那样在每个请求/响应周期中发送。它也更高效,因为服务器不需要每次都验证用户的凭据,它只需将会话 ID 与会话对象匹配,这是一个快速查找。

然而,有几个缺点。首先,会话 ID 仅在执行登录的浏览器中有效;它不能跨多个域工作。当 API 需要支持多个前端(如网站和移动应用程序)时,这是一个明显的问题。其次,必须保持会话对象的最新状态,这在具有多个服务器的大型站点中可能具有挑战性。如何在每个服务器之间维护会话对象的准确性?第三,cookie 为每个请求发送,即使是那些不需要认证的请求,这也是低效的。

因此,通常不建议对任何具有多个前端的 API 使用基于会话的认证方案。

令牌认证

第三种主要方法——也是我们将在博客 API 中实现的方法——是使用令牌认证。由于单页应用程序的兴起,这是近年来最流行的方法。

基于令牌的认证是无状态的:一旦客户端将初始用户凭据发送到服务器,就会生成唯一令牌,然后由客户端作为 cookie 或本地存储存储。然后在每个传入的 HTTP 请求的标头中传递此令牌,服务器使用它来验证用户是否已认证。服务器本身不保留用户的记录,只保留令牌是否有效。

Cookie 用于读取服务器端信息。它们的大小较小(4 KB),并自动随每个 HTTP 请求发送。本地存储设计用于客户端信息。它大得多(5120 KB),其内容包括默认情况下不随每个 HTTP 请求发送。

存储在 cookie 和本地存储中的令牌都容易受到 XSS 攻击。当前的最佳实践是将令牌存储在具有 httpOnlySecure cookie 标志的 cookie 中。

让我们在这个质询/响应流中查看实际 HTTP 消息的简单版本。请注意,HTTP 标头 WWW-Authenticate 指定了令牌的使用,该令牌用于响应 Authorization 标头请求。

Diagram

Client                                      Server
------                                      ------
--------------------------------------->
GET / HTTP/1.1

<-------------------------------------
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Token

--------------------------------------->
GET / HTTP/1.1
Authorization: Token 401f7ac837da42b97f613d789819ff93537bee6a

<-------------------------------------
HTTP/1.1 200 OK

这种方法有多个好处。由于令牌存储在客户端,将服务器扩展到维护最新会话对象不再是问题。并且令牌可以在多个前端之间共享:相同的令牌可以代表网站上的用户和移动应用程序上的相同用户。相同的会话 ID 不能在不同的前端之间共享。

这种方法的潜在缺点之一是令牌可能会变得相当大。令牌包含所有用户信息,而不仅仅是像会话ID/会话对象设置那样的ID。由于令牌随每个请求发送,管理其大小可能会成为性能问题。

令牌的实现方式也可能有很大差异。Django REST Framework 的内置令牌认证故意非常基础。因此,它不支持设置令牌过期,这是一种可以添加的安全改进。它还为每个用户只生成一个令牌,因此在网站上的用户和后来的移动应用程序上将使用相同的令牌。由于有关用户的信息存储在本地,这可能会导致维护或更新两组客户端信息的问题。

JSON Web Tokens(JWTs)是一种较新的令牌形式,包含加密签名的 JSON 数据。JWT 最初设计用于 OAuth,OAuth 是一种网站共享对用户信息的访问权限而不实际共享用户密码的开放标准方式。JWT 可以在服务器上使用第三方包(如 djangorestframework-simplejwt)生成,或通过第三方服务(如 Auth0)生成。然而,关于使用 JWT 进行用户认证的优缺点,开发人员之间正在进行辩论,正确覆盖它超出了本书的范围。这就是为什么我们在本书中将坚持使用内置的令牌认证。

默认认证

第一步是配置我们的新认证设置。Django REST Framework 附带了许多隐式设置的设置。例如,在我们将其更新为 IsAuthenticated 之前,DEFAULT_PERMISSION_CLASSES 设置为 AllowAny

DEFAULT_AUTHENTICATION_CLASSES 默认设置,让我们将 SessionAuthenticationBasicAuthentication 都明确添加到我们的 django_project/settings.py 文件中。

Code

# django_project/settings.py
REST_FRAMEWORK = {
    "DEFAULT_PERMISSION_CLASSES": [
        "rest_framework.permissions.IsAuthenticated",
    ],
    "DEFAULT_AUTHENTICATION_CLASSES": [  # new
        "rest_framework.authentication.SessionAuthentication",
        "rest_framework.authentication.BasicAuthentication",
    ],
}

为什么两种方法都使用?答案是它们服务于不同的目的。会话用于为可浏览 API 提供动力,以及登录和注销它的能力。基本认证用于在 HTTP 标头中为 API 本身传递会话 ID。

如果您重新访问 [[http://127.0.0.1:8000/api/v1/](http://127.0.0.1:8000/api/v1/](http://127.0.0.1:8000/api/v1/](http://127.0.0.1:8000/api/v1/`)) 的可浏览 API,它将像以前一样工作。从技术上讲,没有任何变化,我们只是使默认设置明确。

实现令牌认证

现在我们需要更新我们的认证系统以使用令牌。第一步是更新我们的 DEFAULT_AUTHENTICATION_CLASSES 设置以使用令牌认证,如下所示:

Code

# django_project/settings.py
REST_FRAMEWORK = {
    "DEFAULT_PERMISSION_CLASSES": [
        "rest_framework.permissions.IsAuthenticated",
    ],
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework.authentication.SessionAuthentication",
        "rest_framework.authentication.TokenAuthentication",  # new
    ],
}

我们保留 SessionAuthentication,因为我们仍然需要它用于可浏览 API,但现在使用令牌在我们的 HTTP 标头中来回传递认证凭据。我们还需要添加 authtoken 应用程序,它在服务器上生成令牌。它包含在 Django REST Framework 中,但必须添加到我们的 INSTALLED_APPS 设置中:

Code

# django_project/settings.py
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    # 3rd-party apps
    "rest_framework",
    "corsheaders",
    "rest_framework.authtoken",  # new
    # Local
    "accounts.apps.AccountsConfig",
    "posts.apps.PostsConfig",
]

由于我们已经对 INSTALLED_APPS 进行了更改,我们需要同步数据库。使用 Control+c 停止服务器,然后运行以下命令。

Shell

(.venv) > python manage.py migrate
Operations to perform:
  Apply all migrations: accounts, admin, auth, authtoken, contenttypes, posts, sessions
Running migrations:
  Applying authtoken.0001_initial... OK
  Applying authtoken.0002_auto_20160226_1747... OK
  Applying authtoken.0003_tokenproxy... OK

现在再次启动服务器。

Shell

(.venv) > python manage.py runserver

如果您导航到 [[http://127.0.0.1:8000/admin/](http://127.0.0.1:8000/admin/](http://127.0.0.1:8000/admin/](http://127.0.0.1:8000/admin/`)) 的 Django 管理员,您会看到现在顶部有一个"Tokens"部分。确保使用您的超级用户帐户登录以进行访问。

Admin Homepage with Tokens

单击"Tokens"的链接。目前没有令牌,这可能令人惊讶。

Admin Tokens Page

毕竟我们有现有用户。但是,令牌只在有用户登录的 API 调用后生成。我们还没有这样做,所以没有令牌。我们很快就会!

端点

我们还需要创建端点,以便用户可以登录和注销。我们可以为此目的创建一个专用的用户应用程序,然后添加我们自己的 URL、视图和序列化器。然而,用户认证是一个我们真的不想犯错误的领域。由于几乎所有 API 都需要此功能,因此可以使用几个优秀且经过测试的第三方包,而不是自己编写。

值得注意的是,我们将结合使用 dj-rest-authdjango-allauth 来简化事情。不要因为使用第三方包而感到不好。它们的存在是有原因的,即使是最好的 Django 专业人员也一直依赖它们。如果没有必要,没有必要重新发明轮子!

dj-rest-auth

首先,我们将添加登录、注销和密码重置 API 端点。这些来自流行的 dj-rest-auth 包。使用 Control+c 停止服务器,然后安装它。

Shell

(.venv) > python -m pip install dj-rest-auth==2.1.11

将新应用程序添加到我们的 django_project/settings.py 文件中的 INSTALLED_APPS 配置。

Code

# django_project/settings.py
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    # 3rd-party apps
    "rest_framework",
    "corsheaders",
    "rest_framework.authtoken",
    "dj_rest_auth",  # new
    # Local
    "accounts.apps.AccountsConfig",
    "posts.apps.PostsConfig",
]

使用 dj_rest_auth 包更新我们的 django_project/urls.py 文件。我们将 URL 路由设置为 api/v1/dj-rest-auth。确保注意 URL 应该有一个破折号 - 而不是下划线 _,这是一个容易犯的错误。

Code

# django_project/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path("admin/", admin.site.urls),
    path("api/v1/", include("posts.urls")),
    path("api-auth/", include("rest_framework.urls")),
    path("api/v1/dj-rest-auth/", include("dj_rest_auth.urls")),  # new
]

我们完成了!如果您曾经尝试实现自己的用户认证端点,那么 dj-rest-auth 为我们节省的时间和麻烦确实令人惊叹。现在我们可以启动服务器,看看 dj-rest-auth 提供了什么。

Shell

(.venv) > python manage.py runserver

我们在 [[http://127.0.0.1:8000/api/v1/dj-rest-auth/login/](http://127.0.0.1:8000/api/v1/dj-rest-auth/login/](http://127.0.0.1:8000/api/v1/dj-rest-auth/login/](http://127.0.0.1:8000/api/v1/dj-rest-auth/login/`)) 有一个工作的登录端点。

API Log In Endpoint

还有一个注销端点在 [[http://127.0.0.1:8000/api/v1/dj-rest-auth/logout/。](http://127.0.0.1:8000/api/v1/dj-rest-auth/logout/。](http://127.0.0.1:8000/api/v1/dj-rest-auth/logout/。](http://127.0.0.1:8000/api/v1/dj-rest-auth/logout/`。))

API Log Out Endpoint

还有用于密码重置的端点,位于: - [[http://127.0.0.1:8000/api/v1/dj-rest-auth/password/reset](http://127.0.0.1:8000/api/v1/dj-rest-auth/password/reset](http://127.0.0.1:8000/api/v1/dj-rest-auth/password/reset](http://127.0.0.1:8000/api/v1/dj-rest-auth/password/reset`))

API Password Reset

以及用于密码重置确认的端点,位于: - [[http://127.0.0.1:8000/api/v1/dj-rest-auth/password/reset/confirm](http://127.0.0.1:8000/api/v1/dj-rest-auth/password/reset/confirm](http://127.0.0.1:8000/api/v1/dj-rest-auth/password/reset/confirm](http://127.0.0.1:8000/api/v1/dj-rest-auth/password/reset/confirm`))

API Password Reset Confirm

用户注册

接下来是我们的用户注册或注册端点。传统的 Django 不附带用户注册的内置视图或 URL,Django REST Framework 也不附带。这意味着我们需要从头开始编写自己的代码;考虑到错误的严重性以及安全影响,这是一种有些风险的方法。

一种流行的方法是使用第三方包 django-allauth,它附带用户注册以及 Django 认证系统的一些额外功能,如通过 Facebook、Google、Twitter 等的社交认证。如果我们从 dj-rest-auth 包中添加 dj_rest_auth.registration,那么我们也有用户注册端点!

使用 Control+c 停止本地服务器并安装 django-allauth

Shell

(.venv) > python -m pip install django-allauth~=0.48.0

然后更新我们的 INSTALLED_APPS 设置。我们必须添加几个新配置: - django.contrib.sites - allauth - allauth.account - allauth.socialaccount - dj_rest_auth.registration

Code

# django_project/settings.py
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "django.contrib.sites",  # new
    # 3rd-party apps
    "rest_framework",
    "corsheaders",
    "rest_framework.authtoken",
    "allauth",  # new
    "allauth.account",  # new
    "allauth.socialaccount",  # new
    "dj_rest_auth",
    "dj_rest_auth.registration",  # new
    # Local
    "accounts.apps.AccountsConfig",
    "posts.apps.PostsConfig",
]

django-allauth 需要添加到现有上下文处理器之后的 TEMPLATES 配置中,并将 EMAIL_BACKEND 设置为控制台,并添加 SITE_ID 为 1。

Code

# django_project/settings.py
TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [],
        "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",
                "django.template.context_processors.request",  # new
            ],
        },
    },
]

EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"  # new
SITE_ID = 1  # new

需要电子邮件后端配置,因为默认情况下,当新用户注册时,将发送一封电子邮件,要求他们确认其帐户。我们不会同时设置电子邮件服务器,而是使用 console.EmailBackend 设置将电子邮件输出到控制台。

SITE_ID 是内置 Django"站点"框架的一部分,这是一种从同一个 Django 项目托管多个网站的方法。我们这里只有一个网站,但 django-allauth 使用站点框架,因此我们必须指定默认设置。

好的。我们添加了新应用程序,所以是时候更新数据库了。

Shell

(.venv) > python manage.py migrate

然后为注册添加新的 URL 路由。

Code

# django_project/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path("admin/", admin.site.urls),
    path("api/v1/", include("posts.urls")),
    path("api-auth/", include("rest_framework.urls")),
    path("api/v1/dj-rest-auth/", include("dj_rest_auth.urls")),
    path("api/v1/dj-rest-auth/registration/",  # new
         include("dj_rest_auth.registration.urls")),
]

我们完成了。我们可以运行本地服务器。

Shell

(.venv) > python manage.py runserver

现在在以下位置有一个用户注册端点: - [[http://127.0.0.1:8000/api/v1/dj-rest-auth/registration/](http://127.0.0.1:8000/api/v1/dj-rest-auth/registration/](http://127.0.0.1:8000/api/v1/dj-rest-auth/registration/](http://127.0.0.1:8000/api/v1/dj-rest-auth/registration/`))

令牌

为了确保一切正常,请通过新的用户注册端点创建第三个用户帐户。我称我的用户为 testuser1

API Register New User

单击"POST"按钮后,下一个屏幕显示来自服务器的 HTTP 响应。我们的用户注册 POST 成功,因此顶部的状态码为 HTTP 201 Created。返回值键是此新用户的 auth_token

API Auth Key

如果您查看命令行控制台,Django-allauth 已自动生成一封电子邮件。可以更新此默认文本,并通过其他配置添加电子邮件 SMTP 服务器,这些内容涵盖在《Django for Beginners》一书中。

切换到 Web 浏览器中 [[http://127.0.0.1:8000/admin/](http://127.0.0.1:8000/admin/](http://127.0.0.1:8000/admin/](http://127.0.0.1:8000/admin/`)) 的 Django 管理员。您需要为此使用超级用户帐户。这里有许多 Django-allauth 添加的新字段。单击"Tokens"的链接,您将被重定向到"Tokens"页面。

Admin Tokens

Django REST Framework 已为 testuser1 用户生成了一个令牌。随着通过 API 创建其他用户,他们的令牌也将出现在这里。

一个合理的问题是,为什么我们的超级用户帐户或 testuser 没有令牌?答案是我们在添加令牌认证之前创建了那些帐户。但不用担心,一旦我们通过 API 使用其中任何一个帐户登录,令牌将自动添加并可用。

继续,让我们使用新的 testuser1 帐户登录。确保注销管理员,然后在 Web 浏览器中导航到 [[http://127.0.0.1:8000/api/v1/dj-rest-auth/login/](http://127.0.0.1:8000/api/v1/dj-rest-auth/login/](http://127.0.0.1:8000/api/v1/dj-rest-auth/login/](http://127.0.0.1:8000/api/v1/dj-rest-auth/login/))。输入我们的testuser1` 帐户的信息。单击"POST"按钮。

API Log In testuser1

发生了两件事。在右上角,我们的用户帐户 testuser1 可见,确认我们现在已登录。此外,服务器已发回带有令牌的 HTTP 响应。

API Log In Token

在我们的前端框架中,我们需要捕获并存储此令牌。传统上,这发生在客户端,要么在本地存储中,要么作为 cookie,然后所有未来的请求都在标头中包含令牌作为认证用户的一种方式。请注意,这个主题有额外的安全问题,因此您应该小心实施您选择的前端框架的最佳实践。

最后,我们应该将我们的新工作提交到 Git。

Shell

(.venv) > git status
(.venv) > git add -A
(.venv) > git commit -m "add user authentication"

结论

用户认证是首次使用 Web API 时最难掌握的区域之一。如果没有单体结构的优势,我们开发人员必须深入理解和适当配置我们的 HTTP 请求/响应周期。

Django REST Framework 附带了许多对此过程的内置支持,包括内置令牌认证。但是,开发人员必须自己配置其他区域,如用户注册和专用 URL/视图。因此,一种流行、强大和安全的方法是依赖第三方包 dj-rest-authdjango-allauth 来最小化我们必须从头编写的代码量。