认证(Authentication)与权限(Permission)不是一回事。认证是通过用户提供的用户ID/密码组合或者Token来验证用户的身份。权限(Permission)的校验发生验证用户身份以后,是由系统根据分配权限确定用户可以访问何种资源以及对这种资源进行何种操作,这个过程也被称为授权(Authorization)。
无论是Django还是DRF, 当用户成功通过身份验证以后,系统会把已通过验证的用户对象与request请求绑定,这样一来你就可以使用request.user获取这个用户对象的所有信息了。
在前面的教程中我们编写ArticleList和ArticleView两个基于类的视图(如下所示)。前者如果收到GET请求会返回文章资源列表,如果收到POST请求则添加文章;后者如果收到GET请求就返回单篇文章资源,如果收到PUT或DELETE请求,就对文章资源进行修改或删除。
# coding=utf-8
from .models import Article
from .serializers import ArticleSerializer
# generic class-based views
from rest_framework import generics
class ArticleList(generics.ListCreateAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
# 将request.user与author绑定
def perform_create(self, serializer):
serializer.save(author=self.request.user)
class ArticleDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Article.objects.all()
serializer_class =ArticleSerializer
以上两个视图其实是有很大问题的,因为任何用户包括匿名用户也可以对文章资源进行修改。比如当你访问单篇文章资源时,你不仅可以看到红色的delete按钮和修改文章内容的表单,而且可以在未登录的情况对它们进行操作。
在Django传统视图开发中你可能会使用@login_required和@permission_required这样的装饰器要求用户先登录或进行权限验证。在DRF中你不需要做,这是因为REST framework 包含许多默认权限类,我们可以用来限制谁可以访问给定的视图。在这种情况下,我们需要的是 IsAuthenticatedOrReadOnly 类,它将确保经过身份验证的请求获得读写访问权限,未经身份验证的请求将获得只读读的权限。
现在修改我们的视图(zlblog/views.py),添加如下代码:
from rest_framework import generics
from rest_framework import permissions
from .permissions import IsOwnerOrReadOnly
class ArticleList(generics.ListCreateAPIView):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
# important
def perform_create(self, serializer):
serializer.save(author=self.request.user)
class ArticleDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Article.objects.all()
serializer_class =ArticleSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
此时再访问文章资源列表或单篇文章资源时,你会看到红色的delete按钮和添加修改表单都已消失。当你重新登录验证身份后,你又可以看到delete按钮和修改表单了。
你可能会问,DRF中用户应该访问哪个url登录验证身份呢? 是admin吗?当然不是admin页面,因为只有管理员才能通过admin页面登录。DRF中你可以将登录页面api-auth添加到你的项目urls中,如下所示:
#apiproject/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('v1/', include('zlblog.urls')),#包含zlblog中的urls
path('api-auth/', include('rest_framework.urls')), # 用户登录页面
]
然后访问http://127.0.0.1:8000/api-auth/login/你就可以看到专门的DRF的登录页面了,如下所示:
除了IsAuthenticatedOrReadOnly 类,DRF自带的常用权限类还包括:
IsAuthenticated类:仅限已经通过身份验证的用户访问;
AllowAny类:允许任何用户访问;
IsAdminUser类:仅限管理员访问;
DjangoModelPermissions类:只有在用户经过身份验证并分配了相关模型权限时,才会获得授权访问相关模型。
DjangoModelPermissionsOrReadOnly类:与前者类似,但可以给匿名用户访问API的可读权限。
DjangoObjectPermissions类:只有在用户经过身份验证并分配了相关对象权限时,才会获得授权访问相关对象。通常与django-gaurdian联用实现对象级别的权限控制。
IsAuthenticatedOrReadOnly 类并不能实现只有文章 article 的创建者才可以更新或删除它,这时我们还需要自定义一个名为IsOwnerOrReadOnly 的权限类,把它加入到ArticleDetail视图里。
首先我们在zlblog文件夹下创建permissions.py,添加如下代码:
# coding=utf-8
from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission):
"""
自定义权限只允许对象的创建者才能编辑它。
"""
def has_object_permission(self, request, view, obj):
# 读取权限被允许用于任何请求,
# 所以我们始终允许 GET,HEAD 或 OPTIONS 请求。
if request.method in permissions.SAFE_METHODS:
return True
# 写入权限只允许给 article 的作者。
return obj.author == request.user
然后修改我们的视图,IsOwnerOrReadOnly 的权限类,把它加入到ArticleDetail视图的permission_classes里。这样就完美实现了文初我们想要实现的三个功能。DRF支持权限类的插拔,是不是很帅?
在前面的案例中,我们都是在基于类的API视图里通过permission_classes属性设置的权限类。如果你有些权限是全局或全站通用的,你还可以在settings.py中使用 DEFAULT_PERMISSION_CLASSES 全局设置默认权限策略。
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
)
}
如果未指定,则此设置默认为允许无限制访问:
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.AllowAny',
)
如果你习惯使用基于函数的视图编写API,你可以按如下方式给你的函数视图添加权限。
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
@api_view(['GET'])
@permission_classes((IsAuthenticated, ))
def example_view(request, format=None):
content = {
'status': 'request was permitted'
}
return Response(content)
注意:当通过类属性或装饰器设置新的权限类时,您会告诉视图忽略 settings.py 文件上设置的默认列表。
身份验证是将传入的请求对象(request)与一组标识凭据(例如请求来自的用户或其签名的令牌token)相关联的机制。REST framework 提供了一些开箱即用的身份验证方案,并且还允许你实现自定义方案。
DRF的每个认证方案实际上是一个类。你可以在视图中使用一个或多个认证方案类。REST framework 将尝试使用列表中的每个类进行身份验证,并使用成功完成验证的第一个类的返回的元组设置 request.user 和request.auth。
用户通过认证后request.user返回Django的User实例,否则返回AnonymousUser的实例。request.auth通常为None。如果使用token认证,request.auth可以包含认证过的token。
Session认证SessionAuthentication类:此认证方案使用Django的默认session后端进行身份验证。当客户端发送登录请求通过验证后,Django通过session将用户信息存储在服务器中保持用户的请求状态。Session身份验证适用于与你的网站在相同的Session环境中运行的AJAX客户端 (注:这也是Session认证的最大弊端)。
基本认证BasicAuthentication类:此认证方案使用HTTP 基本认证,针对用户的用户名和密码进行认证。使用这种方式后浏览器会跳出登录框让用户输入用户名和密码认证。基本认证通常只适用于测试。
远程认证RemoteUserAuthentication类:此认证方案为用户名不存在的用户自动创建用户实例。这个很少用,具体见文档。
Token认证TokenAuthentication类:该认证方案是DRF提供的使用简单的基于Token的HTTP认证方案。当客户端发送登录请求时,服务器便会生成一个Token并将此Token返回给客户端,作为客户端进行请求的一个标识以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码。后面我们会详细介绍如何使用这种认证方案。
注意:如果你在生产环境下使用BasicAuthentication和TokenAuthentication认证,你必须确保你的API仅在https可用。
如何在DRF中使用你的认证方案 ?
方式1:settings.py中设置默认的全局认证方案
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
)}
**方式2:基于类的视图(CBV)中使用-authentication_classes **
from rest_framework.authentication import SessionAuthentication, BasicAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
class ExampleView(APIView):
authentication_classes = (SessionAuthentication, BasicAuthentication)
permission_classes = (IsAuthenticated,)
方式3:函数视图中使用
@api_view(['GET'])
@authentication_classes((SessionAuthentication, BasicAuthentication))
@permission_classes((IsAuthenticated,))
def example_view(request, format=None):
content = {
'user': unicode(request.user), # `django.contrib.auth.User` 实例。
'auth': unicode(request.auth), # None
}
return Response(content)
如何自定义认证方案?
要实现自定义的认证方案,要继承BaseAuthentication类并且重写.authenticate(self, request)方法。如果认证成功,该方法应返回(user, auth)的二元元组,否则返回None。
以下示例将以自定义请求标头中名称为’X_USERNAME’提供的用户名作为用户对任何传入请求进行身份验证,其它类似自定义认证需求比如支持用户同时按用户名或email进行验证。
from django.contrib.auth.models import User
from rest_framework import authentication
from rest_framework import exceptions
class ExampleAuthentication(authentication.BaseAuthentication):
def authenticate(self, request):
username = request.META.get('X_USERNAME')
if not username:
return None
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
raise exceptions.AuthenticationFailed('No such user')
return (user, None)
DRF自带的TokenAuthentication方案可以实现基本的token认证,整个流程如下:
首先,你需要将修改settings.py, 加入如下app。
INSTALLED_APPS = (
...
'rest_framework.authtoken'
)
后文再单独讲解一个例子。
然而JSON Web Token(JWT)是一种更新的使用token进行身份认证的标准。与DRF内置的TokenAuthentication方案不同,JWT身份验证不需要使用数据库来验证令牌, 而且可以轻松设置token失效期或刷新token, 是API开发中当前最流行的跨域认证解决方案。本文将详细介绍JWT认证的工作原理以及如何通过djangorestframework-simplejwt 这个第三方包轻松实现JWT认证。
JSON Web Token(JWT)是一种开放标准,它定义了一种紧凑且自包含的方式,用于各方之间安全地将信息以JSON对象传输。由于此信息是经过数字签名的,因此可以被验证和信任。JWT用于为应用程序创建访问token,通常适用于API身份验证和服务器到服务器的授权。那么如何理解紧凑和自包含这两个词的含义呢?
那么JWT认证是如何工作的呢? 首先客户端提交用户登录信息验证身份通过后,服务器生成一个用于证明用户身份的令牌(token),也就是一个加密后的长字符串,并将其发送给客户端。在后续请求中,客户端以各种方式(比如通过url参数或者请求头)将这个令牌发送回服务器,服务器就知道请求来自哪个特定身份的用户了。
JSON Web Token由三部分组成,这些部分由点(.)分隔,分别是header(头部),payload(有效负载)和signature(签名)。
header(头部): 识别以何种算法来生成签名;
pyload(有效负载): 用来存放实际需要传递的数据;
signature(签名): 安全验证token有效性,防止数据被篡改。
通过http传输的数据实际上是加密后的JWT,它是由两个点分割的base64-URL长字符串组成,解密后我们可以得到header, payload和signature三部分。我们可以简单的使用 https://jwt.io/ 官网来生成或解析一个JWT,如下所示:
接下来我们将使用django-rest-framework-simplejwt这个第三方软件包进行JWT身份验证。
django-rest-framework-simplejwt为Django REST框架提供了JSON Web令牌认证后端。它提供一组保守的默认功能来涵盖了JWT的最常见用例。它还非常容易扩展。
pip install djangorestframework-simplejwt
首先,我们需要告诉DRF我们使用jwt认证作为后台认证方案。修改apiproject/settings.py:
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend'
],
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
}
其次,我们需要提供用户可以获取和刷新token的urls地址,这两个urls分别对应TokenObtainPairView和TokenRefreshView两个视图。
# coding=utf-8
# apiproject/urls.py
from django.contrib import admin
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from django.conf import settings
from django.conf.urls.static import static
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
urlpatterns = [
path('admin/', admin.site.urls),
path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('v1/', include('zlblog.urls')), #包含其它页面的路由地址
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
最后,我们可以开始使用postman测试了。通过POST方法发送登录请求到http://127.0.0.1:8000/token/, 请求数据包括username和password。如果登录成功,你将得到两个长字符串,一个是access token(访问令牌),还有一个是refresh token(刷新令牌),如下所示:
但是,这里引发了跨域问题。安装跨越库文件:
pip install django-cors-headers
修改setting.py文件,有两处,如下所示:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'corsheaders', # 注册跨域app
'zlblog',
'rest_framework.authtoken'
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware', # 跨域中间件
'django.middleware.locale.LocaleMiddleware', #支持中文语言
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
然后再用postman仿真就可以得到结果:
修改上文有一个受保护的视图(比如这里的/articles/),权限(permission_classes)是IsAuthenticated,只有验证用户才可访问。如果未授权,返回如下结果:
访问这个保护视图时你只需要在请求头的Authorization选项里输入你刚才获取的access token即可,如下所示:
DRF接口会自定验证token的有效性。
不给这个access token默认只有5分钟有效。5分钟过后,当你再次访问保护视图时,你将得到如下token已失效或过期的提示。
那么问题来了,Simple JWT中的access token默认有效期是5分钟,那么refresh token默认有效期是多长呢? 答案是24小时。
Simple JWT的默认设置如下所示:
# JWT配置
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5), # Access Token的有效期
'REFRESH_TOKEN_LIFETIME': timedelta(days=7), # Refresh Token的有效期
# 对于大部分情况,设置以上两项就可以了,以下为默认配置项目,可根据需要进行调整
# 是否自动刷新Refresh Token
'ROTATE_REFRESH_TOKENS': False,
# 刷新Refresh Token时是否将旧Token加入黑名单,如果设置为False,则旧的刷新令牌仍然可以用于获取新的访问令牌。需要将'rest_framework_simplejwt.token_blacklist'加入到'INSTALLED_APPS'的配置中
'BLACKLIST_AFTER_ROTATION': False,
'ALGORITHM': 'HS256', # 加密算法
'SIGNING_KEY': settings.SECRET_KEY, # 签名密匙,这里使用Django的SECRET_KEY
# 如为True,则在每次使用访问令牌进行身份验证时,更新用户最后登录时间
"UPDATE_LAST_LOGIN": False,
# 用于验证JWT签名的密钥返回的内容。可以是字符串形式的密钥,也可以是一个字典。
"VERIFYING_KEY": "",
"AUDIENCE": None,# JWT中的"Audience"声明,用于指定该JWT的预期接收者。
"ISSUER": None, # JWT中的"Issuer"声明,用于指定该JWT的发行者。
"JSON_ENCODER": None, # 用于序列化JWT负载的JSON编码器。默认为Django的JSON编码器。
"JWK_URL": None, # 包含公钥的URL,用于验证JWT签名。
"LEEWAY": 0, # 允许的时钟偏差量,以秒为单位。用于在验证JWT的过期时间和生效时间时考虑时钟偏差。
# 用于指定JWT在HTTP请求头中使用的身份验证方案。默认为"Bearer"
"AUTH_HEADER_TYPES": ("Bearer",),
# 包含JWT的HTTP请求头的名称。默认为"HTTP_AUTHORIZATION"
"AUTH_HEADER_NAME": "HTTP_AUTHORIZATION",
# 用户模型中用作用户ID的字段。默认为"id"。
"USER_ID_FIELD": "id",
# JWT负载中包含用户ID的声明。默认为"user_id"。
"USER_ID_CLAIM": "user_id",
# 用于指定用户身份验证规则的函数或方法。默认使用Django的默认身份验证方法进行身份验证。
"USER_AUTHENTICATION_RULE": "rest_framework_simplejwt.authentication.default_user_authentication_rule",
# 用于指定可以使用的令牌类。默认为"rest_framework_simplejwt.tokens.AccessToken"。
"AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",),
# JWT负载中包含令牌类型的声明。默认为"token_type"。
"TOKEN_TYPE_CLAIM": "token_type",
# 用于指定可以使用的用户模型类。默认为"rest_framework_simplejwt.models.TokenUser"。
"TOKEN_USER_CLASS": "rest_framework_simplejwt.models.TokenUser",
# JWT负载中包含JWT ID的声明。默认为"jti"。
"JTI_CLAIM": "jti",
# 在使用滑动令牌时,JWT负载中包含刷新令牌过期时间的声明。默认为"refresh_exp"。
"SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp",
# 滑动令牌的生命周期。默认为5分钟。
"SLIDING_TOKEN_LIFETIME": timedelta(minutes=5),
# 滑动令牌可以用于刷新的时间段。默认为1天。
"SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1),
# 用于生成访问令牌和刷新令牌的序列化器。
"TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainPairSerializer",
# 用于刷新访问令牌的序列化器。默认
"TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSerializer",
# 用于验证令牌的序列化器。
"TOKEN_VERIFY_SERIALIZER": "rest_framework_simplejwt.serializers.TokenVerifySerializer",
# 用于列出或撤销已失效JWT的序列化器。
"TOKEN_BLACKLIST_SERIALIZER": "rest_framework_simplejwt.serializers.TokenBlacklistSerializer",
# 用于生成滑动令牌的序列化器。
"SLIDING_TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainSlidingSerializer",
# 用于刷新滑动令牌的序列化器。
"SLIDING_TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSlidingSerializer",
}
如果要覆盖Simple JWT的默认设置,可以修改settings.py, 如下所示。下例将refresh token的有效期改为了15天。
from datetime import timedelta
SIMPLE_JWT = {
'REFRESH_TOKEN_LIFETIME': timedelta(days=15),
'ROTATE_REFRESH_TOKENS': True,
}
如果你对Simple JWT返回的access token进行解码,你会发现这个token的payload数据部分包括token类型,token失效时间,jti(一个类似随机字符串)和user_id。如果你希望在payload部分提供更多信息,比如用户的username,这时你就要自定义令牌(token)了。
首先,编写你的zlblog/seralizers.py,添加如下代码。该序列化器继承了TokenObtainPairSerializer类。
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
class MyTokenObtainPairSerializer(TokenObtainPairSerializer):
@classmethod
def get_token(cls, user):
token = super(MyTokenObtainPairSerializer, cls).get_token(user)
# Add custom claims
token['username'] = user.username
return token
其次,不使用Simple JWT提供的默认视图,使用自定义视图。修改zlblog/views.py, 添加如下代码:
from rest_framework_simplejwt.views import TokenObtainPairView
from rest_framework.permissions import AllowAny
from .serializers import MyTokenObtainPairSerializer
class MyObtainTokenPairView(TokenObtainPairView):
permission_classes = (AllowAny,)
serializer_class = MyTokenObtainPairSerializer
最后,修改zlblog/urls.py, 添加如下代码,将/token/指向新的自定义的视图。注意:本例中的app名为zlblog,所以是从zlblog.views导入的MyObtainTokenPairView。
# coding=utf-8
from django.contrib import admin
from django.urls import path, include
from zlblog.views import MyObtainTokenPairView
from rest_framework.routers import DefaultRouter
from django.conf import settings
from django.conf.urls.static import static
from rest_framework_simplejwt.views import TokenRefreshView
urlpatterns = [
path('admin/', admin.site.urls),
path('token/', MyObtainTokenPairView.as_view(), name='token_obtain_pair'),#自定义token
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('v1/', include('zlblog.urls')), #包含其它页面的路由地址
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
重新发送POST请求到/token/,你将获得新的access token和refresh token。采用 https://jwt.io/ 解码,在负载部分将得到新的字段名:
对重新获取的access token进行解码,你将看到payload部分多了username的内容,是不是很酷? 在实际API开发过程中,通过Json Web Token传递更多数据非常有用。
上面的演示案例是通过用户名和密码登录的,如果我们希望后台同时支持邮箱/密码,手机/密码组合登录怎么办? 这时我们还需要自定义认证后台(Backend)。
首先,修改zlblog/views.py, 添加如下代码:
# coding=utf-8
from django.contrib.auth.backends import ModelBackend
#django的Q对象将SQL表达式封装在Python对象中,该对象可用于与数据库相关的操作。使用Q对象,我们可以使用更少和更简单的代码进行复杂查询。
from django.db.models import Q
from django.contrib.auth import get_user_model
User = get_user_model()
class MyCustomBackend(ModelBackend):
def authenticate(self, request, username=None, password=None, **kwargs):
try:
user = User.objects.get(Q(username=username) | Q(email=username) )
if user.check_password(password):
return user
except Exception as e:
return None
其次,修改zlblog/settings.py, 把你自定义的验证方式添加到AUTHENTICATION_BACKENDS里去。
AUTHENTICATION_BACKENDS = (
'zlblog.views.MyCustomBackend',
)
修改好后,你使用postman发送邮箱/密码组合到/token/,将同样可以获得access token和refresh token。是不是又学到了一招?