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
}
我们来看两个比较重要的函数
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了。