所有文章 > API开发 > 使用Django REST Framework构建API——第二部分
使用Django REST Framework构建API——第二部分

使用Django REST Framework构建API——第二部分

在本文的第二部分,我将向您展示如何利用Django REST Framework提供的两个核心功能——身份验证和权限,来保护您的API。如果没有阅读第一部分,请重新阅读,然后再继续,因为本文将在此基础上进行深入的探讨。

在这一章节中,我们将为您的音乐API添加访问限制。假设您的API由于安全原因不再对外公开,而是要求只有注册用户才能使用,那么我们就需要引入相应的机制来确保这一点。这些机制正是身份验证、授权和权限。

那么,什么是身份验证呢?

身份验证是指将传入的请求与特定的身份识别凭证(例如用户ID或签名令牌)进行关联的过程。身份验证本身并不决定是否允许或拒绝请求,它仅仅是识别出发送请求的凭证。当用户通过身份验证后,Django REST Framework会进一步检查用户是否有权访问他们请求的资源。关于权限和授权的具体内容,我们将在后文中详细讨论。

身份验证是在视图处理流程的最开始阶段进行的,它会在授权检查之前以及允许其他代码继续执行之前进行。

接下来,我将向您展示如何为我们在第一部分中开发的音乐服务API添加基于令牌的身份验证机制。这里我们将使用JSON Web Token(JWT)作为令牌。使用JWT的优势在于,您无需在数据库中保存令牌信息。

此外,JWT提供了一种既简单又强大的方式,用于在客户端和服务器之间安全地传输身份验证信息。它是一种紧凑且URL安全的表示方法,用于在双方之间传递声明信息。JWT中的声明信息被编码为JSON对象,并使用JSON Web Signature(JWS)进行数字签名。

在使用JWT时,请务必通过HTTPS协议公开您的终端节点,以确保通过JWT交换的数据的完整性免受中间人攻击。

让我们将 JWT 集成到API中

首先,您需要安装一个第三方软件包,该软件包为 API 提供 JWT 接口。打开 CLI 并输入以下命令;

(venv)music_service$ pip install djangorestframework-jwt

安装 djangorestframework-jwt 包后,您现在可以继续打开文件并将 添加到 Django REST 框架。

# ...

# Look for the REST_FRAMEWORK inside the settings.py file

REST_FRAMEWORK = {
# ...

# inside the Rest framework settings dictionary, add the auth settings
# Authentication settings
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
],

#...
}

仍然在api/settings.py文件中,您需要添加一个包含JWT设置的字典。这个字典有助于定义JWT使用的默认值。在继续之前,请将以下代码复制并粘贴到您的api/settings.py文件中。

# ...

# Inside the settings.py file, add the JWT dictionary

# JWT settings
JWT_AUTH = {
'JWT_ENCODE_HANDLER':
'rest_framework_jwt.utils.jwt_encode_handler',

'JWT_DECODE_HANDLER':
'rest_framework_jwt.utils.jwt_decode_handler',

'JWT_PAYLOAD_HANDLER':
'rest_framework_jwt.utils.jwt_payload_handler',

'JWT_PAYLOAD_GET_USER_ID_HANDLER':
'rest_framework_jwt.utils.jwt_get_user_id_from_payload_handler',

'JWT_RESPONSE_PAYLOAD_HANDLER':
'rest_framework_jwt.utils.jwt_response_payload_handler',

'JWT_SECRET_KEY': SECRET_KEY,
'JWT_GET_USER_SECRET_KEY': None,
'JWT_PUBLIC_KEY': None,
'JWT_PRIVATE_KEY': None,
'JWT_ALGORITHM': 'HS256',
'JWT_VERIFY': True,
'JWT_VERIFY_EXPIRATION': True,
'JWT_LEEWAY': 0,
'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=300),
'JWT_AUDIENCE': None,
'JWT_ISSUER': None,

'JWT_ALLOW_REFRESH': False,
'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7),

'JWT_AUTH_HEADER_PREFIX': 'Bearer',
'JWT_AUTH_COOKIE': None,
}


# ...

**此时,您已经在项目中成功配置了JWT,并且也设置了全局的身份验证参数。这些身份验证设置会默认应用于API中的所有视图,除非您在视图级别进行了特定的覆盖。

接下来,让我们深入探讨一下DRF中的权限设置。

为什么我们需要设置权限呢?

权限、身份验证和限制共同决定了是否应该授予某个请求访问权限或拒绝其访问。

权限检查总是在视图处理流程的最开始阶段进行,只有在权限检查通过之后,才会允许其他代码继续执行。权限检查通常会利用request.userrequest.auth属性中的身份验证信息,来判断是否应该允许传入的请求。

权限机制用于为不同类别的用户授予或拒绝访问API不同部分的权限。

最简单的权限设置是允许所有经过身份验证的用户访问,而拒绝所有未经身份验证的用户。在DRF中,这对应于IsAuthenticated类。

现在让我们为API添加权限。打开api/settings.py文件并将DjangoPermissionsOrnonReadOnly添加到Django REST框架的DEFAULT_PERMISSION_CLASSES中。

# ...

# Look for the REST_FRAMEWORK inside the settings.py file

REST_FRAMEWORK = {
# ...

# inside the Rest framework settings dictionary, add the permission settings
# Permission settings
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
],

#...
}

DjangoModelPermissionsOrAnonReadOnly 类不仅融合了 Django 的权限系统,还特地为未经身份验证的用户提供了对 API 的只读访问权限。至此,您已经成功配置了全局性的权限设置,这些设置将默认应用于 API 中与特定端点(例如 songs/:id)相关联的详情视图。当然,您也有权在视图层面对全局设置进行个性化调整。在接下来的内容中,当我们在 GET songs/ 端点上配置权限时,您将亲眼见证这一过程的实现。

用户登录视图

为了让用户能够顺利使用您的 API,他们需要先成功登录并获得相应的令牌。现在,您需要添加一个视图,专门用于处理用户尝试登录 API 时的身份验证流程,并向客户端返回令牌。

测试先行

在正式开始编码视图之前,按照惯例,我们先添加相应的测试。

请打开 music/tests.py 文件,并添加以下代码行。同时,您还需要对 BaseViewTest 类进行更新,具体更新内容如下面的代码片段所示。

# ...

# Add this line at the top of the tests.py file
from django.contrib.auth.models import User

# update the BaseViewTest to this

class BaseViewTest(APITestCase):
client = APIClient()

@staticmethod
def create_song(title="", artist=""):
if title != "" and artist != "":
Songs.objects.create(title=title, artist=artist)

def login_a_user(self, username="", password=""):
url = reverse(
"auth-login",
kwargs={
"version": "v1"
}
)
return self.client.post(
url,
data=json.dumps({
"username": username,
"password": password
}),
content_type="application/json"
)

def setUp(self):
# create a admin user
self.user = User.objects.create_superuser(
username="test_user",
email="test@mail.com",
password="testing",
first_name="test",
last_name="user",
)
# add test data
self.create_song("like glue", "sean paul")
self.create_song("simple song", "konshens")
self.create_song("love is wicked", "brick and lace")
self.create_song("jam rock", "damien marley")

然后添加扩展新的AuthLoginUserTestBaseViewTest,如下面的代码片段所示;

# ....
# Add this line at the top of your tests.py file
from django.contrib.auth.models import User


# ...

# Then add these lines of code at the end of your tests.py file

class AuthLoginUserTest(BaseViewTest):
"""
Tests for the auth/login/ endpoint
"""

def test_login_user_with_valid_credentials(self):
# test login with valid credentials
response = self.login_a_user("test_user", "testing")
# assert token key exists
self.assertIn("token", response.data)
# assert status code is 200 OK
self.assertEqual(response.status_code, status.HTTP_200_OK)
# test login with invalid credentials
response = self.login_a_user("anonymous", "pass")
# assert status code is 401 UNAUTHORIZED
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

创建登录视图

由于您的登录视图需要向客户端返回令牌,因此您需要定义一个序列化器,用于将令牌数据序列化为客户端可以理解的格式。在第一部分中,我已经解释了为什么对于返回数据的视图,我们需要为其指定序列化器。

现在,为了创建一个用于序列化令牌的序列化器,请打开 music/serializers.py 文件,并在其中添加以下代码行。

# Add these lines at the top of your views.py file
from django.contrib.auth.models import User
from django.contrib.auth import authenticate, login
from rest_framework_jwt.settings import api_settings
from rest_framework import permissions

# Get the JWT settings, add these lines after the import/from lines
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER

# ...

# Add this view to your views.py file

class LoginView(generics.CreateAPIView):
"""
POST auth/login/
"""
# This permission class will overide the global permission
# class setting
permission_classes = (permissions.AllowAny,)

queryset = User.objects.all()

def post(self, request, *args, **kwargs):
username = request.data.get("username", "")
password = request.data.get("password", "")
user = authenticate(request, username=username, password=password)
if user is not None:
# login saves the user’s ID in the session,
# using Django’s session framework.
login(request, user)
serializer = TokenSerializer(data={
# using drf jwt utility functions to generate a token
"token": jwt_encode_handler(
jwt_payload_handler(user)
)})
serializer.is_valid()
return Response(serializer.data)
return Response(status=status.HTTP_401_UNAUTHORIZED)

正如之前所提到的,Django REST Framework 允许我们在视图级别自定义权限类,以覆盖全局设置。在上面的代码示例中,我们通过设置  LoginView 的 permission_classes 属性,实现了对登录视图的权限控制,这里我们使用了 AllowAny 类,它允许所有人公开访问这个视图,因为作为一个登录接口,其公开性是至关重要的。

接下来,我们需要为第一部分中定义的 ListSongsView 设置权限。由于我们希望保护歌曲列表,防止未登录的用户查看,因此我们将 ListSongsView 的 permission_classes 属性设置为 IsAuthenticated。经过这样的设置后,ListSongsView 的代码应该如下所示:

class ListSongsView(ListAPIView):
"""
Provides a get method handler.
"""
queryset = Songs.objects.all()
serializer_class = SongsSerializer
permission_classes = (permissions.IsAuthenticated,)

连接视图

在正式运行测试之前,我们还需要将 LoginView 连接到对应的URL上。这样,当用户尝试访问登录接口时,Django才能正确地调用 LoginView 来处理请求。

打开music/urls.py文件并将以下代码行添加到现有的urlpatterns列表中。

# Add *LoginView* to this line in  your urls.py file
from .views import ListCreateSongsView, SongsDetailView, LoginView

urlpatterns = [
# ...

# Some where in your existing urlpatterns list, Add this line
path('auth/login/', LoginView.as_view(), name="auth-login")

# ...
]

现在让我们测试一下!

首先,让我们运行自动化测试。运行命令;

(venv)music_service$ python manage.py test

执行完毕后,您将在终端中看到测试结果输出;

Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.F
===================================================================
FAIL: test_get_all_songs (music.tests.GetAllSongsTest)
-------------------------------------------------------------------
Traceback (most recent call last):
File "/your/dir/to/music_service/music/tests.py", line xxx, in test_get_all_songs
self.assertEqual(response.data, serialized.data)
AssertionError: {'detail': 'Authentication credentials we[13 chars]ed.'} != [OrderedDict([('title', 'like glue'), ('a[224 chars]')])]
--------------------------------------------------------------------Ran 2 tests in 0.010s
FAILED (failures=1)
Destroying test database for alias 'default'...

为什么测试失败?

遇到测试失败时,请不必惊慌!通常,问题的根源往往隐藏在一些细微之处。

这实际上是一个积极的信号,意味着我们之前在第1部分中编写的测试现在因为向ListSongsView添加了权限而失败了。

在命令行界面中,测试失败的反馈可能不够直观,难以迅速定位问题所在。为了更清晰地了解失败的原因,你可以尝试在浏览器中访问http://127.0.0.1:8000/api/v1/songs/。此时,你应该会看到类似下面的提示信息:

当你看到红色的错误信息提示“未提供身份验证凭证”时,这意味着你现在需要登录才能查看所有歌曲。这正是我们在本教程这一部分想要实现的目标:保护API端点,确保只有经过身份验证的用户才能访问。

让我们修复测试

现在,要修复第 1 部分中的测试,请按照下面代码片段中的注释进行操作,并按照说明进行操作。

# 1. update the urlpatterns list in api/urls.py file to this;
from rest_framework_jwt.views import obtain_jwt_token

urlpatterns = [
path('admin/', admin.site.urls),
path('api-token-auth/', obtain_jwt_token, name='create-token'),
re_path('api/(?P<version>(v1|v2))/', include('music.urls'))
]

#2. Then update the BaseViewTest class in music/tests.py and add
# this method to it

# class BaseViewTest(APITestCase):
# ...
def login_client(self, username="", password=""):
# get a token from DRF
response = self.client.post(
reverse('create-token'),
data=json.dumps(
{
'username': username,
'password': password
}
),
content_type='application/json'
)
self.token = response.data['token']
# set the token in the header
self.client.credentials(
HTTP_AUTHORIZATION='Bearer ' + self.token
)
self.client.login(username=username, password=password)
return self.token

#3. Update the test and add invoke self.login_client method
# Below is the complete updated test class from part 1

class GetAllSongsTest(BaseViewTest):

def test_get_all_songs(self):
"""
This test ensures that all songs added in the setUp method
exist when we make a GET request to the songs/ endpoint
"""
# this is the update you need to add to the test, login
self.login_client('test_user', 'testing')
# hit the API endpoint
response = self.client.get(
reverse("songs-all", kwargs={"version": "v1"})
)
# fetch the data from db
expected = Songs.objects.all()
serialized = SongsSerializer(expected, many=True)
self.assertEqual(response.data, serialized.data)
self.assertEqual(response.status_code, status.HTTP_200_OK)

在成功更新测试代码后,您的测试现在应该已经全部通过。

额外奖励:用户注册视图

既然您已经有了用户登录的视图,那么我认为您同样需要一个用于用户注册的视图。作为额外奖励,我将向您展示如何添加一个供API用户使用的注册视图。

此视图将对所有人开放,因此您无需对其设置访问限制。您可以将视图的permission_classes设置为AllowAny,以便任何人都能访问。

添加测试

首先您需要编写测试。

打开music/tests.py并添加以下代码行;

# ...

# At the end of tests.py file, add these lines of code

class AuthRegisterUserTest(AuthBaseTest):
"""
Tests for auth/register/ endpoint
"""
def test_register_a_user_with_valid_data(self):
url = reverse(
"auth-register",
kwargs={
"version": "v1"
}
)
response = self.client.post(
url,
data=json.dumps(
{
"username": "new_user",
"password": "new_pass",
"email": "new_user@mail.com"
}
),
content_type="application/json"
)
# assert status code is 201 CREATED
self.assertEqual(response.status_code, status.HTTP_201_CREATED)

def test_register_a_user_with_invalid_data(self):
url = reverse(
"auth-register",
kwargs={
"version": "v1"
}
)
response = self.client.post(
url,
data=json.dumps(
{
"username": "",
"password": "",
"email": ""
}
),
content_type='application/json'
)
# assert status code
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

创建注册视图

现在打开music/views.py并添加以下代码行:

# ...

# Add these lines to the views.py file
class RegisterUsers(generics.CreateAPIView):
"""
POST auth/register/
"""
permission_classes = (permissions.AllowAny,)

def post(self, request, *args, **kwargs):
username = request.data.get("username", "")
password = request.data.get("password", "")
email = request.data.get("email", "")
if not username and not password and not email:
return Response(
data={
"message": "username, password and email is required to register a user"
},
status=status.HTTP_400_BAD_REQUEST
)
new_user = User.objects.create_user(
username=username, password=password, email=email
)
return Response(status=status.HTTP_201_CREATED)

连接视图

在正式运行测试之前,我们还需要做一项重要的工作:通过配置URL来将新视图链接起来。

打开music/urls.py文件并将以下代码行添加到现有的urlpatterns列表中。

# Add this line to your urls.py file

urlpatterns = [
# ...

# Some where in your existing urlpatterns list, Add this line
path('auth/register/', RegisterUsersView.as_view(), name="auth-register")

# ...
]

现在,URL已经配置完毕,您可以使用python manage.py test命令来运行测试了。

收获

如您所见,在Django REST Framework(DRF)中设置安全性是非常简单的。您只需编写极少量的代码即可完成。

您通过全局设置来配置API的授权和权限,从而确保API的安全性。

此外,您还可以在每个视图级别上设置安全性。

为了练习,您可以继续实现下面这个表格中列出的简单音乐服务API的其余认证端点;

1EndpointHTTP MethodCRUD MethodResponse
2auth/login/POSTREADAuthenticate user
3auth/register/POSTCREATECreate a new user
4auth/reset-password/PUTUPDATEUpdate user password
5auth/logout/GETREADLogout user

额外资源推荐

我强烈推荐您浏览以下资源,以便更深入地了解其他HTTP身份验证方案。掌握这些方案并洞悉安全性的演进历程至关重要。

此外,我还推荐您使用一款名为Postman的出色工具,它能帮助您手动测试API端点。您只需在开发电脑上进行安装,即可立即上手尝试。

希望以上内容对您有所帮助,并衷心感谢您阅读本文。

原文链接:https://medium.com/backticks-tildes/lets-build-an-api-with-django-rest-framework-part-2-cfb87e2c8a6c

#你可能也喜欢这些API文章!