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

在传统的整体式Django网站认证中,认证更为简单,并且涉及基于会话的Cookie模式,我们将在下面进行回顾。 但是使用API会有些棘手。 请记住,HTTP是无状态协议,因此没有内置的方式可以记住用户是否从一个请求到另一个请求进行了身份验证。 每次用户请求受限资源时,它都必须验证自己。
解决方案是在每个HTTP请求中传递唯一的标识符。 令人困惑的是,此标识符的形式尚无公认的方法,它可以采用多种形式。 Django REST Framework仅随附了​​四个不同的内置身份验证选项​​! 而且还有更多第三方软件包提供了其他功能,例如JSON Web令牌( JSON Web Tokens,JWT)。

在本章中,我们将彻底探索API身份验证的工作原理,回顾每种方法的利弊,然后为我们的Blog API做出明智的选择。 到最后,我们将创建用于注册,登录和注销的API端点。

基本认证

HTTP身份验证的最常见形式称为​​“基本”身份验证​​。 客户端发出HTTP请求时,必须在授予访问权限之前强制发送批准的身份验证凭据。

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

  1. 客户端发出HTTP请求
  2. 服务器以包含401(未授权)状态的HTTP响应进行响应

代码和WWW-Authenticate HTTP标头以及有关如何授权的详细信息

  1. 客户端通过​​Authorization​​ HTTP标头发送回凭据
  2. 服务器检查凭据并以200 OK或403 Forbiddenstatus响应码

批准后,客户端将使用授权HTTP标头凭据发送所有将来的请求。 我们还可以将这种交换可视化如下:


请注意,发送的授权凭证是 ​​​<username>:<password>​​​的未加密的​​base64编码​​版本。 所以在我的情况下,这是*wsv:password123* 的base64编码为*d3N2OnBhc3N3b3JkMTIz*。

这种方法的主要优点是简单。 但是有几个主要缺点。 首先,对于每个单个请求,服务器必须查找并验证用户名和密码,这是低效的。 最好先进行一次查询,然后传递某种表示该用户已被批准的令牌。 其次,用户凭证通过明文传递,而不是完全不加密。 这是非常不安全的。 任何未经加密的互联网流量都可以轻松地捕获和重用。 因此,基本身份验证只能通过​​HTTPS​​(HTTP的安全版本)使用。


会话认证

像传统的Django一样,整体式网站长期以来一直使用替代身份验证方案,该方案将会话和cookie结合在一起。 在较高级别上,客户端使用其凭据(用户名/密码)进行身份验证,然后从服务器接收会话ID(存储为cookie)。 然后,此会话ID将在以后的每个HTTP请求的标头中传递。
传递会话ID后,服务器将使用它来查找会话对象,该对象包含给定用户的所有可用信息,包括凭据。
这种方法是有状态的,因为必须在服务器(会话对象)和客户端(会话ID)上都保存并维护一条记录。
让我们回顾一下基本流程:

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

Django REST Framework中的默认设置实际上是基本身份验证和会话身份验证的组合。 使用Django的传统的基于会话的认证系统,并通过基本身份验证在每个请求的HTTP标头中传递会话ID。
这种方法的优点是更安全,因为用户凭据仅发送一次,而不是像基本身份验证那样在每个请求/响应周期中发送一次。 由于服务器不必每次都验证用户的凭据,它只需将会话ID与会话对象进行匹配即可快速查找,因此效率更高。
但是有几个缺点。 首先,会话ID仅在执行登录的浏览器中有效; 它不能跨多个域工作。 当API需要支持多个前端(例如网站和移动应用程序)时,这是一个明显的问题。 其次,会话对象必须保持最新状态,这在具有多个服务器的大型站点中可能是一个挑战。 您如何在每个服务器上保持会话对象的准确性? 第三,对于每个单独的请求,即使是不需要身份验证的请求,都会发送cookie,这样效率很低。
结果,通常不建议对将具有多个前端的任何API使用基于会话的身份验证方案。

Token 认证

第三种主要方法以及我们将在Blog API中实现的方法是使用Token身份验证。 由于单页应用程序的兴起,这是近年来最流行的方法。
基于Token的身份验证是无状态的:客户端将初始用户凭据发送到服务器后,将生成唯一令牌,然后由客户端将其存储为cookie或​​本地存储​​。 然后,此Token在每个传入的HTTP请求的标头中传递,服务器使用它来验证用户的身份。 服务器本身不会保留用户记录,只是令牌是否有效。

让我们看一下此challenge/response流中的实际HTTP消息的简单版本。 请注意,HTTP标头WWW-Authenticate指定在响应授权标头请求中使用的令牌的使用。
![image-20200918132157044](.assets/image-20200918132157044.png)
这种方法有很多好处。 由于令牌存储在客户端上,因此扩展服务器以维护最新的会话对象不再是问题。 令牌可以在多个前端之间共享:同一Token可以代表网站上的用户和移动应用程序上的同一用户。 相同的会话ID无法在不同的前端之间共享,这是一个主要限制。
潜在的不利因素是令牌可能会变得很大。 令牌包含所有用户信息,而不仅仅是一个会话ID /会话对象设置的ID。 由于令牌是在每个请求上发送的,因此管理令牌的大小可能会成为性能问题。
令牌的实施方式也可能有很大不同。 Django REST框架的内置​​TokenAuthentication​​是非常基础的设置。 因此,它不支持设置令牌过期,这是可以添加的安全性改进。 它还为每个用户仅生成一个令牌,因此网站上的用户以及随后的移动应用程序将使用相同的令牌。 由于有关用户的信息存储在本地,因此这可能会导致维护和更新两组客户信息的问题。

JSON Web令牌(JWT)是令牌的增强的新版本,可以通过几个第三方软件包将其添加到Django REST Framework中。 JWT具有许多优点,包括生成唯一的客户端令牌和令牌过期的能力。 它们既可以在服务器上生成,也可以使用第三方服务(如​​Auth0​​)生成。 而且,JWT可以被加密,从而使其更安全地通过不安全的HTTP连接发送。

最终,对于大多数Web API来说,最安全的选择是使用基于令牌的身份验证方案。 JWT是一种不错的,现代的添加,尽管它们需要其他配置。 因此,在本书中,我们将使用内置的TokenAuthentication。

默认认证

第一步是配置我们的新身份验证设置。 Django REST框架随附​​许多隐式设置​​的设置。 例如,在我们将DEFAULT_PERMISSION_CLASSES更新为IsAuthenticated之前,已将其设置为AllowAny。

默认情况下,DEFAULT_AUTHENTICATION_CLASSES设置为SessionAuthentication和BasicAuthentication。 让我们将它们明确添加到我们的blog_project / settings.py文件中。

REST_FRAMEWORK = { 
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
'DEFAULT_AUTHENTICATION_CLASSES': [ # new
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication'
],
}

为什么要同时使用两种方法? 答案是它们有不同的用途。 会话用于增强Browsable API的功能以及登录和注销该功能的能力。 BasicAuthentication用于在API本身的HTTP标头中传递会话ID。
如果您在 http://127.0.0.1:8000/api/v1/ 上重新浏览了可浏览的API,它将像以前一样工作。 从技术上讲,什么都没有改变,我们只是将默认设置明确化了。

实现token认证

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

# blog_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,因为我们的Browsable API仍需要它,但现在使用令牌在HTTP标头中来回传递身份验证凭据。
我们还需要添加authtoken应用,该应用会在服务器上生成令牌。 它包含在Django REST Framework中,但必须添加到我们的INSTALLED_ APPS设置中:

# blog_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',
'rest_framework.authtoken', # new

# Local
'posts.apps.PostsConfig',
]

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

(blogapi) $ python manage.py migrate

现在再次启动服务器。

(blogapi) $ python manage.py runserver

如果您访问 http://127.0.0.1:8000/admin/ 上的Django admin,则会看到顶部现在有一个Tokens部分。 确保您使用超级用户帐户登录才能访问。
![image-20200918133411073](.assets/image-20200918133411073.png)
单击令牌链接以转到令牌页面,网址为:http://127.0.0.1:8000/admin/authtoken/token/。
![image-20200918133447741](.assets/image-20200918133447741.png)
当前没有令牌可能令人惊讶。 毕竟我们有现有用户。 但是,仅在有用于用户登录的API调用之后才生成令牌。我们尚未这样做,因此尚无令牌。 我们将很快!

终端

我们还需要创建端点,以便用户可以登录和注销。 为此,我们可以创建一个专用的用户应用程序,然后添加我们自己的网址,视图和序列化程序。 但是,用户身份验证是我们真正不想犯错误的领域。 并且由于几乎所有的API都需要此功能,因此可以使用一些优秀且经过测试的第三方程序包是有道理的。
值得注意的是,我们将结合使用​​django-rest-auth​​来简化操作。 对于使用第三方软件包,请不要感到难过。 它们存在是有原因的,甚至最好的Django专业人士始终都依赖它们。 不必重新设计轮子!


Django-Rest-Auth

首先,我们将添加登录,注销和密码重置API端点。 这些与流行的django-rest-auth软件包一起提供。 使用Control + c停止服务器,然后安装它。

(blogapi) $ pipenv install django-rest-auth==0.9.5

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

# blog_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',
'rest_framework.authtoken',
'rest_auth', # new

# Local
'posts.apps.PostsConfig',
]

使用 ​​rest_auth​​软件包更新我们的blog_project / urls.py文件。 我们将网址路由设置为 ​​api/v1/rest-auth​​。


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

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

我们完成了! 如果您曾经尝试实现自己的用户身份验证端点,那么django-rest-auth为我们节省了多少时间和令人头疼的问题,真是令人惊讶。
现在我们可以启动服务器以查看django-rest-auth提供了什么。

(blogapi) $ python manage.py runserver

我们在 ​​http://127.0.0.1:8000/api/v1/rest-auth/login/ ​​上有一个有效的登录端点。

![image-20200918134056701](.assets/image-20200918134056701.png)
并在 http://127.0.0.1:8000/api/v1/rest-auth/logout/ 中注销端点。
![image-20200918134133559](.assets/image-20200918134133559.png)
还有用于密码重置的端点:
​ http://127.0.0.1:8000/api/v1/rest-auth/password/reset ​

![image-20200918134218800](.assets/image-20200918134218800.png)
并确认密码重置:
http://127.0.0.1:8000/api/v1/rest-auth/password/reset/confirm
![image-20200918134252367](.assets/image-20200918134252367.png)

用户注册

接下来是我们的用户注册或注册端点。 传统Django不附带用于用户注册的内置视图或URL,Django REST Framework也没有。 这意味着我们需要从头开始编写自己的代码; 考虑到错误的严重性和安全性,这种方法有些冒险。
一种流行的方法是使用第三方软件包​​django-allauth​​,该软件包随附用户注册以及Django身份验证系统的许多其他功能,例如通过Facebook,Google,Twitter等进行社会身份验证。如果添加rest_auth。 从django-rest-auth软件包进行注册,那么我们也有用户注册端点!

使用Control + c停止本地服务器,然后安装django-allauth。

(blogapi) $ pipenv install django-allauth==0.40.0

然后更新我们的INSTALLED_APPS设置。 我们必须添加几个新的配置:

  • django.contrib.sites
  • allauth
  • allauth.account
  • allauth.socialaccount
  • rest_auth.registration

确保还包括EMAIL_BACKEND和SITE_ID。 从技术上讲,它们在settings.py文件中的放置位置无关紧要,但是通常会在底部添加其他配置。

# blog_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',
'rest_framework.authtoken',
'allauth', # new
'allauth.account', # new
'allauth.socialaccount', # new
'rest_auth',
'rest_auth.registration', # new

# Local
'posts.apps.PostsConfig', ]

...

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

需要电子邮件后端配置,因为默认情况下,注册新用户时会发送一封电子邮件,要求他们确认其帐户。 与其设置电子邮件服务器,不如使用console.EmailBackend设置将电子邮件输出到控制台。
SITE_ID是内置Django“站点”框架的一部分,该框架是托管来自同一Django项目的多个网站的一种方式。 显然,我们这里只有一个站点,但django-allauth使用站点框架,因此我们必须指定默认设置。
好。 我们添加了新的应用程序,现在该更新数据库了。

(blogapi) $ python manage.py migrate

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

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

urlpatterns = [
path('admin/', admin.site.urls),
path('api/v1/', include('posts.urls')),
path('api-auth/', include('rest_framework.urls')),
path('api/v1/rest-auth/', include('rest_auth.urls')),
path('api/v1/rest-auth/registration/', include('rest_auth.registration.urls')),
]

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

(blogapi) $ python manage.py runserver

现在在以下位置有一个用户注册端点:
http://127.0.0.1:8000/api/v1/rest-auth/registration/
![image-20200918135143855](.assets/image-20200918135143855.png)

Tokens

为确保一切正常,请通过可浏览的API端点创建第三个用户帐户。 我已经将我的用户称为testuser2。 然后点击“ POST”按钮。
![image-20200918135225957](.assets/image-20200918135225957.png)
下一个屏幕显示来自服务器的HTTP响应。 我们的用户注册POST成功,因此在顶部创建了状态代码HTTP 201。 返回值密钥是此新用户的身份验证令牌。
![image-20200918135255752](.assets/image-20200918135255752.png)
如果您查看命令行控制台,那么django-allauth将自动生成一封电子邮件。 可以更新此默认文本,并添加具有Django初学者介绍的其他配置的电子邮件SMTP服务器。

Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: [example.com] Please Confirm Your E-mail Address From: webmaster@localhost
To: testuser2@email.com
Date: Wed, 10 Oct 2019 19:29:24 -0000
Message-ID: <153626216499.84718.7765647716299907673@1.0.0.127.in-addr.arpa>

Hello from example.com!
You're receiving this e-mail because user testuser2 has given yours as an\ e-mail address to connect their account.

To confirm this is correct, go to http://127.0.0.1:8000/api/v1/rest-auth/\ registration/account-confirm-email/MQ:1fxzy0:4y-f6DqQFZVNB_-PgBI4Iq_M4iM/

Thank you from example.com!
example.com

在您的网络浏览器中,通过 http://127.0.0.1:8000/admin/ 切换到Django admin。 您将需要使用您的超级用户帐户。
然后,单击页面顶部的令牌链接。
![image-20200918135432126](.assets/image-20200918135432126.png)
您将被重定向到位于以下位置的令牌页面:
http://127.0.0.1:8000/admin/authtoken/token/。
![image-20200918135510644](.assets/image-20200918135510644.png)
Django REST框架已为testuser2用户生成了一个令牌。 通过API创建其他用户时,其令牌也将显示在此处。
逻辑上的问题是,为什么我们的超级用户帐户或测试用户没有令牌? 答案是我们在添加令牌认证之前创建了这些帐户。 但是不用担心,一旦我们通过API使用任一帐户登录,令牌就会自动添加并可用。
继续,让我们使用我们的新testuser2帐户登录。 打开Web浏览器,访问 http://127.0.0.1:8000/api/v1/rest-auth/login/。
输入我们的testuser2帐户的信息。 点击“ POST”按钮。
![image-20200918135612870](.assets/image-20200918135612870.png)
发生了两件事。 在右上角,我们的用户帐户testuser2可见,确认我们现在已登录。服务器还发送了带有令牌的HTTP响应。
![image-20200918135639783](.assets/image-20200918135639783.png)
在我们的前端框架中,我们需要在​​本地存储​​中或以cookie的形式在客户端上捕获并存储此令牌。 然后配置我们的应用程序,以便所有将来的请求都在标头中包含令牌,以作为认证用户的一种方式。


总结

首次使用Web API时,用户身份验证是最难掌握的领域之一。 没有整体结构的好处,作为开发人员,我们必须深入了解并适当配置我们的HTTP请求/响应周期。
Django REST Framework对此过程提供了很多内置支持,包括内置TokenAuthentication。 但是,开发人员必须自己配置其他区域,例如用户注册和专用的url /视图。 因此,一种流行,强大且安全的方法是依靠第三方软件包django-rest-auth和django-allauth来最大程度地减少必须从头开始编写的代码量。