前言
在前后端分离开发时为什么需要用户认证呢?原因是由于HTTP是无状态的协议,当我们通过帐号密码验证一个用户时,下一个request请求时就把刚刚的用户忘了。于是我们就无法确认该用户的情况,就要再验证一次。所以为了保证系统安全,我们就需要验证用户否处于登录状态。
传统方式
前后端分离通过Restful API进行数据交互时,如何验证用户的登录信息及权限。在原来的项目中,使用的是最传统也是最简单的方式,前端登录,后端根据用户信息生成一个token,并保存这个token 和对应的用户id到数据库或缓存Session中,接着把token传给用户,存入浏览器 cookie,之后浏览器请求带上这个cookie,后端根据这个cookie值来查询用户,验证是否失效。
这样会遇到的问题:
- 如果我们的页面出现了 XSS 漏洞,由于 cookie 可以被 JavaScript 读取,XSS 漏洞会导致用户 token 泄露,cookie 的泄露意味着用户信息不再安全。尽管我们通过转义输出内容,使用 CDN 等可以尽量避免 XSS 注入,但谁也不能保证在大型的项目中不会出现这个问题。还可以设置 httpOnly 以及 secure 项。设置 httpOnly 后 cookie 将不能被 JS 读取,浏览器会自动的把它加在请求的 header 当中,设置 secure 的话,cookie 就只允许通过 HTTPS 传输。secure 选项可以过滤掉一些使用 HTTP 协议的 XSS 注入,但并不能完全阻止。
- 如果将验证信息保存在数据库中,后端每次都需要根据token查出用户id,这就增加了数据库的查询和存储开销。若把验证信息保存在session中,有加大了服务器端的存储压力
PS: 在前后端分离的项目中SessionAuthentication这种验证模式比较少见,所以本文不做介绍
Json Web Token(JWT)
JWT 是一个开放标准(RFC 7519),它定义了一种用于简洁,自包含的用于通信双方之间以 JSON 对象的形式安全传递信息的方法。JWT 可以使用 HMAC 算法或者是 RSA 的公钥密钥对进行签名。
它具备两个特点:
- 简洁(Compact):可以通过URL, POST 参数或者在 HTTP header 发送,因为数据量小,传输速度快;
- 自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库;
使用 DRF 自带的用户体系
设置身份验证方案
可以使用DEFAULT_AUTHENTICATION_CLASSES设置全局设置默认认证方案
""" 可根据自己的使用情况,删除不需要的认证方式 """
""" 使用全局配置的话,是每个接口都要求认证 """
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication',
)
}
还可以使用APIView基于类的视图在每个视图或每个视图集的基础上设置身份验证方案
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,)
def get(self, request, format=None):
content = {
'user': unicode(request.user), # `django.contrib.auth.User` instance.
'auth': unicode(request.auth), # None
}
return Response(content)
BasicAuthentication
此身份验证方案使用HTTP基本身份验证,根据用户的用户名和密码进行签名
此身份验证通常仅适用于测试
注意:如果您BasicAuthentication在生产中使用,则必须确保您的API仅可用https。您还应确保您的API客户端始终在登录时重新请求用户名和密码,并且永远不会将这些详细信息存储到持久存储中
如果已成功通过身份验证,请BasicAuthentication提供以下凭据:
- request.user将是一个Django User实例
- request.auth会的None
TokenAuthentication(重要)
此身份验证方案使用简单的基于令牌的HTTP身份验证方案。令牌认证适用于客户端 - 服务器设置,例如本机桌面和移动客户端。
要使用该TokenAuthentication方案,您需要配置要包含的身份验证类TokenAuthentication,并rest_framework.authtoken在您的INSTALLED_APPS设置中另外包含:
""" 此验证方法会建立一张独立的数据表,添加完该配置之后需要 migrate 一下 """
INSTALLED_APPS = (
...
'rest_framework.authtoken'
)
使用时TokenAuthentication,您可能希望为客户端提供一种机制,以获取给定用户名和密码的令牌。REST框架提供了一个内置视图来提供此行为。要使用它,请将obtain_auth_token视图添加到URLconf
from rest_framework.authtoken import views
urlpatterns += [
url('api-token-auth/', views.obtain_auth_token)
]
----------------------------
""" 请求 token 方法 """
POST: http://127.0.0.1/api-token-auth/
Content: {"username": "admin", "password": "123456"}
Response: {"token": "adwsdfrt4353425456yjghvb"}
服务端主动为用户创建token
from rest_framework.authtoken.models import Token
token = Token.objects.create(user=...)
print(token.key)
token 应包含在Authorization HTTP Header 中
Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b
注意:如果要在标题中使用不同的关键字,例如Bearer,只需子类化TokenAuthentication并设置keyword类变量。
如果已成功通过身份验证,TokenAuthentication提供以下凭据:
- request.user将是一个
Django User实例 - request.auth将是一个
rest_framework.authtoken.models.Token实例
拒绝许可的未经身份验证的响应将导致HTTP 401 Unauthorized使用适当的WWW-Authenticate标头进行响应
WWW-Authenticate: Token
注意:如果您TokenAuthentication在生产中使用,则必须确保您的API仅可用https
DRF自带的token验证的不足
- DRF的token存在数据库中,如果是分布式系统或者两套系统,想要使用同一套验证系统就会出现问题,因为数据是放在一台服务器上
- 没有过期时间,是长期有效,如果泄露,就会存在安全隐患
JWT(json web token)简介
JWT组成(一) Header 头部
加密算法:base64
头部包含了两部分,token 类型和采用的加密算法
base64enc({
"alg": "HS256", // 采用的加密算法
"typ": "JWT" // token类型
})
JWT组成(二) Payload 负载
加密算法:base64
存放信息的模块
{
"iss": "Actoress", // 签发者
"iat": 1441593502, // 签发时间
"exp": 1441594722, // 过期时间
"aud": "www.example.com", // 接收方
"sub": "1234568@163.com" // 面向的用户
}
JWT组成(三) Signature 签名
前面两部分都是使用 Base64 进行编码的,即前端可以解开知道里面的信息。Signature 需要使用编码后的 header 和 payload 以及我们提供的一个密钥,然后使用 header 中指定的签名算法(HS256)进行签名。签名的作用是保证 JWT 没有被篡改过。
三个部分通过.连接在一起就是我们的 JWT 了,它可能长这个样子,长度貌似和你的加密算法和私钥有关系。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjU3ZmVmMTY0ZTU0YWY2NGZmYzUzZGJkNSIsInhzcmYiOiI0ZWE1YzUwOGE2NTY2ZTc2MjQwNTQzZjhmZWIwNmZkNDU3Nzc3YmUzOTU0OWM0MDE2NDM2YWZkYTY1ZDIzMzBlIiwiaWF0IjoxNDc2NDI3OTMzfQ.PA3QjeyZSUh7H0GfE0vJaKW4LjKJuC3dVLQiY4hii8s
JWT流程

在DRF中使用JWT
这里使用 DRF 官方文档推荐的第三方包django-rest-framework-simplejwt
1.安装
pip install djangorestframework_simplejwt
2.添加配置
在settings.py添加rest_framework_simplejwt.authentication.JWTAuthentication到身份验证类列表
""" settings.py """
REST_FRAMEWORK = {
...
'DEFAULT_AUTHENTICATION_CLASSES': (
...
'rest_framework_simplejwt.authentication.JWTAuthentication',
)
...
}
此外,在您的根urls.py文件(或任何其他URL配置)中,包括Simple JWT TokenObtainPairView 和 TokenRefreshView views的路由:
""" urls.py """
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
urlpatterns = [
...
url(r'^api/token/$', TokenObtainPairView.as_view(), name='token_obtain_pair'),
url(r'^api/token/refresh/$', TokenRefreshView.as_view(), name='token_refresh'),
...
]
TokenVerifyView如果您希望允许API用户验证HMAC签名的令牌而无需访问您的签名密钥,您还可以包含Simple JWT的路由:
urlpatterns = [
...
url(r'^api/token/verify/$', TokenVerifyView.as_view(), name='token_verify'),
...
]
3.JWT配置
这里显示了这些设置的默认值
""" settings.py """
from datetime import timedelta
...
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5),
'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
'ROTATE_REFRESH_TOKENS': False,
'BLACKLIST_AFTER_ROTATION': True,
'ALGORITHM': 'HS256',
'SIGNING_KEY': settings.SECRET_KEY,
'VERIFYING_KEY': None,
'AUTH_HEADER_TYPES': ('Bearer',),
'USER_ID_FIELD': 'id',
'USER_ID_CLAIM': 'user_id',
'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
'TOKEN_TYPE_CLAIM': 'token_type',
'JTI_CLAIM': 'jti',
'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp',
'SLIDING_TOKEN_LIFETIME': timedelta(minutes=5),
'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1),
}
ACCESS_TOKEN_LIFETIME
datetime.timedelta对象,它指定访问令牌的有效期。timedelta在令牌生成期间,此值将添加到当前UTC时间,以获取令牌的默认“exp”声明值。
REFRESH_TOKEN_LIFETIME
datetime.timedelta对象,指定刷新令牌有效的时间。timedelta在令牌生成期间,此值将添加到当前UTC时间,以获取令牌的默认“exp”声明值。
ROTATE_REFRESH_TOKENS
设置True为时,如果提交TokenRefreshView了刷新令牌,则将返回新的刷新令牌以及新的访问令牌。这个新的刷新令牌将通过JSON响应中的“刷新”键提供。新的刷新令牌将具有更新的到期时间,该时间是通过将REFRESH_TOKEN_LIFETIME 设置中的timedelta添加到请求的当前时间来确定的。如果黑名单应用程序正在使用且BLACKLIST_AFTER_ROTATION设置设置为True,则提交到刷新视图的刷新令牌将添加到黑名单中。
BLACKLIST_AFTER_ROTATION
设置为True时,如果黑名单应用程序正在使用且设置设置为True,则会将提交的刷新令牌 TokenRefreshView添加到黑名单ROTATE_REFRESH_TOKENS中True。
ALGORITHM
来自PyJWT库的算法将用于对令牌执行签名/验证操作。使用对称的HMAC签名和验证,可以使用以下算法:'HS256','HS384', 'HS512'。选择HMAC算法时,该SIGNING_KEY设置将用作签名密钥和验证密钥。在这种情况下,该VERIFYING_KEY设置将被忽略。使用非对称RSA签名和验证,下面的算法可用于:'RS256', 'RS384','RS512'。选择RSA算法时, SIGNING_KEY必须将设置设置为包含RSA私钥的字符串。同样,VERIFYING_KEY必须将设置设置为包含RSA公钥的字符串
SIGNING_KEY
签名密钥,用于签署生成的令牌的内容。对于HMAC签名,这应该是一个随机字符串,其中至少包含签名协议所需的数据位。对于RSA签名,这应该是一个包含2048位或更长的RSA私钥的字符串。由于Simple JWT默认使用256位HMAC签名,因此 SIGNING_KEY设置默认SECRET_KEY为django项目的设置值。虽然这是Simple JWT可以提供的最合理的默认值,但建议开发人员将此设置更改为独立于django项目密钥的值。这将使得在受到攻击时更改用于令牌的签名密钥变得更容易。
VERIFYING_KEY
验证密钥,用于验证生成的令牌的内容。如果ALGORITHM设置指定了HMAC算法, VERIFYING_KEY则将忽略该SIGNING_KEY设置并使用该设置的值 。如果ALGORITHM设置指定了RSA算法,则VERIFYING_KEY必须将该设置设置为包含RSA公钥的字符串。
AUTH_HEADER_TYPES
对于需要身份验证的视图,将接受的授权标头类型。例如,值'Bearer'表示需要身份验证的视图将查找具有以下格式的标头: Authorization: Bearer 。此设置还可能包含可能的标头类型的列表或元组(例如('Bearer', 'JWT'))。如果以这种方式使用列表或元组,并且身份验证失败,则集合中的第一个项将用于在响应中构建“WWW-Authenticate”标头。
USER_ID_FIELD
来自用户模型的数据库字段,该字段将包含在生成的标记中以标识用户。建议此设置的值指定一旦选择初始值后通常不会更改的字段。例如,指定“用户名”或“电子邮件”字段将是一个糟糕的选择,因为帐户的用户名或电子邮件可能会更改,具体取决于给定服务中的帐户管理的设计方式。这可以允许使用旧用户名创建新帐户,同时现有令牌仍然有效,其使用该用户名作为用户标识符。
USER_ID_CLAIM
生成的令牌中的声明将用于存储用户标识符。例如,设置值'user_id'意味着生成的令牌包括包含用户标识符的“user_id”声明。
AUTH_TOKEN_CLASSES
指向允许证明身份验证的令牌类型的类的点路径列表。有关此内容的更多信息,请参阅下面的“令牌类型”部分。
TOKEN_TYPE_CLAIM
用于存储令牌类型的声明名称。有关此内容的更多信息,请参阅下面的“令牌类型”部分。
JTI_CLAIM
声明名称,用于存储令牌的唯一标识符。此标识符用于标识黑名单应用中的已撤销令牌。在某些情况下,除了默认的“jti”声明之外,可能还需要使用另一个声明来存储这样的值。
SLIDING_TOKEN_LIFETIME
一个datetime.timedelta对象,它指定滑动令牌的有效时间以证明身份验证。timedelta在令牌生成期间,此值将添加到当前UTC时间,以获取令牌的默认“exp”声明值。有关此内容的更多信息,请参阅下面的“滑动令牌”部分。
SLIDING_TOKEN_REFRESH_LIFETIME
一个datetime.timedelta对象,指定滑动令牌有效刷新的时间。timedelta在令牌生成期间,此值将添加到当前UTC时间,以获取令牌的默认“exp”声明值。有关此内容的更多信息,请参阅下面的“滑动令牌”部分。
SLIDING_TOKEN_REFRESH_EXP_CLAIM
声明名称,用于存储滑动令牌刷新周期的exipration时间。有关此内容的更多信息,请参阅下面的“滑动令牌”部分。