JWT

JWT

img

传统token和jwt认证的区别

  • 基于传统的token认证方式

    用户登录, 服务端给返回token,并将token保存在服务端
    以后在访问的时候,需要携带token,服务端获取token后,再去数据库获取token校验
    
  • JWT

    用户登录,服务端给用户返回一个token(服务端不保存)
    以后访问的时候,需要携带token,在服务端获取token,在做token的校验
    优势:相较于传统的token相比,在无需保存在服务端token
    

Jwt实现的过程

jwt的生成token格式如下,即:由 . 连接的三段字符串组成。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

生成规则

  • 第一段:HEADER部分,固定包含算法和token类型,对此json进行base64url加密,这就是token的第一段。

    {
      "alg": "HS256",
      "typ": "JWT"
    }
    
  • 第二段: PAYLOAD部分,包含一些数据,对此json进行base64url加密,这就是token的第二段。

    {
      "sub": "1234567890",
      "name": "John Doe",
      "iat": 1516239022
      ...
    }
    
  • 第三段: SIGNATURE部分,把前两段的base密文通过.拼接起来,然后对其进行HS256加密,再然后对hs256密文进行base64url加密,最终得到token的第三段。

    base64url(
        HMACSHA256(
          base64UrlEncode(header) + "." + base64UrlEncode(payload),
          your-256-bit-secret (秘钥加盐)
        )
    )
    

最后将三段字符串通过 .拼接起来就生成了jwt的token

注意:base64url加密是先做base64加密,然后再将 - 替代 +_ 替代 /

代码实现

我们可以先用pyjwt,后面用 rest_framework_jwt实现

  • 实现

    import jwt
    import datetime
    from jwt import exceptions
    SALT = 'iv%x6xo7l7_u9bf_u!9#g#m*)*=ej@bek5)(@u3kh*72+unjv='
    def create_token():
        # 构造header
        headers = {
            'typ': 'jwt',
            'alg': 'HS256'
        }
        # 构造payload
        payload = {
            'user_id': 1, # 自定义用户ID
            'username': 'wupeiqi', # 自定义用户名
            'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=5) # 超时时间
        }
        result = jwt.encode(payload=payload, key=SALT, algorithm="HS256", headers=headers).decode('utf-8')
        return result
    if __name__ == '__main__':
        token = create_token()
        print(token)
    

jwt校验token

一般在认证成功后,把jwt生成的token返回给用户,以后用户再次访问时候需要携带token,此时jwt需要对token进行超时合法性校验。

获取token之后,会按照以下步骤进行校验:

  • 将token分割成 header_segmentpayload_segmentcrypto_segment 三部分

    jwt_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
    signing_input, crypto_segment = jwt_token.rsplit(b'.', 1)
    header_segment, payload_segment = signing_input.split(b'.', 1)
    
  • 对第一部分header_segment进行base64url解密,得到header

  • 对第二部分payload_segment进行base64url解密,得到payload

  • 对第三部分crypto_segment进行base64url解密,得到signature

  • 对第三部分signature部分数据进行合法性校验

    • 拼接前两段密文,即:signing_input
    • 从第一段明文中获取加密算法,默认:HS256
    • 使用 算法+盐 对signing_input 进行加密,将得到的结果和signature密文进行比较。
import jwt
import datetime
from jwt import exceptions
def get_payload(token):
    """
    根据token获取payload
    :param token:
    :return:
    """
    try:
        # 从token中获取payload【不校验合法性】
        # unverified_payload = jwt.decode(token, None, False)
        # print(unverified_payload)
        # 从token中获取payload【校验合法性】
        verified_payload = jwt.decode(token, SALT, True)
        return verified_payload
    except exceptions.ExpiredSignatureError:
        print('token已失效')
    except jwt.DecodeError:
        print('token认证失败')
    except jwt.InvalidTokenError:
        print('非法的token')
if __name__ == '__main__':
    token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NzM1NTU1NzksInVzZXJuYW1lIjoid3VwZWlxaSIsInVzZXJfaWQiOjF9.xj-7qSts6Yg5Ui55-aUOHJS4KSaeLq5weXMui2IIEJU"
    payload = get_payload(token)

jwt认证算法:签发和校验

  • 签发 :根据登录请求提交来的账号,密码,设备信息签发token

    1)用基本信息存储json字典,采用base64算法加密得到 头字符串
    2)用关键信息存储json字典,采用base64算法加密得到 体字符串
    3)用头、体加密字符串再加安全码信息存储json字典,采用hash md5算法加密得到 签名字符串
    
    账号密码就能根据User表得到user对象,形成的三段字符串用 . 拼接成token返回给前台
    
  • 校验:根据客户端带来的token的请求,反解出user对象

    1)将token按 . 拆分为三段字符串,第一段 头加密字符串 一般不需要做任何处理
    2)第二段 体加密字符串,要反解出用户主键,通过主键从User表中就能得到登录用户,过期时间和设备信息都是安全信息,确保token没过期,且时同一设备来的
    3)再用 第一段 + 第二段 + 服务器安全码 不可逆md5加密,与第三段 签名字符串 进行碰撞校验,通过后才能代表第二段校验得到的user对象就是合法的登录用户
    

认证的流程开发

1)用账号密码访问登录接口,登录接口逻辑中调用 签发token 算法,得到token,返回给客户端,客户端自己存到cookies中

2)校验token的算法应该写在认证类中(在认证类中调用),全局配置给认证组件,所有视图类请求,都会进行认证校验,所以请求带了token,就会反解出user对象,在视图类中用request.user就能访问登录的用户

注:登录接口需要做 认证 + 权限 两个局部禁用

drf jwt安装和使用

pip install djangorestframework-jwt
# 1 创建超级用户
python3 manage.py createsuperuser
# 2 配置路由urls.py
from django.urls import path
from rest_framework_jwt.views import obtain_jwt_token
urlpatterns = [
    path('login/', obtain_jwt_token),
]
# 3 postman测试
向后端接口发送post请求,携带用户名密码,即可看到生成的token

# 4 setting.py中配置认证使用jwt提供的jsonwebtoken
# 5 postman发送访问请求(必须带jwt空格)

使用全套的djangorestframework-jwt(内置权限类)

# urls.py
from rest_framework_jwt.views import obtain_jwt_token
urlpatterns = [

    path('login/',obtain_jwt_token),
    path('order/',views.OrderView.as_view())
]


# views.py
class OrderView(GenericAPIView):
    authentication_classes = [JSONWebTokenAuthentication]
    permission_classes = [IsAuthenticated] # 加上游客不能访问,不加游客能访问

    def get(self, request, *args, **kwargs):
        return Response('订单信息')

  • 这里的登录视图rest_framework_jwt帮我们在内部做了视图,我们只需要在配置一个登录的路由去调用它就可以了
  • 关于视图里面的函数做认证,我们需要配置一个内置用户访问权限限制,JSONWebTokenAuthentication和IsAuthenticated才是一套的控制登录用户访问,
  • JSONWebTokenAuthentication加这个游客是可以访问的,校验规则:这里里面必须填authenticate,jwt空格传,才解析,如果不传不解析,到request里面没有东西,

控制登录接口返回的数据格式

1. 自己写登录接口
2. 用内置,控制返回的数据格式
-jwt的配置信息中有这个属性
'JWT_RESPONSE_PAYLOAD_HANDLER':
'rest_framework_jwt.utils.jwt_response_payload_handler',
-重写jwt_response_payload_handler,配置成咱们自己的

  • 用内置,控制返回的数据格式
第一步:
class OrderView(GenericAPIView):
    authentication_classes = [JSONWebTokenAuthentication]
    permission_classes = [IsAuthenticated]

    def get(self, request, *args, **kwargs):
        return Response('订单信息')

第二步:
def jwt_response_payload_handler(token, user=None, request=None):  # 返回什么样式,前端就是上面样式

    return {
        'token': token,
        'msg': '登录成功',
        'status': 100,
        'username': user.username

    }

第三步:在自己的settings配置
JWT_AUTH = {
    'JWT_RESPONSE_PAYLOAD_HANDLER':'api.utils.ahth.jwt_response_payload_handler'
}



自定义权限类



from api.utils.ahth import MyJwtAuthentication
class OrderView2(GenericAPIView):
    authentication_classes = [MyJwtAuthentication]
    # permission_classes = [IsAuthenticated]

    def get(self, request, *args, **kwargs):
        print(request.user)
        print(1111)
        return Response('商品信息')
    
    
# 第一种  
from rest_framework.authentication import BaseAuthentication
from rest_framework_jwt.authentication import BaseJSONWebTokenAuthentication
from rest_framework.exceptions import AuthenticationFailed
from  rest_framework_jwt.authentication import jwt_decode_handler
from rest_framework_jwt.utils import jwt_decode_handler # 跟上面死一样的   
from api.models import User
class MyJwtAuthentication(BaseAuthentication):
    def authenticate(self, request):
        jwt_value=request.META.get('HTTP_AUTHORIZATION')
        if jwt_value:
            try:
            #jwt提供了通过三段token,取出payload的方法,并且有校验功能
                payload=jwt_decode_handler(jwt_value)
            except jwt.ExpiredSignature:
                raise AuthenticationFailed('签名过期')
            except jwt.InvalidTokenError:
                raise AuthenticationFailed('用户非法')
            except Exception as e:
                # 所有异常都会走到这
                raise AuthenticationFailed(str(e))
            # 因为payload就是用户信息的字典
            print(payload)
            # return payload, jwt_value
            # 需要得到user对象,
            # 第一种,去数据库查
             user=models.User.objects.get(pk=payload.get('user_id'))
            # 第二种不查库
            #user=User(id=payload.get('user_id'),username=payload.get('username'))# 生成对象里面
            return user,jwt_value
        # 没有值,直接抛异常
        raise AuthenticationFailed('您没有携带认证信息')
        
 




# 第2种
 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用户对象】

"""

手动签发token,多方式登录

vews.py
from rest_framework.viewsets import ViewSet
from api.ser import UserModelSerializer
class LoginView(ViewSet):


    def login(self,request,*args,**kwargs):
        login_ser = UserModelSerializer(data=request.data,context={'request':request})
        login_ser.is_valid(raise_exception=True)
        token = login_ser.context.get('token')
        username = login_ser.context.get('username')
        return Response({'status':100,'msg':"成功",'token':token,'username':username})


    
ser.py
from rest_framework import serializers

from api import models
from rest_framework_jwt.utils import jwt_encode_handler
from rest_framework_jwt.utils import  jwt_payload_handler # 签发token
from rest_framework.exceptions import  ValidationError
class UserModelSerializer(serializers.ModelSerializer):
    username = serializers.CharField() # 这里要去覆盖username。因为他是唯一字段
    class Meta:
        model = models.User
        fields = ['username', 'password']

    def validate(self, attrs):
        print(self.context)
        username = attrs.get('username')
        password = attrs.get('password')
        # 判断,username 数据不同,查询字段不一样
        # 正则匹配手机
        import re
        if re.match('^1[3-9][0-9]{9}$',username):
          user = models.User.objects.filter(mobile=username).first()
        elif re.match('^.+@.+$', username):
            user = models.User.objects.filter(email=username).first()

        else:
            user = models.User.objects.filter(username=username).first()
        if user:
            # 校验密码
            if user.check_password(password):
                # 签发token
                payload = jwt_payload_handler(user)
                token = jwt_encode_handler(payload)
                self.context['token'] = token
                self.context['username'] = user.username
                return attrs
            else:
                raise ValidationError('密码错误')
        else:
            raise ValidationError('用户不存在')
"""
from rest_framework_jwt.utils import jwt_encode_handler
from rest_framework_jwt.utils import  jwt_payload_handler # 签发token

    payload = jwt_payload_handler(user) # 把user传入,得到payload
    token = jwt_encode_handler(payload) 把payload 传进去得到token
"""

  • payload = jwt_payload_handler(user) # 把user传入,得到payload,
  • token = jwt_encode_handler(payload) 把payload 传进去得到token
  • 因为username在user是一个字段,因为我们走的是post,它默认是查数据库保存,我们创建一个useraname字段覆盖,重新覆盖username字段,数据中它是unique,post,认为你保存数据,自己有校验没过

jwt的参数配置

# jwt的配置
import datetime
JWT_AUTH={
    'JWT_RESPONSE_PAYLOAD_HANDLER':'app02.utils.my_jwt_response_payload_handler',
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7), # 过期时间,手动配置
}

pyjwt的应用

视图

from app01.utils.jwt_token import create_token
class ProLoginView(APIView):
    """jwt登录认证"""

    def post(self,request,*args,**kwargs):
        user = request.data.get('username')
        pwd = request.data.get('password')
        user_obj = models.UserInfo.objects.filter(username=user,password=pwd).first()
        if not user_obj:
            return Response({'code':1000,'error':'用户名或密码错误'})
        token = create_token({'id':user_obj.id,'name':user_obj.username})
        return Response({'code': 10001, 'data': token})


from app01.exensions.auth import JwtQueryParamsAuthentication
class ProOrderView(APIView):
    authentication_classes = [JwtQueryParamsAuthentication]

    def get(self, request, *args, **kwargs):

        print(request.user)
        return Response('订单列表')

生成token

import jwt
import datetime
from django.conf import settings
def create_token(payload,timeout=1):
    
    salt = settings.SECRET_KEY
    headers = {
        'typ': 'jwt',
        'alg': 'HS256'
    }
    # 构造payload
    payload['exp'] = datetime.datetime.utcnow() + datetime.timedelta(minutes=timeout)  # 超时时间

    token = jwt.encode(payload=payload, key=salt, algorithm="HS256", headers=headers).decode('utf-8')
    return token


认证token
from rest_framework.authentication import BaseAuthentication
from django.conf import settings
from jwt import exceptions
from rest_framework.response import Response
import jwt
from rest_framework.exceptions import AuthenticationFailed
class JwtQueryParamsAuthentication(BaseAuthentication):
    def authenticate(self, request):
        token = request.query_params.get('token')
        salt = settings.SECRET_KEY
        try:
            payload = jwt.decode(token, salt, True)
        except exceptions.ExpiredSignatureError:
            msg = 'token已失效'
            raise AuthenticationFailed({'code':1003,"msg":msg})
        except jwt.DecodeError:
            msg = 'token认证失败'
            raise AuthenticationFailed({'code': 1003, "msg": msg})
        except jwt.InvalidTokenError:
            msg = '非法的token'
            raise AuthenticationFailed({'code': 1003, "msg": msg})
        return (payload, token) # payload就是user,



        #可以有三种返回值
        # 抛出异常,后面的函数就不会执行
        # return一个元组(1,2)认证通过,在视图中调用request.user,就是元组的一个值,request.auth时第二个值
        # return None 在验证
posted @ 2020-07-19 23:26  小子,你摊上事了  阅读(115)  评论(0编辑  收藏  举报