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
posted @ 2020-07-16 00:57  the3times  阅读(1133)  评论(1编辑  收藏  举报