jwt全流程分析
jwt介绍
pass待补充....
jwt本质使用流程
-
第一步,用户提交用户名和密码给服务端,如果登录成功,使用jwt创建一个token,并给用户返回
eyJ0eXAiOiJqd3QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6InpjYyIsImV4cCI6MTU5NDczODg5MX0.OCG4mUhs_yXIkxtxvG9MWJWjpbvnSGDcqMVtpsn_0mo # toekn串使用点分割成三部分,分别是:header.payload.signature
- 第一段字符串 header内部包含了算法/token类型。json转换成字符串,然后做base64url加密( base64转码)
# header { "alg": "HS256", "typ": "JWT" }
-
第二段字符串pyload,自定义的值,json转换成字符串,然后做base64url加密
# payload { 'id':11, 'name':'dd', 'exp':1134(超时时间) }
-
第三段字符串:
第一步:把1,2部分的bash64串拼接 第二步:前两部分拼接后的串进行hs256加密+加盐 第三步:对hs256加密后的密文在进行base64url加密
-
以后用户访问时,需要携带token,后端需要对token校验
-
获取token,
-
第一步:对token进行切割
-
第二部:对第二段进行base64url解密,获取pyload信息
检查超时时间是否超时
-
第三步:由于三部部分的字符串不能反解,把第一+二段在进行hs256加密
第一步:token分割,获取前两部分的bash64串 第二步:对前两部分的bash64串加密进行hs256加密+加盐 第三步:hash密文转bash64串,再和token的第三部分签名做比较 如果相等,表示token没有修改通过
-
补充:客户端如果串改了token的前两部分,然后做hash加密,生成第三部分拼接起来。由于客户端不知道服务端在hash成第三部分签名时使用了何种hash加密方式,不知道加了什么盐,结果就是token校验失败。因此服务端hash时加盐是很有必要的。
djangorestframework-jwt
djangorestframework-jwt
是drf内使用的一个第三方jwt模块,可以帮我们快速实现jwt认证。
缺陷:仅能在drf中使用,无法在django或者flask中使用。
下载安装
pip36 install djangorestframework-jwt
简单使用
先登陆,获取token;下次访问其他页面时携带token值才可以访问使用了jwt认证校验的视图页面。
登陆认证:添加一条url即可
# 配置一条url即可完成登陆是自动生成token值,返回前段的过程
from rest_framework_jwt.views import obtain_jwt_token,ObtainJSONWebToken
urlpatterns = [
path('login/', obtain_jwt_token),
]
# obtain_jwt_token等价于,ObtainJSONWebToken.as_view()是drf-jw
# 这是drf-jwt帮我们实现的一个登陆视图,该视图内部自动调用drf-jwt的登陆序列化器,完成登陆校验并生成一个token值返回给前段
全局配置:使用drf-jwt认证
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES":
[
"rest_framework_jwt.authentication.JSONWebTokenAuthentication",
],
'DEFAULT_PERMISSION_CLASSES': [
"rest_framework.permissions.IsAuthenticated",
],
}
# 注意:
drf-jwt内置的JSONWebTokenAuthentication需要配合drf的IsAuthenticated权限校验一块使用
这是因为当drf-jwt找不到认证token时会直接返回None,即通过了认证,此时需要在权限中限制。
在请求体中携带token键值对访问
# postman中,简直对分别是
键:Authorization
值 JWT eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImphY2siLCJleHAiOjE1OTQ4MDM0MjIsImVtYWlsIjoiIn0.NiKTPXypqrXVfN0rvoMw9SDWJEyv3Fd6tMTBvVE_8ZU
# 注意:键和值之间空一个
# 默认值是一JWT为前缀,可以在jwt的配置中通过JWT_AUTH_HEADER_PREFIX参数修改
补充
# drf-jwt的认证类JSONWebTokenAuthentication继承子drf-jwt的认证基类BaseJSONWebTokenAuthentication
# drf-jwt的认证基类继承自drf的认证基类BaseAuthentication,并实现了authenticate方法。
基于drf-jwt内置的认证类实现:控制用户登陆后才能访问&不登陆也能访问
因为drf-jwt在认证时如果用户为携带token值,认证类的操作是不做任何处理直接返回None;这就使得如果用户为认证也能通过认证;
因此drf-jwt内置的认证类需要配合内置权限IsAuthenticated
一块使用,才能实现为登陆用户才能访问;如果没有使用权限校验则未登陆也能访问。
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.permissions import IsAuthenticated
# 1 订单系统,控制用户登录后才能访问
class OrderAPIView(APIView):
authentication_classes = [JSONWebTokenAuthentication, ]
permission_classes = [IsAuthenticated, ]
def get(self,request,*args,**kwargs):
return Response('订单信息')
# 2 图书系统,不登录后也能访问
class BooKAPIView(APIView):
authentication_classes = [JSONWebTokenAuthentication, ]
def get(self,request,*args,**kwargs):
return Response('图书系统')
控制登陆接口返回的数据格式
# drf-jwt内置提供的仅需配置一条url即可实现登陆接口,但是该接口返回的数据仅有token键值对
# 控制登陆接口返回的数据格式,有两种解决方法:
- 重写登陆接口
- 使用内置的接口,但是重写jwt_response_payload_handler方法
# 推荐使用方案2,直接重写该方法即可自定义返回数据格式
drf-jwt内置登陆接口源码阅读
为什么登陆时仅配置一条url就可以生成token并返回给客户端
from rest_framework_jwt.views import ObtainJSONWebToken
path('login/', obtain_jwt_token)
# 登陆接口源码阅读的入口就是登陆的url
# 登陆url中的obtain_jwt_token其实就是ObtainJSONWebToken类调用as_view()的结果,
# ObtainJSONWebToken是一个视图类,继承了drf-jwt的JSONWebTokenAPIView
# jwt的JSONWebTokenAPIView提供了post方法,post方法内将生成的token通过jwt_response_payload_handler方法返回定制返回数据格式
# 另外ObtainJSONWebToken配套的序列化器是drf-jwt内置的JSONWebTokenSerializer
# 该序列化器在全局钩子校验时,当提交过来的用户名和密码校验成功后会自定生成一个token值,保存在序列化器对象中。
# 这样在视图ObtainJSONWebToken中就可以取到这个token值,进而返回给前端
from rest_framework_jwt.utils import 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 }
自定制认证类
继承drf-jwt的认证类JSONWebTokenAuthentication
,并重写authenticate
方法。
import jwt
from jwt import exceptions
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework_jwt.authentication import jwt_decode_handler
from rest_framework.exceptions import AuthenticationFailed
class DrfJwtTokenAuth(JSONWebTokenAuthentication):
def authenticate(self, request):
jwt_value = request.META.get('HTTP_AUTHORIZATION') # 从请求头中取出token值
try:
payload = jwt_decode_handler(jwt_value) # 从token值中取出payload(字典)
except exceptions.ExpiredSignatureError: # 捕获异常
raise AuthenticationFailed('token已失效')
except jwt.DecodeError:
raise AuthenticationFailed('token认证失败')
except jwt.InvalidTokenError:
raise AuthenticationFailed('非法的token')
return self.authenticate_credentials(payload), jwt_value
# 从payload中取出user对象,内部是查询数据库实现的
# authenticate_credentials是JSONWebTokenAuthentication提供的方法
"""
当认证通过后,需要返回一个包含user对象的二元组,获取这个user对象的方式有两种;
第一种是通过payload中的user_id或者username查数据哭获取user对象;
第二种是通过用户类实例化一个该user对象【仅仅是一个对象,不是user用户对象】
"""
使用自的制认证类在请求页面时在请求体中携带的键值对
键:Authorization
值:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImphY2siLCJleHAiOjE1OTQ4MDM0MjIsImVtYWlsIjoiIn0.NiKTPXypqrXVfN0rvoMw9SDWJEyv3Fd6tMTBvVE_8ZU
# 其中值不需要再添加头 JWT,这是因为我们子定制的认证类的authenticate方法是直接从请求体中取出的token值
# 而内置的JSONWebTokenAuthentication需要是因为它在获取token值的方式是通过自己的get_jwt_value方法获取的。
手动签发token(多方式登陆)
# vies.py
class LoginView(ViewSet):
def login(self, request, *args, **kwargs):
# 可以使用context={'request':request}给序列化器传递参数
ser_obj = LoginModelSerializer(data=request.data)
ser_obj.is_valid(raise_exception=True)
# 基于序列化器对象的context属性传递接收数据
back_dict = {
'user_id': ser_obj.context.get('user_id'),
'user_name': ser_obj.context.get('user_name'),
'token': ser_obj.context.get('token')
}
return Response(back_dict)
# ser.py
from app01 import models, utils
class LoginModelSerializer(serializers.ModelSerializer):
# 为了避免数据库用户名唯一的约束导致用户名验证失败的问题,重写覆盖username
username = serializers.CharField()
class Meta:
model = models.UserInfo
fields = ['username', 'password', ]
def validate(self, attrs):
username = attrs.get('username')
password = attrs.get('password')
user_obj = utils.get_user(username)
if user_obj:
if user_obj.check_password(password):
payload = jwt_payload_handler(user_obj)
token = jwt_encode_handler(payload)
self.context['token'] = token
self.context['user_id'] = user_obj.pk
self.context['user_name'] = user_obj.username
return attrs
else:
raise ValidationError({'password': '密码错误'})
else:
raise ValidationError({'username': '用户名不存在'})
# utils.py
from django.db.models import Q
from app01 import models
def get_user(username):
q = Q() # Q查询的高级应用,或的关系
q.connector = 'or'
q.children.append(('username', username))
q.children.append(('email', username))
q.children.append(('phone', username))
user_obj = models.UserInfo.objects.filter(q).first()
return user_obj
jwt的配置参数
# jwt的配置
import datetime
JWT_AUTH={
'JWT_RESPONSE_PAYLOAD_HANDLER': 'app02.utils.my_jwt_response_payload_handler',
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7), # 过期时间,手动配置
}
使用方法总结
# 手动签发token
payload = jwt_payload_handler(user_obj) # 生成payload
token = jwt_encode_handler(payload) # 生成token
# 手动实现token认证
payload = jwt_decode_handler(jwt_value) # 从token值中取出payload(字典)
user_obj = JSONWebTokenAuthentication.authenticate_credentials(payload) # 从payload中查数据库取出user对象
补充
限制用户唯一
# 限制用户注册时用户名唯一的两种方式
- 方式1:models中限定用户名字段是 unique=True
- 方式2:在注册序列化器中增加用户名的局部勾子函数,约束用户名不能和数据库的用户名相同。
补充Base64
# base64编码和解码
#md5固定长度,不可反解
#base63 变长,可反解
#编码(字符串,json格式字符串)
import base64
import json
dic={'name':'lqz','age':18,'sex':'男'}
dic_str=json.dumps(dic)
ret=base64.b64encode(dic_str.encode('utf-8'))
print(ret)
# 解码
# ret是带解码的串
ret2=base64.b64decode(ret)
print(ret2)
pyjwt
pyjwt是一个独立的JWT Python库。它的使用非常底层,需要我们自己自己生成jwt并做jwt的认证,但是它的使用方法非常方便,只需要我们调用两个方法即可。
优势:可以在任何python的web框架中使用;缺陷:需要自己全流程实现,书写较繁琐
jwt.encode() # 生成jwt串
jwt.decode() # 获取payload字典
下载安装
pip36 install pyjwt
登陆生成token
手写登陆成功后生成token的整个流程,并返回给客户端token值
# jwt登陆成功后生成token
# views.py
from django.contrib import auth
from app01 import models, utils
class LoginPyJwtView(ViewSet):
def login(self, request, *args, **kwargs):
username = request.data.get('username')
password = request.data.get('password')
# 基于auth的登陆验证
user_obj = auth.authenticate(request, username=username, password=password)
if not user_obj:
return Response({'code': 1000, 'error': '用户名或密码错误'})
# 初始payload
payload = {"id": user_obj.pk, "username": user_obj.username}
# 调用自定义的封装函数生成token串
token = utils.create_token(payload)
#
return Response({'code': 1000, 'msg': '登陆成功', 'token': token})
# 自定义封装函数
# utils.py
import jwt
import datetime
from django.conf import settings
def create_token(payload, timeout=1):
"""
有初始payload生成token
:param payload: 初始payload
:param timeout: 默认过期时间1min
:return: token串
"""
# 使用django项目的密匙做hash加密的盐
salt = settings.SECRET_KEY
# 自定制基本的jwt头部字典,其实可以不用,因为源码内部帮我们实现了这个
headers = {
'typ': 'jwt',
'alg': 'HS256'
}
# 更新payload, 添加过期时间
payload['exp'] = datetime.datetime.utcnow() + datetime.timedelta(minutes=timeout) # 超时时间
# 调用pyjwt生成token,默认采用hs256加密,内部调用pyjwt的encode()加密;最终返回一个jwt串
token = jwt.encode(payload=payload, key=salt, algorithm="HS256", headers=headers).decode('utf-8')
return token
内部生成token源码分析
# jwt.encode()内部调用了PYJWT的encode(),在调用PyJWS的encode()
# PyJWS的encode()方法源码
def encode(self,
payload, # type: Union[Dict, bytes]
key, # type: str
algorithm='HS256', # type: str
headers=None, # type: Optional[Dict]
json_encoder=None # type: Optional[Callable]
):
# segments列表存放token串的三部分,等到三部分都放在该列表后会通过join的方法合并成一个toekn串
segments = []
if algorithm is None:
algorithm = 'none'
if algorithm not in self._valid_algs:
pass
# Header,内部写死的,因此我们在自定义登陆认证时可以不写header,即使写了也没有用:(
header = {'typ': self.header_typ, 'alg': algorithm}
if headers:
self._validate_headers(headers)
header.update(headers)
# 将header字典json序列化转成json字符串待base64转码成密文的字符串
json_header = force_bytes(
json.dumps(
header,
separators=(',', ':'),
cls=json_encoder
)
)
# 将header和payload的json字符串转成base64转码成密文的字符串并放在segments列表中
segments.append(base64url_encode(json_header))
segments.append(base64url_encode(payload))
# Segments,拼接前两部分,hash加密成hash值成为token串的第三部分
signing_input = b'.'.join(segments)
try:
alg_obj = self._algorithms[algorithm]
key = alg_obj.prepare_key(key)
signature = alg_obj.sign(signing_input, key)
except KeyError:
if not has_crypto and algorithm in requires_cryptography:
raise NotImplementedError(
"Algorithm '%s' could not be found. Do you have cryptography "
"installed?" % algorithm
)
else:
raise NotImplementedError('Algorithm not supported')
# 将token串的第三部分bash64一下放在segments列表中
segments.append(base64url_encode(signature))
# 最终,拼接三个bash64串为一个完成的token串
return b'.'.join(segments)
token认证校验
token认证校验,需要自己手写获取token的校验和获取user对象
# app_auth.py
import jwt
from jwt import exceptions
from rest_framework.exceptions import AuthenticationFailed
from rest_framework.authentication import BaseAuthentication
from django.conf import settings
from django.contrib.auth import get_user_model
class PyJwtAuthentication(BaseAuthentication): # 继承drf的BaseAuthentication
def authenticate(self, request):
# 从去请求头中获取token值
jwt_value = request.META.get('HTTP_AUTHORIZATION')
# 验证是hash使用的盐也是项目配置文件的盐
salt = settings.SECRET_KEY
try:
# 调用pyjwt内部的decode方法获取payload字典,内部完成校验和解码反序列化等操作
payload = jwt.decode(jwt_value, salt, True)
except exceptions.ExpiredSignatureError:
# 捕获签名过期异常
raise AuthenticationFailed({'code': 1003, "msg": 'token签名过期'})
except jwt.DecodeError:
# 捕获token认证失败异常
raise AuthenticationFailed({'code': 1003, "msg": 'token认证失败'})
except jwt.InvalidTokenError:
# 捕获非法token异常
raise AuthenticationFailed({'code': 1003, "msg": '非法的token'})
# 调用对象方法基于payload中的用户名,查询数据库获取用户对象
user = self.get_user(payload)
return user, jwt_value
def get_user(self, payload):
# 参考rf-jwt内置的登陆token认证,实现的基于payload的用户名查数据库获取user对象
User = get_user_model()
username = payload.get('username')
if not username:
raise AuthenticationFailed('无效的payload')
try:
user = User.objects.get_by_natural_key(username)
except User.DoesNotExist:
raise AuthenticationFailed('用户不存在')
if not user.is_active:
raise AuthenticationFailed('无效的用户')
return user
内部jwt认证源码分析
如何让从token串中反解出payload字符串并做token合法性验证
# jwt.decode(jwt_value, salt, True)内部调用PyJWT的decode(),内部再调用PyJWS的decode方法
# PyJWT的encode()
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
)
# 从token串中反解出payload
payload, _, _, _ = self._load(jwt)
if options is None:
options = {'verify_signature': verify}
else:
options.setdefault('verify_signature', verify)
# 调用父类PyJWS的encode方法获取,返回payload
decoded = super(PyJWT, self).decode(
jwt, key=key, algorithms=algorithms, options=options, **kwargs
)
try:
# 解码反序列化获得payload字典
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
# PyJWS的decode方法
def decode(self,
jwt, # type: str
key='', # type: str
verify=True, # type: bool
algorithms=None, # type: List[str]
options=None, # type: Dict
**kwargs):
merged_options = merge_dict(self.options, options)
verify_signature = merged_options['verify_signature']
if verify_signature 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,token串的前两部分串,头,签名
payload, signing_input, header, signature = self._load(jwt)
if not verify:
warnings.warn('The verify parameter is deprecated. '
'Please use verify_signature in options instead.',
DeprecationWarning, stacklevel=2)
elif verify_signature:
# 调用pyjws的_verify_signature方法验证token串是否合法/过期/有效的
self._verify_signature(payload, signing_input, header, signature,
key, algorithms)
# 返回payload
return payload