JWT认证

内容概要

  • cookie,session,token的区别
  • Json web token (JWT) 简介
  • base64编码和解码
  • jwt的签发和认证
  • django中快速使用jwt
  • jwt定制返回格式
  • jwt源码分析

内容详细

参考博客:https://www.cnblogs.com/48xz/p/16123916.html

cookie,session,token的区别

cookie:直接在浏览器上存储用户信息,很不安全

session:把用户信息存在后端的数据库中,只把对应的字符串放回给前端存放,但需要频繁操作数据库

token:把用户信息加密之后再存放在浏览器中,登录认证的时候把信息重新加密并校验,在一定程度上保证了数据安全,并且不用频繁操作数据库,不给后端很大压力

Json web token (JWT) 简介

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

Json web token (JWT),token是一种认证机制,用在web开发方向,叫JWT

JWT的构成

JWT由3部分组成:标头(Header)、有效载荷(Payload)和签名(Signature)。在传输的时候,会将JWT的3部分分别进行Base64编码后用.进行连接形成最终传输的字符串。比如:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

1. header

JWT头是一个描述JWT元数据的JSON对象,alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256);typ属性表示令牌的类型,JWT令牌统一写为JWT。最后,使用Base64 URL算法将上述JSON对象转换为字符串保存

第一段头部承载的信息:

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

完整的头部就像下面这样的JSON:

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

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

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

2. payload

第二段是载荷,载荷就是存放有效信息的地方,是JWT的主体内容部分,也是一个JSON对象,承载的信息:

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

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

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

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

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

除以上标准注册声明字段外,我们还可以自定义字段,一般会把包含用户信息的数据放到payload中,如下例:

{
  "sub": "1234567890",
  "name": "HammerZe",
  "admin": true
}

注意:虽然说用户信息数据可以存放到payload中,但是默认情况下JWT是未加密的,Base64算法也只是编码并不会提供安全的加密算法,一般程序员拿到Base64编码的字符串都可以解码出内容,所以不要存隐私信息,比如密码,防止泄露,存一些非敏感信息

3. signature

签名哈希部分是对上面两部分数据签名,需要使用base64编码后的header和payload数据,通过指定的算法生成哈希,以确保数据不会被篡改。首先,需要指定一个密钥(secret)。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用header中指定的签名算法(默认情况下为HMAC SHA256)根据以下公式生成签名

HMACSHA256(base64UrlEncode(header) + “.” + base64UrlEncode(payload), secret)

简单的说第三段是签证信息,这个签证信息由三部分组成:

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

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

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

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

总结

注意JWT每部分的作用,在服务端接收到客户端发送过来的JWT token之后:

header和payload可以直接利用base64解码出原文,从header中获取哈希签名的算法,从payload中获取有效数据

signature由于使用了不可逆的加密算法,无法解码出原文,它的作用是校验token有没有被篡改。服务端获取header中的加密算法之后,

base64编码和解码

base64 只是用来做编码和解码的工具,不能加密,加密用 hash 或者 sha256 等模块加密

使用:

编码:

import base64


dic = {'name': 'elijah', 'id': 1}
r1 = str(dic).encode('utf8')
res = base64.b64encode(r1)

# 打印结果
b'eyduYW1lJzogJ2VsaWphaCcsICdpZCc6IDF9'

解码:

import base64


req = base64.b64decode(b'eyduYW1lJzogJ2VsaWphaCcsICdpZCc6IDF9')
print(req)

# 打印结果
b"{'name': 'elijah', 'id': 1}"

jwt的签发和认证

jwt包含头、荷载、数字签名三部分

签发:

如果没有第三方模块,我们需要自己做

  • 1、头,用基本信息和公司信息存储为json字典,再用base64编码
  • 2、荷载,用户id、用户名、过期时间和其它关键信息存储为json字典,再用base64编码
  • 3、签名,用头和荷载拼接起来再通过加密算法和密钥加密得到
  • 4、三个部位用点拼接在一起然后返回给前端

认证:

访问需要登陆的接口

  • 1、将token按 . 拆分为三段字符串
  • 2、第一段(头)和第二段(荷载)使用base64解码,确保token没过期
  • 3、重新把头和荷载拼接起来再通过加密算法和密钥加密得到密文,然后跟第三段(签名)进行比较,相同则表示为合法用户。信息被篡改则校验失败

注意:

大部分的web框架都会有第三方模块支持

django中的jwt第三方模块:

1、django-rest-framework-jwt

下载地址, https://github.com/jpadilla/django-rest-framework-jwt

2、django-rest-framework-simplejwt

下载地址,https://github.com/jazzband/djangorestframework-simplejwt

二者区别:https://blog.csdn.net/lady_killer9/article/details/103075076

django中快速使用jwt

签发:

第一步,安装 pip3 install djangorestframework-jwt

第二步,配置路由,登录时会签发 token

from rest_framework_jwt.views import obtain_jwt_token
urlpatterns = [
    path('token/', obtain_jwt_token),
]

第三步,使用接口测试工具发送post请求到后端,就能基于auth的user表签发token

测试:

image

base64反解信息:

import base64

print(base64.b64decode(b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9'))  # b'{"typ":"JWT","alg":"HS256"}'
# 解码荷载时,如果荷载位数不够,多加两个 “=”号
print(base64.b64decode(b'eyJ1c2VyX2lkIjozLCJ1c2VybmFtZSI6ImVsaWphaCIsImV4cCI6MTY1MjUwMzk2NywiZW1haWwiOiIifQ=='))
# b'{"user_id":3,"username":"elijah","exp":1652503967,"email":""}'

认证:

以前我们对某个视图类执行前进行认证是通过书写认证类来实现的

现在我们使用第三方模块

视图类配置:

from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.permissions import IsAuthenticated
class BookView(GenericViewSet,ListModelMixin):
    ···
     # JSONWebTokenAuthentication :rest_framework_jwt模块写的认证类
    authentication_classes = [JSONWebTokenAuthentication,]
    # 需要配合一个权限类
    permission_classes = [IsAuthenticated,]
    ···

测试:

前端发送请求时,需要在请求头中携带 token

格式: Authorization : jwt (空格) token串

image

jwt定制返回格式

登录之后默认默认的只会返回token串,但我们需要它返回更多数据

自定义签发token(登陆接口)返回格式:

第一步,新建utils.py文件写一个函数

utils.py

# 定义签发token(登陆接口)返回格式
def jwt_response_payload_handler(token, user=None, request=None):
    return {
        'code': 100,
        'msg': "登陆成功",
        'token': token,
        'username': user.username
    }

第二步,在配置文件中配置

jwt模块的配置文件,统一放在JWT_AUTH

JWT_AUTH = {
      'JWT_RESPONSE_PAYLOAD_HANDLER': 'app01.utils.jwt_response_payload_handler',
  }

jwt源码分析

签发源码分析

1.入口:path('login/', obtain_jwt_token)

2.obtain_jwt_token--->obtain_jwt_token = ObtainJSONWebToken.as_view()
ObtainJSONWebToken.as_view(),其实就是一个视图类.as_view()

3.ObtainJSONWebToken类源码
'''
class ObtainJSONWebToken(JSONWebTokenAPIView):
	serializer_class = JSONWebTokenSerializer
'''

4.登录签发token肯定需要一个post方法出来,但是ObtainJSONWebToken类内没有父类JSONWebTokenAPIView写了post方法:
    def post(self, request, *args, **kwargs):
        # 获取数据:{'username': 'Hammer', 'password': '7410'}
        serializer = self.get_serializer(data=request.data)
		# 校验
        if serializer.is_valid():
            user = serializer.object.get('user') or request.user # 获取用户
            token = serializer.object.get('token') # 获取token
            response_data = jwt_response_payload_handler(token, user, request) 
           #  {'code': 100, 'msg': '登陆成功', 'token': 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6IkhhbW1lciIsImV4cCI6MTY0OTU4MTU0NiwiZW1haWwiOiIifQ.2oAjKQ90SV2S9Yxrwppo7BwAOv0xFW4i4AHHBX5Cg2Q', 'username': 'Hammer'}
            response = Response(response_data)
            if api_settings.JWT_AUTH_COOKIE:
               ···
            return response # 定制什么返回什么

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

5.get_serializer(data=request.data)如何获取到用户数据?
JSONWebTokenSerializer序列化类中全局钩子中获取当前登录用户和签发token
···
payload = jwt_payload_handler(user)
                return {
                    'token': jwt_encode_handler(payload),
                    'user': user
                }
···

签发总结

从obtain_jwt_token开始, 通过ObtainJSONWebToken视图类处理,其实是父类JSONWebTokenAPIView的post方法通过传入的用户名和密码处理获取当前用户,签发了token

认证源码分析

# 视图类内认证类搭配权限类使用
    authentication_classes = [JSONWebTokenAuthentication, ]
    permission_classes = [IsAuthenticated, ]

我们在前面写过,如果需要认证肯定需要重写authenticate方法,这里从列表内的认证类作为入口分析:

'''认证类源码'''
class JSONWebTokenAuthentication(BaseJSONWebTokenAuthentication):
    www_authenticate_realm = 'api'

    def get_jwt_value(self, request):
        # 获取传入的Authorization:jwt token串,然后切分
        auth = get_authorization_header(request).split()
        auth_header_prefix = api_settings.JWT_AUTH_HEADER_PREFIX.lower()
		# 获取不到的情况
        if not auth:
            if api_settings.JWT_AUTH_COOKIE:
                return request.COOKIES.get(api_settings.JWT_AUTH_COOKIE)
            return None  # 直接返回None,也不会报错,所以必须搭配权限类使用

        ···

        return auth[1]  # 一切符合判断条件,通过split切分的列表索引到token串
'''认证类父类源码'''
def authenticate(self, request):
        jwt_value = self.get_jwt_value(request) # 获取真正的token,三段式,上面分析
        if jwt_value is None: # 如果没传token,就不认证了,直接通过,所以需要配合权限类一起用
            return None

        try:
            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()# 不知名的错误

        user = self.authenticate_credentials(payload)

        return (user, jwt_value)

签发源码内的其他两个类

导入from rest_framework_jwt.views import obtain_jwt_token,refresh_jwt_token,verify_jwt_token

obtain_jwt_token = ObtainJSONWebToken.as_view()  # 获取token
refresh_jwt_token = RefreshJSONWebToken.as_view()  # 更新token
verify_jwt_token = VerifyJSONWebToken.as_view()  # 认证token

refresh_jwt_token用法

# 配置文件
JWT_AUTH = {
    'JWT_ALLOW_REFRESH': True
}
# 路由
    path('refresh/', refresh_jwt_token)

image

verify_jwt_token用法

path('verify/', verify_jwt_token),

image

自定义User表,签发token

普通写法,视图类写

上面我们写道,签发token是基于Django自带的auth_user表签发,如果我们自定义User表该如何签发token,如下:

视图

# 自定义表签发token
from rest_framework.views import APIView
from rest_framework.viewsets import ViewSetMixin
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework_jwt.settings import api_settings
from app01 import models
class UserView(ViewSetMixin,APIView):
    @action(methods=['POST'],detail=False)
    def login(self,request):
        username = request.data.get('username')
        password = request.data.get('password')
        user = models.UserInfo.objects.filter(username=username,password=password).first()
        response_dict = {'code':None,'msg':None}
        # 源码copy错来使用
        jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
        jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
        if user:
            '''
            签发token去源码copy过来使用
            '''
            # 载荷字典
            payload = jwt_payload_handler(user)
            print(payload)
            # {'user_id': 1, 'username': 'Hammer', 'exp': datetime.datetime(2022, 4, 10, 13, 13, 15, 363206), 'email': '123@qq.com', 'orig_iat': 1649596095}
            # 通过荷载得到token串
            token = jwt_encode_handler(payload)
            response_dict['code'] = 2000
            response_dict['msg'] = '登录成功'
            response_dict['token'] = token

        else:
            response_dict['code'] = 4001
            response_dict['msg'] = '登录失败,用户名或密码错误'
        return Response(response_dict)

模型

# user表
class UserInfo(models.Model):
    username = models.CharField(max_length=32)
    password = models.CharField(max_length=32)
    email = models.EmailField()

路由

from rest_framework.routers import SimpleRouter
router = SimpleRouter()
router.register('user',views.UserView,'user')

image

序列化类中写逻辑

源码中签发校验都在序列化类中完成,这种写法确实比较常用,我们来使用这种方式自定义,将上面视图的校验逻辑写到序列化类中,这个序列化类只用来做反序列化,这样我们就可以利用 反序列化 的字段校验功能来帮助我们校验(模型中的条件),但是我们不做保存操作

视图

from .serializer import UserInfoSerializer
class UserView(ViewSetMixin,APIView):
    @action(methods=['POST'],detail=False)
    def login(self,request):
        # 如果想获取什么这里可以实例化对象写入,比如request
        serializer = UserInfoSerializer(data=request.data, context={'request': request})
        response_dict = {'code':None,'msg':None}
        # 校验,局部钩子,全局钩子都校验完才算校验通过,走自己的校验规则
        if serializer.is_valid():
            # 从序列化器对象中获取token和username
           token = serializer.context.get('token')
           username = serializer.context.get('username')

           response_dict['code']=2000
           response_dict['msg']='登录成功'
           response_dict['token'] = token
           response_dict['username'] = username
        else:
            response_dict['code'] = 4001
            response_dict['msg'] = '登录失败,用户名或密码错误'

        return Response(response_dict)

序列化器

from rest_framework.exceptions import ValidationError


class UserInfoSerializer(serializers.ModelSerializer):
    class Meta:
        model = UserInfo
        # 根据模型里的字段写
        fields = ['username', 'password']

    # 全局钩子
    def validate(self, attrs):
        # attrs是校验过的字段,这里利用
        username = attrs.get('username')
        password = attrs.get('password')
        user = UserInfo.objects.filter(username=username, password=password).first()

        from rest_framework_jwt.settings import api_settings
        jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
        jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER

        if user:  # 登录成功

            payload = jwt_payload_handler(user)  # 得到荷载字典
            token = jwt_encode_handler(payload)  # 通过荷载得到token串
            # 将token放入context字典中
            self.context['token'] = token
            self.context['username'] = username
            # context是serializer和视图类沟通的桥梁
            print(self.context.get('request').method)
        else:  # 登录失败
            raise ValidationError('用户名或密码错误')
        return attrs

image

总结

需要我们注意的是,context只是我们定义的字典,比如上面写到的实例化序列化类中指定的context,那么就可以从序列化类打印出请求的方法,context是序列化类和视图类沟通的桥梁

自定义认证类

auth.py

import jwt
from django.utils.translation import ugettext as _
from rest_framework import exceptions
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
from rest_framework_jwt.settings import api_settings
from .models import UserInfo


class JWTAuthentication(BaseAuthentication):
    def authenticate(self, request):
        # 第一步、取出传入的token,从请求头中取

        # 这里注意,获取的时候格式为:HTTP_请求头的key大写
        jwt_value = request.META.get('HTTP_TOKEN')
        jwt_decode_handler = api_settings.JWT_DECODE_HANDLER
        # 验证token:验证是否过期,是否被篡改,是否有其他未知错误,从源码copy过来使用
        if jwt_value:
            try:
                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:
                msg = _('Unknown Error.')
                raise exceptions.AuthenticationFailed(msg)

            # 第二部、通过payload获得当前登录用户,本质是用户信息通过base64编码到token串的第二段载荷中
            user = UserInfo.objects.filter(pk=payload['user_id']).first()
            # 返回user和token
            return (user, jwt_value)
        else:
            raise AuthenticationFailed('No token was detected')

PYTHON 复制 全屏

视图

from rest_framework.viewsets import ModelViewSet
from .models import Book
from .serializer import BookSerializer
from .auth import JWTAuthentication
class BookView(ModelViewSet):
    queryset = Book.objects.all()
    serializer_class = BookSerializer
    authentication_classes = [JWTAuthentication,]

序列化器

class BookSerializer(serializers.ModelSerializer):
    class Meta:
        model = Book
        fields = '__all__'

路由

from rest_framework.routers import SimpleRouter
router = SimpleRouter()
router.register('book',views.BookView,'book')

正常的情况

image

不携带token的情况

image

总结

  • 从请求头中获取token,格式是HTTP_KEY,key要大写
  • 认证token串没有问题,返回用户信息从载荷中获取,本质是用户信息通过base64编码到token串的第二段载荷中,可以通过base64解码获取到用户信息

补充:HttpRequest.META

HTTP请求的数据在META中

HttpRequest.META

   一个标准的Python 字典,包含所有的HTTP 首部。具体的头部信息取决于客户端和服务器,下面是一些示例:
  取值:

    CONTENT_LENGTH —— 请求的正文的长度(是一个字符串)。
    CONTENT_TYPE —— 请求的正文的MIME 类型。
    HTTP_ACCEPT —— 响应可接收的Content-Type。
    HTTP_ACCEPT_ENCODING —— 响应可接收的编码。
    HTTP_ACCEPT_LANGUAGE —— 响应可接收的语言。
    HTTP_HOST —— 客服端发送的HTTP Host 头部。
    HTTP_REFERER —— Referring 页面。
    HTTP_USER_AGENT —— 客户端的user-agent 字符串。
    QUERY_STRING —— 单个字符串形式的查询字符串(未解析过的形式)。
    REMOTE_ADDR —— 客户端的IP 地址。
    REMOTE_HOST —— 客户端的主机名。
    REMOTE_USER —— 服务器认证后的用户。
    REQUEST_METHOD —— 一个字符串,例如"GET" 或"POST"。
    SERVER_NAME —— 服务器的主机名。
    SERVER_PORT —— 服务器的端口(是一个字符串)。
   从上面可以看到,除 CONTENT_LENGTH 和 CONTENT_TYPE 之外,请求中的任何 HTTP 首部转换为 META 的键时,
    都会将所有字母大写并将连接符替换为下划线最后加上 HTTP_  前缀。
    所以,一个叫做 X-Bender 的头部将转换成 META 中的 HTTP_X_BENDER 键。
posted @ 2022-05-14 13:33  elijah_li  阅读(57)  评论(0编辑  收藏  举报
//一下两个链接最好自己保存下来,再上传到自己的博客园的“文件”选项中