基于令牌的身份验证,允许后端服务与前端(无论是web端,原生移动端或其他端)分离,并驻留在不同域中。 JSON Web Tokens (JWT) 是一种流行的令牌认证实现, 在本文中,我们使用它来验证,通过Django REST 框架.构建notes 的 API中的用户。
我们将设置用户注册和身份验证,并定义 notes 模型。确保当前登录用户对他创建的便笺有所有权,而且用户只能对自己的便笺进行读写操作。
最终项目的完整源代码在此: https://github.com/bonfirealgorithm/notes-...
1. 创建 Django 项目
$ mkdir notes && cd notes
创建虚拟环境并激活它。
$ python -m venv venv
$ source venv/bin/activate
安装 Django, DjangoREST 和 django-cors-headers:
$ pip install django django-rest-framework django-cors-headers
执行上面的命令安装 Django 以及 Django REST Framework, 帮助我们实现API。我们也安装了 django-cors-headers, 实现了跨域的功能。
接下来我们来创建一个新的Django项目:
$ django-admin startproject project .# 在当前目录内部创建名称为project的项目
上面这行命令最后的 . 表示将在当前目录内部创建Django项目。
之后我们可以通过以下命令来创建一个名为 notes 的 app,同时生成数据库迁移文件:
$ python manage.py startapp notes # 创建名为 notes 的 app
$ python manage.py migrate # 执行数据库迁移文件,修改数据库
修改 project 下的 settings.py 配置文件,将新创建的 app 都加入到 installed apps 目录中,同时添加 REST Framework 相关配置信息:
# project/settings.py
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"rest_framework", # 新加入 rest_framework 应用
"corsheaders", # 新加入 corsheaders 应用
"notes", # 新加入 notes 应用
]
MIDDLEWARE = [
"corsheaders.middleware.CorsMiddleware", # 配置跨域中间件
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
# 允许所有客户端跨域
CORS_ORIGIN_ALLOW_ALL = True
REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES":
["rest_framework.permissions.AllowAny",],
"DEFAULT_PARSER_CLASSES":["rest_framework.parsers.JSONParser",],
}
在确保配置无误的情况下,开始运行服务器:
$ python manage.py runserver # 运行 Django 服务器
在浏览器中访问 http://localhost:8000
你应该会看到 Django 的欢迎界面。
但我们仍然没有数据和 API 相关代码 来驱动 API 服务,让我们先创建一个名为 notes 的模型类。
2. 创建一个便签模型和超级用户
在 notes/models.py 文件中添加如下内容:
from django.db import models
from django.contrib.auth.models import User
class Note(models.Model):
owner = models.ForeignKey(User, on_delete=models.CASCADE)
title = models.CharField(max_length=200)
content = models.TextField()
def __str__(self):
return self.title
我们创建了一个有三个属性的便签模型。它的拥有者是一个用户类型的对象,这个对象我们通过 Django 的认证组件引入。稍后,当我们添加完认证,我们将保证这个拥有者设置到当前登录的用户。剩下的两个属性是便签的标题和内容,分别是一个短字符串和一个长文本字段。
同时注册这个模型到管理后台文件 notes/admin.py :
from django.contrib import admin
from .models import Note
admin.site.register(Note)
由于我们已创建了一个新的模型类,因此需要通过创建并运行数据库迁移文件,使得数据库和模型类同步:
$ python manage.py makemigrations # 生成数据库迁移文件
$ python manage.py migrate # 根据数据库执行文件修改数据库
让我们来创建一个管理员账号,这样就可以在后台管理面板中检查并添加 notes 对象。(你可以选择默认的用户名,并在出现提示时跳过电子邮箱输入。)
$ python manage.py createsuperuser # 创建管理员账号
让我们再次启动服务器。
$ python manage.py runserver # 启动服务器
然后访问 http://localhost:8000/admin/,并用之前创建的管理员账号密码登录。
你应该会看到如下的画面:
添加一些 notes 信息,以便我们来处理相关数据。
3. 用 Notes 数据构建 API
现在我们需要添加一些请求地址,这样就能访问到 notes 相关 API 了。
修改 project/urls.py 文件,如下所示:
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls), # 后台管理
path('api/', include('notes.urls')) # API 服务,include 用于 urls 分发
]
接下来在 notes 目录下新建 urls.py 文件,加入以下内容:
from django.urls import path
from rest_framework.routers import SimpleRouter
from .views import NoteViewSet
router = SimpleRouter() # 创建 SimpleRouter() 对象
router.register('notes', NoteViewSet, base_name="notes") # 注册路由
urlpatterns = router.urls # 将路由加入到 urls 中
我们正在使用 rest_framework 的 SimpleRouter 来自动创建路由。尽管我们还未创建 NoteViewSet,但我们已经导入了它。 接下来我们将创建 NoteViewSet , 在这之前,首先需要为我们的模型类创建一个序列化类。
rest_framework 中的序列化类提供了序列化功能,可以将模型中的数据转换成适合 API 的格式,也就是所谓的 JSON。 序列化类能执行数据验证,并根据指定字段生成序列化数据。
在 notes 目录下新建 serializers.py 文件:
from rest_framework import serializers
from .models import Note
class NoteSerializer(serializers.ModelSerializer):
class Meta:
fields = ("id", "owner", "title", "content") # 模型中需要序列化的字段
model = Note # 指定模型类
接下来我们会在 notes/views.py 中创建视图类:
# notes/views.py
from rest_framework import viewsets
from .models import Note
from .serializers import NoteSerializer
class NoteViewSet(viewsets.ModelViewSet):
queryset = Note.objects.all() # 指定 queryset
serializer_class = NoteSerializer # 指定序列化类
确保你已经以管理员身份成功登录后台, 然后访问 http://localhost:8000/api/notes/
你应该能看到已经创建的 notes 数据,还能添加新的 note 数据。
4. 认证和权限
为了便于研究认证和权限,需要在后台管理面板中创建新用户。(你只需要添加用户名和密码就可以了。)
然后在 project/urls.py 文件中的 urlpatterns 列表中添加以下内容:
path('auth/', include('rest_framework.urls')),
这样就能确保我们拥有可以使用 django_rest 登录的请求路径。你会在右上角看到一个根据用户名显示的向下的箭头:
点击它可以注销用户,也能切换用户。
现在有一个很明显的问题,就是说每个用户都能看到其他用户的 notes 数据。 更糟糕的是,未登录的用户也能看到甚至创建 note 数据。(你可以先登录然后访问 http://localhost:8000/api/notes/ 来验证这个问题。)
让我们来解决这个问题。
为什么未登录的用户也有权访问 notes 数据呢?因为我们在 rest_framework 设置了 AllowAny。所以需要进入 project/settings.py 文件,进行以下更改:
# project/settings.py
REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES":
["rest_framework.permissions.IsAuthenticated",], # 修改权限为认证过才能访问
"DEFAULT_PARSER_CLASSES":["rest_framework.parsers.JSONParser",],
}
然后尝试访问 http://localhost:8000/api/notes/
你应该无法看到任何 notes 数据了, 但是会收到一条消息:“detail”: “Authentication credentials were not provided.”(未提供认证凭据)
重新登录,你会看到 notes 相关数据。我们该如何解决登录用户可以查看其他用户的 notes 数据这一问题呢?
对 notes/views.py 文件做出以下更改:
# notes/views.py
from rest_framework import viewsets
from rest_framework import permissions
from .models import Note
from .serializers import NoteSerializer
from rest_framework.exceptions import PermissionDenied
class IsOwner 的类(permissions.BasePermission):,用于权限校验。这样就能确保
def has_object_permission(self, request, view, obj):
return obj.owner == request.user
class NoteViewSet(viewsets.ModelViewSet):
serializer_class = NoteSerializer
permission_classes = (IsOwner,)
# 确保用户只能看到自己的 Note 数据。
def get_queryset(self):
user = self.request.user
if user.is_authenticated:
return Note.objects.filter(owner=user)
raise PermissionDenied()
# 设置当前用户为 Note 对象的所有者
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
我们在类中移除了 queryset 属性并重写了 get_queryset 方法,这样就可以过滤 notes 数据,确保只返回属于当前用户的 note 数据。
此外我们添加了一个 IsOwner 类,用于权限校验。这样就能确保用户只能修改(更新/删除)他自己的相关数据。
最后我们重写了 perform_create 方法,这样就能保证创建一个新的 note 对象时,它的所有者始终是当前用户。
如果你访问 http://localhost:8000/api/notes/ ,那应该只能看到属于当前登录用户的 notes 数据。如果你尝试创建一个新的 note 对象,并将它的所有者设置为其他用户(非当前登录用户),也能成功创建 note 对象,但无论如何,这个 note 对象中的所有者仍然会是当前登录用户。
完成这些修改之后,由于用户只能处理自己的 note 数据,所以我们需要在序列化类中删除 owner 字段。对 notes/serializers.py 文件做出如下更改:
# notes/serializers.py
from rest_framework import serializers
from .models import Note
class NoteSerializer(serializers.ModelSerializer):
class Meta:
fields = ("id", "title", "content") # 删除 owner 字段
model = Note
我们现在已经实现了权限和认证功能,但 django_rest 仍然在使用 session 认证方式。
我们想要实现的是,对于任何客户端,无论是浏览器端还是其他客户端,都能够进行用户注册,登录,注销,在任何地方都能进行认证。为此,我们将使用 JWT 认证方式。
5. 加入 JWT
为了用JWT实现token(令牌)认证,我们将要用到一个库, Simple JWT:
$ pip install djangorestframework_simplejwt
然后我们需要在project/settings.py文件中,将其添加到认证类的列表中:
# project/settings.py
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
"DEFAULT_PARSER_CLASSES": [
"rest_framework.parsers.JSONParser",
],
"DEFAULT_AUTHENTICATION_CLASSES": # 新添加
"rest_framework.authentication.SessionAuthentication", # 新添加
"rest_framework_simplejwt.authentication.JWTAuthentication", # 新添加
],
}
并且添加一个新端点到project/urls.py文件中:
# project/urls.py
from django.contrib import admin
from django.urls import path, include
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('notes.urls')),
path('auth/', include('rest_framework.urls')),
path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), # 新添加
path('api/refresh/', TokenRefreshView.as_view(), name='token_refresh'), # 新添加
]
这些新的端点提供了用户登录所需要的一些东西。登录路由应当为/api/token/。而/api/refresh/路由则用于在旧的token(令牌)过期前获取新的token(令牌)。
事实上,我们并不需要为退出登录设置一个端点,因为服务器并不会维护这个状态。退出登录,我们只需要删除客户端上的token(令牌)值就行。token(令牌)就会"自动"过期失效(可以使用Simple JWT设置来设置时间)。
但是我们眼下还缺少的是注册用户和返回JWT token的方法。
用户注册
这不属于Notes应用,因为这是另一个域,即身份验证。 因此,我们将从创建一个新应用 jwtauth 开始:
$ python manage.py startapp jwtauth
将应用添加到 project/settings.py:
# project/settings.py
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"rest_framework",
"corsheaders",
"notes",
"jwtauth", # 新添加
]
接下来我们需要序列化User对象. 新建 jwtauth/serializers.py:
# jwtauth/serializers.py
from django.contrib.auth import get_user_model
from rest_framework import serializers
User = get_user_model()
class UserCreateSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True, required=True, style={
"input_type": "password"})
password2 = serializers.CharField(
style={"input_type": "password"}, write_only=True, label="Confirm password")
class Meta:
model = User
fields = [
"username",
"email",
"password",
"password2",
]
extra_kwargs = {"password": {"write_only": True}}
def create(self, validated_data):
username = validated_data["username"]
email = validated_data["email"]
password = validated_data["password"]
password2 = validated_data["password2"]
if (email and User.objects.filter(email=email).exclude(username=username).exists()):
raise serializers.ValidationError(
{"email": "Email addresses must be unique."})
if password != password2:
raise serializers.ValidationError(
{"password": "The two passwords differ."})
user = User(username=username, email=email)
user.set_password(password)
user.save()
return user
我们将使用Django自带的User模型,通过get_user_model()方法来调用。用这种方式导入是一个好的习惯,而不是直接导入User模型,因为这可以保证尽管我们已经对其进行了自定义,我们也能得到当前有效的用户模型。
我们也可以重写create()方法,并检验确认密码是否一致,以及没有其他用户使用同一个邮箱地址。
接下来我们要添加一个视图到 jwtauth/views.py 中:
# jwtauth/views.py
from django.contrib.auth import get_user_model
from rest_framework import permissions
from rest_framework import response, decorators, permissions, status
from rest_framework_simplejwt.tokens import RefreshToken
from .serializers import UserCreateSerializer
User = get_user_model()
@decorators.api_view(["POST"])
@decorators.permission_classes([permissions.AllowAny])
def registration(request):
serializer = UserCreateSerializer(data=request.data)
if not serializer.is_valid():
return response.Response(serializer.errors, status.HTTP_400_BAD_REQUEST)
user = serializer.save()
refresh = RefreshToken.for_user(user)
res = {
"refresh": str(refresh),
"access": str(refresh.access_token),
}
return response.Response(res, status.HTTP_201_CREATED)
这是我们首次使用基于view的函数视图而不是基于view的类视图。我们选择一个函数视图是因为它只需要响应POST这个http动词,并且我们使用了装饰器来确保它,以及设置了settings.py文件中定义的权限中的异常,以允许任何人仅访问此端点。
我们进行检查以查看序列化程序是否已验证了我们获得的数据,如果未返回,则返回错误对象。 如果一切正常,我们将保存序列化程序,该序列化程序将返回新创建的用户对象。 然后,我们可以获得该用户的JWT令牌并返回它。
我们需要创建一个urls文件,并向其中添加视图, jwtauth/urls.py:
# jwtauth/urls.py
from django.conf.urls import path
from .views import registration
urlpatterns = [
path('register/', registration, name='register')
]
最后,在project / urls.py中包含 jwtauth 路由:
# project/urls.py
from django.contrib import admin
from django.urls import path, include
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('notes.urls')),
path('auth/', include('rest_framework.urls')),
path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('api/jwtauth/', include('jwtauth.urls'), name='jwtauth'), # 新添加
]
就是这样,我们现在会有一个新的端点: http://localhost:8000/api/jwtaut/register/
我们可以在Postman中测试一下:
如果验证通过,它将返回带有刷新以及访问令牌的对象。
6. 添加 Swagger 文档
最后我们要做的事情就是添加Swagger文档,这样API用户才能看到哪些端点是可用的。
$ pip install django-rest-swagger
将其添加到project/settings.py的INSTALLED_APPS列表中。在REST_FRAMEWORK设置中还包括新的DEFAULT_SCHEMA_CLASS:
# project/settings.py
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"rest_framework",
"rest_framework_swagger", # 新添加
"corsheaders",
"notes",
"jwtauth",
]
REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated",],
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework_simplejwt.authentication.JWTAuthentication",
"rest_framework.authentication.SessionAuthentication",
],
"DEFAULT_PARSER_CLASSES": ["rest_framework.parsers.JSONParser",],
"DEFAULT_SCHEMA_CLASS": "rest_framework.schemas.coreapi.AutoSchema", # 新添加
}
也将其包含在你的project/urls.py文件中。最后我们还要做一些重构。现在刷新和令牌是单独的两个端点。因为他们都是与用户验证有关的,最好让他们都在jwtauth应用中。让我们将他们移到一起,同时也向文档添加一个路由。
# project/urls.py
from django.contrib import admin
from django.urls import path, include
from rest_framework_swagger.views import get_swagger_view
schema_view = get_swagger_view(title="Notes API")
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('notes.urls')),
path('auth/', include('rest_framework.urls')),
path('api/jwtauth/', include('jwtauth.urls'), name='jwtauth'),
path('api/docs/', schema_view),
]
然后将刷新和令牌放到jwtauth/urls.py中:
# jwtauth/urls.py
from django.urls import path
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
from .views import registration
urlpatterns = [
path("register/", registration, name="register"),
path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
path("refresh/", TokenRefreshView.as_view(), name="token_refresh"),
]
去 http://localhost:8000/api/docs/ 看到完整的API列表. 你也应该能看到令牌端点现在属于/jwtauth组中。
就是这样。你现在拥有一个功能齐全的Notes API了,客户端可以从任何一个地方使用它。