《Django By Example》Java-Python-Django社区

Django REST JWT

2019-04-17  本文已影响4人  霡霂976447044

阅读本文之前,先阅读:
https://www.ywnds.com/?p=14967

本文假设你已经会使用DRF和JWT插件。

预备知识点:

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_classJSONWebTokenSerializer。下面先来看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和passwordself.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)

上面的代码主要做了以下几点:

看到最后一个返回一个元组没,这个是给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,
}

总的来说,代码还是很容易看懂,如果不懂原理,很难写好代码。

上一篇下一篇

猜你喜欢

热点阅读