Fork me on GitHub

-drf-JWT认证

一 认证机制对比

在用户注册或登录后,我们想记录用户的登录状态,或者为用户创建身份认证的凭证。我们不再使用Session认证机制,而使用Json Web Token(本质就是token)认证机制。

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

'''

二 构成和工作原理

1 JWT的构成

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

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

第一部分我们称它为头部(header),第二部分我们称其为载荷(payload),第三部分是签证(signatrue)

1)header

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

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

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

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

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

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

2)payload

载荷就是存放有效信息的地方,这些有效信息包含三个内容:

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

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

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

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

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

定义一个payload:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

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

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

3)signature

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

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

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

// javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);

var signature = HMACSHA256(encodedString, 'secret'); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

将这三部分用 . 连接成一个完整的字符串,构成了最终的JWT:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

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

2 本质原理

jwt认证算法:签发与校验

'''
1)jwt分三段式:头.体.签名 (head.payload.sign)
2)头和体是可逆加密,让服务器可以反解出user对象;签名是不可逆加密,保证整个token的安全性
3)头体签名三部分,都采用json格式的字符串,进行加密,可逆加密一般采用base64算法,不可逆加密一般采用hash(md5)算法
4)头中的内容是基本信息:公司信息、项目组信息、token采用的加密方式信息 进行base64加密
{
	"company": "公司信息",
	...
}
5)体中的内容是关键信息:用户主键、用户名、签发时客户端信息(设备号、地址)、过期时间 进行base64加密
{
	"user_id": 1,
	...
}
6)签名中的内容时安全信息:头的加密结果 + 体的加密结果 + 服务器不对外公开的安全码 进行md5加密
{
	"head": "头的加密字符串",
	"payload": "体的加密字符串",
	"secret_key": "安全码"
}

加密时都是jason格式进行加密处理
'''

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

'''
1)基本信息存储到json字典中,然后采用base64算法加密得到 头字符串

2)关键信息存储到json字典中,然后采用base64算法加密得到 体字符串

3)用头、体加密字符串再加安全码信息存储json字典,采用hash md5算法加密得到 签名字符串

账号密码就能根据User表得到user对象,形成的三段字符串用 . 拼接成token返回给前台

'''

校验:根据客户端带token的请求 反解出user对象

'''
1)将token按 . 拆分为三段字符串,第一段 头加密字符串 一般不需要做任何处理(因为不需要反解获取信息)

2)第二段 体加密字符串,要反解出用户主键,通过主键从User表中就能得到登录用户,过期时间和设备信息都
是安全信息,确保token没过期,且是同一设备来的(相比session与cookie安全多,因为这需要盗取token情况下还需要控制之前登录的主机进行登录操作),之后再进行base64加密便于之后签名字符串的校验

3)再用 第一段 + 第二段 + 服务器安全码 不可逆md5加密 与第三段 签名字符串进行碰撞校验,通过后才能代表第二段校验得到的user对象就是合法的登录用户(这样通过算法进行token校验,免去了从数据库取出token进行校验的操作,实现了一定的高并发)此时校验成功才代表你是之前登录过的用户,因为base64是可以在客户端被反解的,需要校验有没有存在在客户端被反解后修改信息的行为
'''

3 drf项目的jwt认证开发流程(重点)

  • 用账号密码访问登录接口,登录接口逻辑中调用 签发token 算法,得到token,返回客户端,客户端自己存到cookies中
  • 校验token的算法应该写在认证类中(在认证类中调用),全局配置给认证组件,所有视图类请求,都会进行认证校验,所以请求带了token,就会反解出user对象,在视图类中用request.user就能访问登录用户了

注意:登录接口需要做到 认证 + 权限 ,因为只有登录了才能产生token有权访问其他接口(注册接口也是)

补充base64编码解码

import base64
import json
dic_info={
  "sub": "1234567890",
  "name": "lqz",
  "admin": True
}
byte_info=json.dumps(dic_info).encode('utf-8')
# base64编码
base64_str=base64.b64encode(byte_info)
print(base64_str)
# base64解码
base64_str='eyJzdWIiOiAiMTIzNDU2Nzg5MCIsICJuYW1lIjogImxxeiIsICJhZG1pbiI6IHRydWV9'
str_url = base64.b64decode(base64_str).decode("utf-8")
print(str_url)

三 drf-jwt安装和简单使用

1 官网

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

2 安装

pip install djangorestframework-jwt

3 简单使用

# 1 创建超级用户
python3 manage.py createsuperuser

# 2 配置路由
from django.urls import path
from rest_framework_jwt.views import obtain_jwt_token
urlpatterns = [
    path('login/', obtain_jwt_token),
]

# 3 postman发送登录请求,成功之后返回生成的token

# 4 用户登录了才能访问某个接口
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.permissions import IsAuthenticated
# 有内置的认证模块,直接导入使用局部使用即可

# 只是认证,不能限制匿名用户访问,因为源码中对匿名用户的条件并没有raise报错
# 而是return None
# 所以要配合权限,是否登录的权限去限制
class BookInfo(APIView):
    authentication_classes = [JSONWebTokenAuthentication, ]
    permission_classes = [IsAuthenticated, ]
    def get(self,request):
        return Response('查看书籍')

如图在请求头加上这样的键值对,注意的是values 中 需要jwt空格后输入token值

# 5 用户是否登录都可访问某个接口,区别的是登录了的产生了user对象
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
class BookInfo(APIView):
    authentication_classes = [JSONWebTokenAuthentication, ]
    def get(self,request):
        return Response('查看书籍')

四 Dango auth的User表自动签发

1 自定义认证返回结果(需要在setting中配置)

# utils.py
from app10.seralizer import UserSerializer

def jwt_response_payload_handler(token, user=None, request=None):
    return {
        'status': 0,
        'msg': 'ok',
        'data': {
            'token': token,
            'user': UserSerializer(user).data
        }
    }

2 配置setting.py

import datetime
JWT_AUTH = {
    # 过期时间1天
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1),
    # 自定义认证结果:见下方序列化user和自定义response
  	# 如果不自定义,返回的格式是固定的,只有token字段
    'JWT_RESPONSE_PAYLOAD_HANDLER': 'app10.utils.jwt_response_payload_handler',
}

配置后登录的返回结果

3 自定义基于JWT的认证类

from rest_framework.exceptions import AuthenticationFailed
from rest_framework_jwt.authentication import jwt_decode_handler
from rest_framework_jwt.authentication import BaseJSONWebTokenAuthentication
import jwt


class JSONWebTokenAuthentication(BaseJSONWebTokenAuthentication):
    def authenticate(self, request):
        # token信息放在请求头,请求地址中
        token = request.META.get('HTTP_Authorization'.upper())
        # 检验token是否合法

        try:
            payload = jwt_decode_handler(token)
            # 解码荷载,得到user_id
        except jwt.ExpiredSignature:
            raise AuthenticationFailed('过期了')
        except jwt.DecodeError:
            raise AuthenticationFailed('解码错误')
        except jwt.InvalidTokenError:
            raise AuthenticationFailed('不合法的token')
        # token认证不过直接报错,杜绝了匿名用户访问该接口
        user = self.authenticate_credentials(payload)
        # 认证通过得到user对象
        return user, token
# 在视图类中配置 局部配置
	authentication_classes = [JSONWebTokenAuthentication, ]

自定义的,所以没有配置jwt的前缀,那么就可以不用使用jwt的前缀了

全局使用

# setting.py
REST_FRAMEWORK = {
    # 认证模块
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'users.app_auth.JSONWebTokenAuthentication',
    ),
}

# 全局使用下进行局部禁用
# 视图类下定义
authentication_classes = []

五 基于jwt的多方式登录

1 手机+密码 用户名+密码 邮箱+密码
2 流程分析(post请求)
	-路由:自动生成
  -视图类:ViewSet(ViewSetMixin, views.APIView)
  -序列化类:重写validate方法,在这里对用户名和密码进行了校验

路由

path('login/', views.LoginUser.as_view({'post': 'login'}))

序列化器

from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from rest_framework_jwt.utils import jwt_encode_handler, jwt_payload_handler
from app10 import models
import re

class LoginSerializer(serializers.ModelSerializer):
    username = serializers.CharField()
    # 重写username,因为输入的用户名可能并不是username,而是手机号或是邮箱
    class Meta:
        model = models.UserInfo
        fields = ['username', 'password']
        extra_kwargs = {'password': {'write_only': True}}

    def validate(self, attrs):
        # 该username是上步重写的username
        username = attrs.get('username')
        password = attrs.get('password')

        # 如果是手机号
        if re.match('^1[3-9]\d{9}$',username):
            # 以手机号登录
            user = models.UserInfo.objects.filter(mobile=username).first()
        elif re.match('^.+@.+$', username):
            # 以邮箱登录
            user =models.UserInfo.objects.filter(email=username).first()
        else:
            # 以用户名登录
            user =models.UserInfo.objects.filter(username=username).first()
        # 再进行输入用户与数据库的数据校验

        # 如果user 有值即用户名校验正确,且密码正确
        if user and user.check_password(password):
            # 登录成功后生成token
            # drf-jwt中有通过user对象生成token的方法
            payload = jwt_payload_handler(user)
            token = jwt_encode_handler(payload)
            # token要在视图类中使用
            # 所以视图类和序列化类之间通过context这个字典传递数据
            self.context['token'] = token
            self.context['username'] = user.username
            return attrs

        else:
            raise ValidationError('用户名或密码错误')

视图

class LoginUser(ViewSet):
    def login(self, request):
        # 实例化得到一个序列化对象,将data数据传入到序列化器中进行逻辑处理
        ser = seralizer.LoginSerializer(data=request.data)
        # 序列化类的对象的校验方法
        ser.is_valid(raise_exception=True)
        # 没有断言报错则是登录成功,返回手动签发token
        token = ser.context.get('token')
        username = ser.context.get('username')
        # return APIResponse(token=token,username=username)
        return APIResponse(token=token, username=username)

封装Response

from app10.seralizer import UserSerializer
from rest_framework.response import Response

# 这是默认的drf-jwt登录后返回自定义
def jwt_response_payload_handler(token, user=None, request=None):
    return {
        'status': 0,
        'msg': 'ok',
        'data': {
            'token': token,
            'user': UserSerializer(user).data
        }
    }

# 这是自定义登录类的返回自定义
class APIResponse(Response):
    def __init__(self, code=100, msg='成功', data=None, status=None, headers=None, content_type=None, **kwargs):
        dic = {'code': code, 'msg': msg}
        if data: # 一般是指序列化返回的值

            dic['data'] = data

        dic.update(kwargs)  # 这里使用update
				# token值在这里接收并以键值对进入字典中,然后赋值给父类的data
        # 这样返回到前端的数据则是父类的data
        super().__init__(data=dic, status=status,
                         template_name=None, headers=headers,
                         exception=False, content_type=content_type)

posted @ 2020-11-17 18:11  artherwan  阅读(93)  评论(0编辑  收藏  举报