Django REST JWT
阅读本文之前,先阅读:
https://www.ywnds.com/?p=14967
本文假设你已经会使用DRF和JWT插件。
预备知识点:
- DRF 提供了几个验证登陆和权限的方式
- 在每次调用视图前DRF会检查授权和权限
- 你可以自己定义验证类,authenticate方法一般需要返回一个user和token。
Demo
class Authentication(BaseAuthentication): # 可以不继承 但是要设置权限为【】
'''认证'''
def authenticate(self, request):
token = request._request.GET.get('token')
token_obj = models.UserToken.objects.filter(token=token).first()
if not token_obj:
raise exceptions.AuthenticationFailed('用户认证失败')
# 在rest framework内部会将这两个字段赋值给request,以供后续操作使用 req.user, req.auth
return (token_obj.user, token_obj)
def authenticate_header(self, request):
pass
JWT,你获取到的Token由header+payload+signature 三者通过点.
组成。
我们要清楚的是,header和payload是base64编码得到的,所以不能存储敏感信息。但是signature是需要前两者一起签名的。签名还需要后端一个密钥,在DRF 的JWT中默认和django的SECRET_KEY相同。也就是说,别人拿不到这个SECRET_KEY,你知道了header和payload也没用,因为你没办法伪造没办法知道SECRET_KEY。
那么怎么验证当前用户已经登陆了?
在payload里面存储用户唯一标识,比如用户唯一id,唯一用户名。后端根据SECRET_KEY校验你的token是不是合法的,合法,得到用户id,从数据库里面查找,返回一个User实例。在DRF的JWT中,
JSONWebTokenAPIView默认的用户校验是基于Django的User模型。
首先,来看一下是如何获得用户Token的, 当你搭建好JWT,你可以
$ curl -X POST -d "username=admin&password=admin123456" http://127.0.0.1:8000/api-token-auth/
你就可以获得一个token, 我们可以看到,最少要两个字段,password和username。那么是哪里定义了这些字段呢?
先看一下是如何获取Token的APIView,查看ObtainJSONWebToken类,
class ObtainJSONWebToken(JSONWebTokenAPIView):
"""
API View that receives a POST with a user's username and password.
Returns a JSON Web Token that can be used for authenticated requests.
"""
serializer_class = JSONWebTokenSerializer
继承自JSONWebTokenAPIView
,并且serializer_class
为JSONWebTokenSerializer
。下面先来看JSONWebTokenSerializer
。
class JSONWebTokenSerializer(Serializer):
"""
Serializer class used to validate a username and password.
'username' is identified by the custom UserModel.USERNAME_FIELD.
Returns a JSON Web Token that can be used to authenticate later calls.
"""
def __init__(self, *args, **kwargs):
"""
Dynamically add the USERNAME_FIELD to self.fields.
"""
super(JSONWebTokenSerializer, self).__init__(*args, **kwargs)
self.fields[self.username_field] = serializers.CharField()
self.fields['password'] = PasswordField(write_only=True)
@property
def username_field(self):
return get_username_field()
def validate(self, attrs):
credentials = {
self.username_field: attrs.get(self.username_field),
'password': attrs.get('password')
}
if all(credentials.values()):
user = authenticate(**credentials)
if user:
if not user.is_active:
msg = _('User account is disabled.')
raise serializers.ValidationError(msg)
payload = jwt_payload_handler(user)
return {
'token': jwt_encode_handler(payload),
'user': user
}
else:
msg = _('Unable to log in with provided credentials.')
raise serializers.ValidationError(msg)
else:
msg = _('Must include "{username_field}" and "password".')
msg = msg.format(username_field=self.username_field)
raise serializers.ValidationError(msg)
主要是定义了两个字段,username和password, self.username_field
会默认得到Django的User模型类的username
字段。
因为在Django的User模型类中设置了一个这样的类属性:
USERNAME_FIELD = 'username'
好了,然后有一个validate方法,这个方法DRF自动调用,这里是用来校验用户名和密码是否正确的,只有这个方法验证通过,才能产生Token数据返回给用户,可以看到,这里面已经生成了token数据了,payload = jwt_payload_handler(user)
, 'token': jwt_encode_handler(payload)
里面最重要的一个校验方法,还是使用Django的authenticate方法,查看该方法实现:
def authenticate(request=None, **credentials):
"""
If the given credentials are valid, return a User object.
"""
for backend, backend_path in _get_backends(return_tuples=True):
try:
user = _authenticate_with_backend(backend, backend_path, request, credentials)
except PermissionDenied:
# This backend says to stop in our tracks - this user should not be allowed in at all.
break
if user is None:
continue
# Annotate the user object with the path of the backend.
user.backend = backend_path
return user
# The credentials supplied are invalid to all backends, fire signal
user_login_failed.send(sender=__name__, credentials=_clean_credentials(credentials), request=request)
校验成功返回User实例,最后会走到post请求函数接口。看父类JSONWebTokenAPIView是怎么返回对于post请求的数据:
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
if serializer.is_valid():
user = serializer.object.get('user') or request.user
token = serializer.object.get('token')
response_data = jwt_response_payload_handler(token, user, request)
response = Response(response_data)
if api_settings.JWT_AUTH_COOKIE:
expiration = (datetime.utcnow() +
api_settings.JWT_EXPIRATION_DELTA)
response.set_cookie(api_settings.JWT_AUTH_COOKIE,
token,
expires=expiration,
httponly=True)
return response
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
因为前面校验它就产生了token了,现在的问题是如何组织json格式返回给用户,
'JWT_RESPONSE_PAYLOAD_HANDLER':
'rest_framework_jwt.utils.jwt_response_payload_handler',
from rest_framework_jwt.utils import jwt_encode_handler, jwt_payload_handler, jwt_response_payload_handler
def jwt_response_payload_handler(token, user=None, request=None):
"""
Returns the response data for both the login and refresh views.
Override to return a custom response such as including the
serialized representation of the User.
Example:
def jwt_response_payload_handler(token, user=None, request=None):
return {
'token': token,
'user': UserSerializer(user, context={'request': request}).data
}
"""
return {
'token': token
}
可以看到,默认只会返回一个token的json格式。
有时候我们想返回包含其它信息的json格式,
你可以手动创建:
payload = jwt_payload_handler(user)
re_dict["token"] = jwt_encode_handler(payload)
如何发布Token我们知道了,那么如何根据Token得到具体用户? 查看
BaseJSONWebTokenAuthentication
def authenticate(self, request):
"""
Returns a two-tuple of `User` and token if a valid signature has been
supplied using JWT-based authentication. Otherwise returns `None`.
"""
jwt_value = self.get_jwt_value(request)
if jwt_value is None:
return None
try:
payload = jwt_decode_handler(jwt_value)
except jwt.ExpiredSignature:
msg = _('Signature has expired.')
raise exceptions.AuthenticationFailed(msg)
except jwt.DecodeError:
msg = _('Error decoding signature.')
raise exceptions.AuthenticationFailed(msg)
except jwt.InvalidTokenError:
raise exceptions.AuthenticationFailed()
user = self.authenticate_credentials(payload)
return (user, jwt_value)
上面的代码主要做了以下几点:
- 得到payload
- 根据payload得到登陆的用户实例
- 返回,给DRF校验
看到最后一个返回一个元组没,这个是给DRF用的,如果你没返回啥,DRF认为你验证不通过,有的话,DRF会在视图参数request添加属性进去,你就直接可以使用:request.user
。下面是如何校验得到用户的逻辑:
def authenticate_credentials(self, payload):
"""
Returns an active user that matches the payload's user id and email.
"""
User = get_user_model()
username = jwt_get_username_from_payload(payload)
if not username:
msg = _('Invalid payload.')
raise exceptions.AuthenticationFailed(msg)
try:
user = User.objects.get_by_natural_key(username)
except User.DoesNotExist:
msg = _('Invalid signature.')
raise exceptions.AuthenticationFailed(msg)
if not user.is_active:
msg = _('User account is disabled.')
raise exceptions.AuthenticationFailed(msg)
return user
可以看到,使用了User是
User = get_user_model()
也就是说,默认使用的Django的User模型,会根据用户名(唯一标识)来查找:
user = User.objects.get_by_natural_key(username)
这个方法的实现是:
def get_by_natural_key(self, username):
return self.get(**{self.model.USERNAME_FIELD: username})
也就是这个方法就是等价于调用模型类的:
Model.get(username=username)
可以配置的属性
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': settings.SECRET_KEY,
'JWT_GET_USER_SECRET_KEY': None,
'JWT_PUBLIC_KEY': None,
'JWT_PRIVATE_KEY': None,
'JWT_ALGORITHM': 'HS256',
# 如果秘钥是错误的,它会引发一个jwt.DecodeError
'JWT_VERIFY': True,
'JWT_VERIFY_EXPIRATION': True,
'JWT_LEEWAY': 0,
# Token过期时间设置
'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=300),
'JWT_AUDIENCE': None,
'JWT_ISSUER': None,
# 是否开启允许Token刷新服务,及限制Token刷新间隔时间,从原始Token获取开始计算
'JWT_ALLOW_REFRESH': False,
'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7),
# 定义与令牌一起发送的Authorization标头值前缀
'JWT_AUTH_HEADER_PREFIX': 'JWT',
'JWT_AUTH_COOKIE': None,
}
总的来说,代码还是很容易看懂,如果不懂原理,很难写好代码。
- Django提供了User基类,其它验证框架都在此基础上实现。
- 你可以修改继承验证框架逻辑,验证自己的User模型。
- 你可以继承自django的User模型。