JWT认证

1. Token

首先大致了解一下什么是 Token

  • Token 是一种客户端认证机制、令牌,是一个经过加密的字符串,安全性强,支持跨域

  • 用户第一次登录,服务器通过数据库校验其用户名和密码是否合法,则再生成一个token串,服务端会返回Token给前端,前端可以在每次请求的时候带上Token证明自己的合法地位

  • Token 的生成一般是采用uuid保证唯一性,当用户登录时为其生成唯一的token,存储一般保存在数据库中

 

 

2. Json Web Token

Json Web Token 简称 JWT,其本质就是 token 认证机制。

  • Json web token (JWT),是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准
  • 该 token 被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。
  • JWT 的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该 token 也可直接被用于认证,也可被加密。

 

2.1 JWT的构成

JWT 就是一段字符串,由三段信息构成的,将这三段信息文本用.链接一起就构成了JWT 字符串,像这样的:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
  • 第一部分我们称它为头部(header),可以存放公司信息,加密方式等等
  • 第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),可以存放用户信息
  • 第三部分是签证(signature),是把第一段和第二段通过某种加密方式 + 秘钥加密得到一个字符串。同样的,验证就是拿到第一段和第二段使用同样的加密方式+秘钥重新加密,将得到字符串跟第三段比较,如果一样,表示没有被篡改,如果不一样,表明被篡改了,token 不能用了

2.1.1 header

jwt 的头部承载两部分信息:

  • 声明类型,这里是jwt
  • 声明加密的算法,通常直接使用 HMAC SHA256

完整的头部类似于下面的 JSON:

{
  'typ': 'JWT',
  'alg': 'HS256'
}

然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分

  • 对称加密:加密和解密都使用同一个秘钥
  • 非对称加密:加密用公钥,解密用私钥

加密后的结果

eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJIUzI1NiJ9

2.1.2 base64

Base64是网络上最常见的用于传输8Bit字节码的编码方式之一
Base64就是一种基于64个可打印字符来表示二进制数据的方法。
Base64编码是从二进制到字符的过程,可用于在HTTP环境下传递较长的标识信息。
采用Base64编码具有不可读性,需要解码后才能阅读。
 

base64的长度一定是4的倍数,如果不到用 = 号补齐

s = json.dumps(s)

# 对字符串进行编码,
base64.b64encode(s.encode('utf8'))
base64.b64encode(bytes(s, encoding='utf8'))
s = b'eyJ0eXAiOiAiSldUIiwgImFsZyI6ICJIUzI1NiJ9'

# 对使用 b64 编码的字符串进行解码,注意要是 byte 类型。
base64.b64decode(s)

2.1.2 payload

载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分

  • 标准中注册的声明
  • 公共的声明
  • 私有的声明

标准中注册的声明 (建议但不强制使用) :

  • iss: jwt签发者
  • sub: jwt所面向的用户
  • aud: 接收jwt的一方
  • exp: jwt的过期时间,这个过期时间必须要大于签发时间
  • nbf: 定义在什么时间之前,该jwt都是不可用的.
  • iat: jwt的签发时间
  • jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避时序攻击。

公共的声明 : 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息,但不建议添加敏感信息,因为该部分在客户端可解密

私有的声明 : 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。

定义一个payload:

{
  "sub": "1234567890",
  "name": "xwx",
  "admin": True
}

然后将其进行 base64 加密,得到 JWT 的第二部分

b'eyJzdWIiOiAiMTIzNDU2Nzg5MCIsICJuYW1lIjogImxxeiIsICJhZG1pbiI6IHRydWV9'

2.1.3 signature

JWT的第三部分是一个签证信息,这个签证信息由三部分组成:

  • header (base64 编码后的)
  • payload (base64 编码后的)
  • secret

这个部分需要 base64 加密后的 header 和 payload 使用.连接组成的字符串
然后通过 header 中声明的加密方式进行加盐 secret 组合加密,然后就构成了 jwt 的第三部分

最终的样子类似于

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

注意secret 是保存在服务器端的,jwt 的签发生成也是在服务器端的,secret 就是用来进行 jwt 的签发和 jwt 的验证,所以,它就是服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个 secret, 那就意味着客户端是可以自我签发 jwt 了。

2.2 签发与校验

  • 签发:根据登录请求提交来的 账号 + 密码 + 设备信息 签发 token
  • 校验:根据客户端带token的请求 反解出 user 对象

3. drf-jwt

关于签发和核验JWT,我们可以使用Django REST framework JWT扩展来完成。

官网:http://getblimp.github.io/django-rest-framework-jwt/

下载:

pip3 install djangorestframework-jwt

除了 djangorestframework-jwt 还有 djangorestframework-simplejwt,俩者用法很相似。

3.1 快速使用

JWT行动封装了登录功能,不用自己写登录接口,使用的表是 django auth 的 user 表,需要创建超级用户表

  • 1 创建超级用户
python3 manage.py createsuperuser
  • 配置路由urls.py
from django.urls import path
from rest_framework_jwt.views import obtain_jwt_token

urlpatterns = [
    path('login/', obtain_jwt_token),
]
  • postman 测试
    向后端接口发送 post 请求,携带用户名密码,即可看到生成的 token


3.2 修改返回格式

  • 新建一个py文件,编写函数
def jwt_response_payload_handler(token, user=None, request=None):
    return {'code': 100, 'msg': '登陆成功', 'token': token, 'username': user.username}
  • 配置文件中配置
JWT_AUTH = {
    'JWT_RESPONSE_PAYLOAD_HANDLER': 'app01.utils.jwt_response_payload_handler',
}
  • 修改后如下所示


3.3 jwt 验证

  • 导入认证类、权限类
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.permissions import IsAuthenticated
 
  • 在视图函数中添加认证类、权限类
class BookView(ViewSet):
    authentication_classes = [JSONWebTokenAuthentication,]
    permission_classes = [IsAuthenticated,]
 
  • 向视图函数发送 get 请求,并且需要在请求头中携带 token,请求头键名是 Authorization,键值是jwt token串必须要以 jwt 开头,与token串之间用空格隔开
请求头中 key:  Authorization
请求头的 value:jwt token串
 
  • 验证失败会有以下错误:

4. 签发和认证源码分析

4.1 签发源码分析

突破口:

登录接口,路由匹配成功,执行obtain_jwt_token---》发送post请求---》

from rest_framework_jwt.views import obtain_jwt_token
path('login/',obtain_jwt_token),

# Ctrl+点击obtain_jwt_token,查看源码
obtain_jwt_token = ObtainJSONWebToken.as_view()
refresh_jwt_token = RefreshJSONWebToken.as_view()
verify_jwt_token = VerifyJSONWebToken.as_view()

# 执行ObtainJSONWebToken的post方法,继承JSONWebTokenAPIView
    def post(self, request, *args, **kwargs):
        # 实例化得到序列化类
        serializer = self.get_serializer(data=request.data)
        # 做校验:字段自己,局部钩子,全局钩子
        if serializer.is_valid():
            # user:当前登录用户
            user = serializer.object.get('user') or request.user
            # 签发的token
            token = serializer.object.get('token')
            # 构造返回格式,咱们可以自定制
            response_data = jwt_response_payload_handler(token, user, request)
            response = Response(response_data)
            if api_settings.JWT_AUTH_COOKIE:
                expiration = (datetime.utcnow() +
                              api_settings.JWT_EXPIRATION_DELTA)
                response.set_cookie(api_settings.JWT_AUTH_COOKIE,
                                    token,
                                    expires=expiration,
                                    httponly=True)
            #最终返回了咱们定制的返回格式
            return response

        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

  # 如何得到user,如何签发的token----》在序列化类的全局钩子中得到的user和签发的token
    -JSONWebTokenSerializer---全局钩子---validate
    #前端传入,校验过后的数据---》{"username":"lqz","password":"lqz1e2345"}
    def validate(self, attrs):
        credentials = {
            # self.username_field: attrs.get(self.username_field),
            'username':attrs.get('username')
            'password': attrs.get('password')
        }

        if all(credentials.values()):
            # auth 模块,authenticate 可以传入用户名、密码,如果用户存在,就返回用户对象,如果不存就是None
            # 正确的用户
            user = authenticate(**credentials)

            if user:
                # 校验用户是否是活跃用户,如果禁用了,不能登录成功
                if not user.is_active:
                    msg = _('User account is disabled.')
                    raise serializers.ValidationError(msg)
                # 荷载----》通过user得到荷载   {id,name,email,exp}
                payload = jwt_payload_handler(user)
                
                return {
                    # jwt_encode_handler通过荷载得到token串
                    'token': jwt_encode_handler(payload),
                    'user': user
                }
            else:
                msg = _('Unable to log in with provided credentials.')
                raise serializers.ValidationError(msg)
        else:
            msg = _('Must include "{username_field}" and "password".')
            msg = msg.format(username_field=self.username_field)
            raise serializers.ValidationError(msg)

从源码中得到两个重要信息:

1 通过user得到荷载:payload = jwt_payload_handler(user)
2 通过荷载签发token:jwt_encode_handler(payload)

 

补充小知识点:

翻译函数:from django.utils.translation import ugettext as _

只要在配置文件中将语言配置改成了其他国的语言,前面加上_即可将原文翻译成相应的语言展示

例:

配置文件更改语言环境:

# 找到项目级别里面的setting文件,修改如下配置

"""
LANGUAGE_CODE = 'zh-hans'

TIME_ZONE = 'Asia/Shanghai'
"""

# 注意:如果是在drf里面使用,则需要对drf进行注册
INSTALLED_APPS = [
    'rest_framework',
]
msg = _('Unable to log in with provided credentials.')

# 会将括号里的文字自动翻译成中文显示

4.2 认证源码分析

突破口:

认证的时候用到了JSONWebTokenAuthentication

from rest_framework_jwt.authentication import JSONWebTokenAuthentication

# JSONWebTokenAuthentication---->父类BaseJSONWebTokenAuthentication----》authenticate方法
    def authenticate(self, request):
        # 前端带在请求头中的token 值
        jwt_value = self.get_jwt_value(request)
        # 如果没有携带token,就不校验了
        if jwt_value is None:
            return None

        try:
            # jwt_value就是token
            # 通过token,得到荷载,中途会出错
            # 出错的原因:
                -篡改token
                -过期了
                -未知错误
            payload = jwt_decode_handler(jwt_value)
        except jwt.ExpiredSignature:
            msg = _('Signature has expired.')
            raise exceptions.AuthenticationFailed(msg)
        except jwt.DecodeError:
            msg = _('Error decoding signature.')
            raise exceptions.AuthenticationFailed(msg)
        except jwt.InvalidTokenError:
            raise exceptions.AuthenticationFailed()

        # 如果能顺利解开,没有被异常捕获,说明token是可以信任的,payload就可以使用,通过payload得到当前登录用户
        user = self.authenticate_credentials(payload)
        # 返回当前登录用户,token
        return (user, jwt_value)
     # jwt_value = self.get_jwt_value(request)
    def get_jwt_value(self, request):
        # 拿到了前端请求头中传入的 jwt dasdfasdfasdfa
        # auth=[jwt,asdfasdfasdf]
        auth = get_authorization_header(request).split()
        # 'jwt'
        auth_header_prefix = api_settings.JWT_AUTH_HEADER_PREFIX.lower()

        if not auth:
            # 请求头中如果没带,去cookie中取
            if api_settings.JWT_AUTH_COOKIE:
                return request.COOKIES.get(api_settings.JWT_AUTH_COOKIE)
            return None

        if smart_text(auth[0].lower()) != auth_header_prefix:
            return None

        if len(auth) == 1:
            msg = _('Invalid Authorization header. No credentials provided.')
            raise exceptions.AuthenticationFailed(msg)
        elif len(auth) > 2:
            msg = _('Invalid Authorization header. Credentials string '
                    'should not contain spaces.')
            raise exceptions.AuthenticationFailed(msg)

        return auth[1]
  
 # 认证类配置了,如果不传jwt,不会校验,一定配合权限类使用

 

5. 自定义用户表签发 token

在前面使用的方式都需要依赖 Django auth 提供的用户表或者继承了该表的拓展表。我们可以自定义一个用户表进行签发 token。

  • 用户表
class User(models.Model):
    username = models.CharField(max_length=32)
    password = models.CharField(max_length=32)
  • 视图函数
from app01 import models
from rest_framework_jwt.settings import api_settings
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER

class User(APIView):
    def post(self, request):
        username = request.data.get('username')
        password = request.data.get('password')
        user = models.User.objects.filter(username=username, password=password).first()
        if user:
            # 登录成功签发token
            payload = jwt_payload_handler(user)  # 根据当前登录用户获取荷载
            print(payload)
            token = jwt_encode_handler(payload)  # 根据荷载生成token

            return Response({'code': 100, 'msg': '登录成功', 'token': token})
        else:
            return Response({'code': 101, 'msg': '用户名或密码错误'})
 
 

 

6. 自定义认证类验证 token

  • 认证类
from rest_framework_jwt.settings import api_settings
from rest_framework import exceptions
from app01 import models

jwt_decode_handler = api_settings.JWT_DECODE_HANDLER
jwt_get_username_from_payload = api_settings.JWT_PAYLOAD_GET_USERNAME_HANDLER


class LoginAuth(BaseAuthentication):
    def authenticate(self, request):
    
        # 1 取出 token
        jwt_value = request.META.get('HTTP_TOKEN')
        
        # 2 验证token是否合法
        # try:
        #     payload = jwt_decode_handler(jwt_value)
        # except jwt.ExpiredSignature:
        #     msg = 'token过期了'
        #     raise exceptions.AuthenticationFailed(msg)
        # except jwt.DecodeError:
        #     msg = 'token解码错误'
        #     raise exceptions.AuthenticationFailed(msg)
        # except jwt.InvalidTokenError:
        #     msg = '解析token未知错误'
        #     raise exceptions.AuthenticationFailed(msg)
        
        try:
            payload = jwt_decode_handler(jwt_value)
        except Exception:
            raise exceptions.AuthenticationFailed('token错误')

        print(payload)  # 荷载 user_id
        
        user = models.User.objects.filter(pk=payload['user_id']).first()
        return user, jwt_value
 
  • 视图函数

添加认证类

authentication_classes = [LoginAuth, ]
 
  • 使用的时候,键名为 token,键值为 token 串,并且不需要加上 jwt

      





自定义签发 token

from rest_framework.viewsets import ViewSet
from rest_framework.response import Response
import json
import hashlib
import time
import base64
from django.contrib import auth
from rest_framework.exceptions import AuthenticationFailed
from app01 import models


class Login(ViewSet):
    def get_header(self):
        header_dict = {'key': 'base64'}
        header_dict = json.dumps(header_dict)
        return base64.b64encode(header_dict.encode('utf8'))

    def get_body(self, request):
        try:
            username = request.data.get('username')
            password = request.data.get('password')
        except Exception:
            raise Exception('用户出现异常')
        user = auth.authenticate(username=username, password=password)

        if not user:
            raise AuthenticationFailed('账号或密码错误')

        body_dict = {'user_id': user.pk, 'username': user.username, 'exp': time.time() + 100}
        return base64.b64encode(json.dumps(body_dict).encode('utf8'))

    def get_key(self):
        key = 'key'
        return key

    def get_token(self, request):
        header = self.get_header().decode('utf8')
        body = self.get_body(request).decode('utf8')
        key = self.get_key()

        signature_dict = {
            'header': header,
            'payload': body,
            'secret': key
        }

        signature_dict = json.dumps(signature_dict)

        h = hashlib.md5()
        h.update(bytes(signature_dict, encoding='utf8'))
        signature = h.hexdigest()

        token = f'{header}.{body}.{signature}'
        return token

    def login(self, request):
        token = self.get_token(request)
        return Response({'status': 100, 'msg': '登录成功', 'token': token})
 

自定义认证 token

from rest_framework.authentication import BaseAuthentication
import hashlib
import base64
import json
import time
from app01 import models
from rest_framework.exceptions import AuthenticationFailed


class LoginAuthenication(BaseAuthentication):
    def get_key(self):
        key = 'key'
        return key

    def authenticate(self, request):
        token = request.META.get('HTTP_TOKEN')
        if not token:
            raise AuthenticationFailed('未提供 token 认证信息')

        header, body, signature = token.split('.')

        signature_dict = {
            'header': header,
            'payload': body,
            'secret': self.get_key()
        }
        signature_dict = json.dumps(signature_dict)

        h = hashlib.md5()
        h.update(bytes(signature_dict, encoding='utf8'))
        res = h.hexdigest()

        if res != signature:
            raise AuthenticationFailed('认证异常')

        body = json.loads(base64.b64decode(bytes(body, encoding='utf8')))
        user_id = body.get('user_id')
        user = models.MyUser.objects.filter(pk=user_id).first()

        now_time = time.time()
        old_time = body.get('exp')

        if now_time > old_time:
            raise AuthenticationFailed('token 已过期')

        return user, token
 
 

6. 自定义 JWT 过期时间

import datetime
JWT_AUTH = {
    # 过期时间1天
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1),
}
 

 

posted @ 2023-02-10 17:42  莫~慌  阅读(214)  评论(0编辑  收藏  举报