django jwt token认证中rest_framework_jwt的refresh token有效期

导读

jwt在业界已经广泛使用,但这篇文章不是用来介绍jwt的,也不是用来介绍rest_framework_jwt的,而是跟各位掰扯掰扯rest_framework_jwt中的refresh token功能,因为它很可能不是你想象中的refresh token哦 。

场景再现

在jwt鉴权过程中往往会使用accesstoken 和 refreshtoken,顾名思义,refreshtoken是用来更新后的token,如果项目中配置了refreshtoken过期时间一般就是指refreshtoken的有效期,而且这个有效期比accesstoken的有效期要长得多。这个是一般场景中的释义,但是近期在用rest_framewrok_jwt对django项目进行鉴权的时候发现rest_framewrok_jwt 中refresh token的有效期完全不是这个意思。
大家都知道在django settings当中 ‘JWT_REFRESH_EXPIRATION_DELTA’: datetime.timedelta(days=1) 用来配置refresh token的有效期,很多人默认refresh token的有效期就是1天。如果将token有效期设置为’JWT_EXPIRATION_DELTA’: datetime.timedelta(seconds=60),当前同时生成token和refreshtoken 去试试他们的有效时间。你得到的结果应该是refreshtoken刚过60s refreshtoken就无效了,而且不能刷新token,这是为什么呢?其实JWT_REFRESH_EXPIRATION_DELTA的真正意思是token能被refresh的有效期而不是refresh token的有效期,也就是说你在JWT_REFRESH_EXPIRATION_DELTA设置的时间内可以去refresh token,过了这个时间就不能refresh token了,而refresh出来的token的有效期实际上跟access token是同一个时间,没错,就是60s。
下面我们就来通过源码证实一下。

源码佐证

refresh token的源码:
path: rest_framework_jwt.serializers.RefreshJSONWebTokenSerializer

 class RefreshJSONWebTokenSerializer(VerificationBaseSerializer):
        """
        Refresh an access token.
        """
    
        def validate(self, attrs):
            token = attrs['token']
            # 这一段用来对需要进行refresh的代码进行校验
            payload = self._check_payload(token=token)
            user = self._check_user(payload=payload)
            # Get and check 'orig_iat'
            orig_iat = payload.get('orig_iat')
    
            if orig_iat:
                # Verify expiration
                refresh_limit = api_settings.JWT_REFRESH_EXPIRATION_DELTA
                if isinstance(refresh_limit, timedelta):
                    refresh_limit = (refresh_limit.days * 24 * 3600 +
                                     refresh_limit.seconds)
                expiration_timestamp = orig_iat + int(refresh_limit)
                now_timestamp = timegm(datetime.utcnow().utctimetuple())
    
                if now_timestamp > expiration_timestamp:
                    msg = _('Refresh has expired.')
                    raise serializers.ValidationError(msg)
            else:
                msg = _('orig_iat field is required.')
                raise serializers.ValidationError(msg)
    
            new_payload = jwt_payload_handler(user)
            new_payload['orig_iat'] = orig_iat
            return {
     
                'token': jwt_encode_handler(new_payload),
                'user': user
            }

我们来看两个比较重要的函数

  1. payload = self._check_payload(token=token)
    这个主要是来检查需要刷新的token,有一项检查就是当前token是否过期
serializers.py
    def _check_payload(self, token, **kwargs):
        # Check payload valid (based off of JSONWebTokenAuthentication,
        # may want to refactor)
        try:
            payload = jwt_decode_handler(token, **kwargs)
        except jwt.ExpiredSignature:
            msg = _('Signature has expired.')
            raise serializers.ValidationError(msg)
        except jwt.DecodeError:
            msg = _('Error decoding signature.')
            raise serializers.ValidationError(msg)

        return payload
serializers.py
User = get_user_model()
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
jwt_decode_handler = api_settings.JWT_DECODE_HANDLER
jwt_get_username_from_payload = api_settings.JWT_PAYLOAD_GET_USERNAME_HANDLER
rest_framework_jwt/settings.py
DEFAULTS = {
     
    'JWT_ENCODE_HANDLER':
    'rest_framework_jwt.utils.jwt_encode_handler',

    'JWT_DECODE_HANDLER':
    'rest_framework_jwt.utils.jwt_decode_handler',
utils.py
def jwt_decode_handler(token):
    options = {
     
        'verify_exp': api_settings.JWT_VERIFY_EXPIRATION,
    }
    # get user from token, BEFORE verification, to get user secret key
    unverified_payload = jwt.decode(token, None, False)
    secret_key = jwt_get_secret_key(unverified_payload)
    a = jwt.decode(
        token,
        api_settings.JWT_PUBLIC_KEY or secret_key,
        api_settings.JWT_VERIFY,
        options=options,
        leeway=api_settings.JWT_LEEWAY,
        audience=api_settings.JWT_AUDIENCE,
        issuer=api_settings.JWT_ISSUER,
        algorithms=[api_settings.JWT_ALGORITHM],
    )
    return jwt.decode(
        token,
        api_settings.JWT_PUBLIC_KEY or secret_key,
        api_settings.JWT_VERIFY,
        options=options,
        leeway=api_settings.JWT_LEEWAY,
        audience=api_settings.JWT_AUDIENCE,
        issuer=api_settings.JWT_ISSUER,
        algorithms=[api_settings.JWT_ALGORITHM]
    )
api_jwt.py
_jwt_global_obj = PyJWT()
encode = _jwt_global_obj.encode
decode = _jwt_global_obj.decode

    def decode(self,
               jwt,  # type: str
               key='',   # type: str
               verify=True,  # type: bool
               algorithms=None,  # type: List[str]
               options=None,  # type: Dict
               **kwargs):

        if verify and not algorithms:
            warnings.warn(
                'It is strongly recommended that you pass in a ' +
                'value for the "algorithms" argument when calling decode(). ' +
                'This argument will be mandatory in a future version.',
                DeprecationWarning
            )

        payload, _, _, _ = self._load(jwt)

        if options is None:
            options = {
     'verify_signature': verify}
        else:
            options.setdefault('verify_signature', verify)

        decoded = super(PyJWT, self).decode(
            jwt, key=key, algorithms=algorithms, options=options, **kwargs
        )

        try:
            payload = json.loads(decoded.decode('utf-8'))
        except ValueError as e:
            raise DecodeError('Invalid payload string: %s' % e)
        if not isinstance(payload, Mapping):
            raise DecodeError('Invalid payload string: must be a json object')

        if verify:
            merged_options = merge_dict(self.options, options)
            self._validate_claims(payload, merged_options, **kwargs)

        return payload

    def _validate_claims(self, payload, options, audience=None, issuer=None,
                         leeway=0, **kwargs):

        if 'verify_expiration' in kwargs:
            options['verify_exp'] = kwargs.get('verify_expiration', True)
            warnings.warn('The verify_expiration parameter is deprecated. '
                          'Please use verify_exp in options instead.',
                          DeprecationWarning)
        if isinstance(leeway, timedelta):
            leeway = leeway.total_seconds()

        if not isinstance(audience, (string_types, type(None), Iterable)):
            raise TypeError('audience must be a string, iterable, or None')

        self._validate_required_claims(payload, options)

        now = timegm(datetime.utcnow().utctimetuple())

        if 'iat' in payload and options.get('verify_iat'):
            self._validate_iat(payload, now, leeway)

        if 'nbf' in payload and options.get('verify_nbf'):
            self._validate_nbf(payload, now, leeway)

        if 'exp' in payload and options.get('verify_exp'):
            self._validate_exp(payload, now, leeway)

        if options.get('verify_iss'):
            self._validate_iss(payload, issuer)

        if options.get('verify_aud'):
            self._validate_aud(payload, audience)
    def _validate_exp(self, payload, now, leeway):
        try:
            exp = int(payload['exp'])
        except ValueError:
            raise DecodeError('Expiration Time claim (exp) must be an'
                              ' integer.')

        if exp < (now - leeway):
            raise ExpiredSignatureError('Signature has expired')

现在看到庐山真面目了,比较exp < (now - leeway), 其中leeway默认是0。当重新refreshtoken的时候仍旧要先判断当前的token是否过期,那我们就要看看生成refresh token的时候exp是怎么获取的。
2. 第二部分就是刷新token中生成的新token

		new_payload = jwt_payload_handler(user)
        new_payload['orig_iat'] = orig_iat
        return {
     
            'token': jwt_encode_handler(new_payload),
            'user': user
        }
utils.py
def jwt_payload_handler(user):
    username_field = get_username_field()
    username = get_username(user)

    warnings.warn(
        'The following fields will be removed in the future: '
        '`email` and `user_id`. ',
        DeprecationWarning
    )

    payload = {
     
        'user_id': user.pk,
        'username': username,
        'exp': datetime.utcnow() + api_settings.JWT_EXPIRATION_DELTA
    }
    if hasattr(user, 'email'):
        payload['email'] = user.email
    if isinstance(user.pk, uuid.UUID):
        payload['user_id'] = str(user.pk)

    payload[username_field] = username

    # Include original issued at time for a brand new token,
    # to allow token refresh
    if api_settings.JWT_ALLOW_REFRESH:
        payload['orig_iat'] = timegm(
            datetime.utcnow().utctimetuple()
        )

    if api_settings.JWT_AUDIENCE is not None:
        payload['aud'] = api_settings.JWT_AUDIENCE

    if api_settings.JWT_ISSUER is not None:
        payload['iss'] = api_settings.JWT_ISSUER

    return payload

从代码 ‘exp’: datetime.utcnow() + api_settings.JWT_EXPIRATION_DELTA可以看到新生成的token exp是当前时间+token的有效时间,而不是JWT_REFRESH_EXPIRATION_DELTA的时间,也就是说refresh的token有效时间仍旧是60s,只不过过期时间从当前生成的时间算起。因此,在第一步校验token的时候,如果token已经超过60s会直接报token过期,走不到下一步生成新的token。

总结

通过看源码也证实了restframework_jwt中refresh token的有效期实际上就是token的有效期,而JWT_REFRESH_EXPIRATION_DELTA代表的是刷新token这个操作的过期时间。
也就是说我们在鉴权时,需要在token有效期内及时刷新token才能保证token的有效性,而且即便是及时刷新token,整个token的有效期最长也就是JWT_REFRESH_EXPIRATION_DELTA+JWT_EXPIRATION_DELTA,超过这个时间就需要重新登录获取token了。

你可能感兴趣的:(django,jwt)