JWT相关知识点


一、jwt介绍和原理

通过上面的第二部分的发展史,我们可以得知目前我们的校验方式的本质就是给前端发cookie,但是后端不存,这个cookie的本质分成三块:请求头、用户的信息以及充当cookie的唯一码、以及签名,这个签名就是签名两部分的内容加密后的东西,这样后端就不用存储session了,前端发送请求的时候只需要把签名解密,然后跟请求头还有内部的用户信息以及cookie进行对比就能校验,并且安全性也比较高。

概念

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

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

构成与工作原理

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

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature).

1.header

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

  • 声明类型,这里是jwt
  • 声明加密的算法 通常直接使用 HMAC SHA256
  • 还可能有公司信息

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

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

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

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

2.payload

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

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

主要的信息如下

-荷载:payload
	-存放有效信息的地方
	-过期时间
	-签发时间
	-用户id
	-用户名字等

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

  • 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了。

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

文档网站:http://getblimp.github.io/django-rest-framework-jwt/

本质原理

jwt认证算法:签发与校验

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

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

"""
1)用基本信息存储json字典,采用base64算法加密得到 头字符串
2)用关键信息存储json字典,采用base64算法加密得到 体字符串
3)用头、体加密字符串再加安全码信息存储json字典,采用hash md5算法加密得到 签名字符串

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

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

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

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

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

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

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

补充base64编码解码

ps:base64编码后,字符长度一定是4的倍数,如果不是,使用 = 补齐, = 不表示数据,不补齐会报错

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)

base64 应用场景

'''
1 jwt 使用了base64
2 网络中传输数据,也会经常使用 base64编码
3 网络传输中,有的图片使用base64编码
'''
s='去网上找,比如12306的图片都是用base64加密过的,找到他的地址去掉前面两部分就能得到base64加密后的字符串'
res=base64.b64decode(s)
with open('a.png','wb') as f:
    f.write(res)

drf-jwt安装和简单使用

1.1 官网

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

1.2 安装

pip install djangorestframework-jwt

1.3 使用:

# 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空格)

二、drf-jwt快速使用

django+drf 平台开发jwt这套,有两个模块,加上自定义的,总共有三种方式

	-djangorestframework-jwt  ---》一直可以用(但是已经停止维护了,可是内部逻辑仍然是对的,可以使用,如果面试官问起来,咋们就说咋们用的是我们自己自定义的就行了,顺手装一波)
    -djangorestframework-simplejwt---》公司用的多---》我们可以自己尝试一下
    -自己封装jwt签发和认证

使用步骤

步骤一:安装djangorestframework-jwt模块

步骤二:快速签发token

urls.py

'需要在接口文档代码的基础上添加一些配置'
from rest_framework_jwt.views import obtain_jwt_token

'然后要添加一条路由用于登陆(这里因为我们用的是别人的模块,所以要按别人的意思来,如果我们需要重写这个模块对应的user表,就需要自己编写jwt的认证代码了)'

urlpatterns = [
    path('admin/', admin.site.urls),
    path('docs/', include_docs_urls(title='zzh的项目接口文件')),
    path('api/v1/', include(router.urls)),
    path('login/', obtain_jwt_token),
]

步骤三:postman发送请求

http://127.0.0.1:8000/login/发送post请求,携带username和password(这两个变量的名称需要跟User表中的字段名称一致)

三、定制返回格式

上文我们也提到了,如果是基于auth的User表签发token,就可以不自己写了对应的登陆方法了,但是登录接口返回的格式,只有token,不符合公司规范,因此我们就有了定制返回格式的需求

使用步骤

步骤一:重写一个jwt_response_payload_handler函数,定制返回的格式(我们是通过查看jwt的源码发现他是定义返回格式的,他的注释中给我们提示了返回的格式应该是什么形式,我们在内部添加数据即可)

在app中创建一个utils.py

        def jwt_response_payload_handler(token, user=None, request=None):
            return {
                'code': 100,
                'msg': '登录成功',
                'token': token,
                'username': user.username
                # 'icon':user.icon
            }

步骤二:编写了这个自定义的方法后,我们需要在注册文件中更改配置信息

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

步骤三:使用postman测试,就能看到返回的格式了

image

四、jwt的认证类

接口要登录后才能访问、使用,jwt内部有一个认证类,如果我们要使用他的认证类就必须要配合着drf的一个权限类一起使用。

同时这个认证类的校验方式也有点特别,当我们传入token的时候,他会进行校验,但是当我们不传入token 的时候就直接不校验了。

使用方式

视图层(views.py)中导入认证类和权限类然后再视图类中使用

from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.permissions import IsAuthenticated

class BookView(ModelViewSet):
    """
    你是猪
    """
    queryset = Book.objects.all()
    serializer_class = BookSerializer
    authentication_classes = [JSONWebTokenAuthentication]
    permission_classes = [IsAuthenticated]

通过查看IsAuthenticated的源码我们可以发现这个权限类其实就是用来判断我们是否登陆的。

测试

当我们在测试的时候需要注意,我们登陆后会获得一串字符,而我们后面在用各种请求进行查看图书等操作时,需要在请求头中添加字段:Authorization,以及对应的值,而这个值就是登陆后得到的字符串

jwt eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6Inp6aCIsImV4cCI6MTY3NTk0NTY1MSwiZW1haWwiOiI5NzY4NjM0MjlAcXEuY29tIn0.auQ1NDp-WQwKGh2Tg9rlKUOZt3kaUzL-oKLloHlPT0Q

我们可以看到,他是jwt三个字母开头,然后中间接一个空格,然后再接字符串

image

五、jwt配置文件

需要记住的配置信息

'JWT_RESPONSE_PAYLOAD_HANDLER': 'app01.utils.jwt_response_payload_handler',
我们通过之前的学习可以得知jwt_response_payload_handler这个方法是定制返回信息的格式的,但是因为要使用我们自定义的方法所以需要在配置信息中注册,这里就是注册信息

'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=300),
这里是设置jwt认证的过期时间的

需要了解的配置信息

'JWT_AUTH_HEADER_PREFIX': 'JWT',
当我们在用postman朝接口发送请求的时候我们需要在Authorization中输入jwt,同时前面需要写jwt加空格来拼接,这里就是设置开头拼接字符的配置

'JWT_SECRET_KEY': settings.SECRET_KEY,
这就是密匙,不重要但是很关键

六、drf-jwt源码执行流程(了解)

6.0 auth的user表的补充知识

1、django 的auth user表,密码是加密的,即便的同样的密码,密文都不一样

-每次加密,都随机生成一个盐,把盐拼在加密后的串中
# 比如
pbkdf2_sha256$260000$B9ZRmPFpWb3H4kdDDmgYA9$CM3Q/ZfYyXzxwvjZ+HOcdovaJS7681kDsW77hr5fo5o=

明文:lqz12345 
盐:B9ZRmPFpWb3H4kdDDmgYA9

后期来了明文lqz12345

2、自定义用户表,生成密码用密文

这里老师点了一些,没有展开讲(不是重点内容,建议课后自行研究),对于密码的加密,用的是make_password方法。

from django.contrib.auth.models import AbstractUser
'我们从auth的models中找相关的方法'

make_password被使用与下方的方法中,通过代码我们可以得知他就是对密码进行了处理

class UserManager(BaseUserManager):
    use_in_migrations = True

    def _create_user(self, username, email, password, **extra_fields):
        """
        Create and save a user with the given username, email, and password.
        """
        if not username:
            raise ValueError('The given username must be set')
        email = self.normalize_email(email)
        # Lookup the real model class from the global app registry so this
        # manager method can be used in migrations. This is fine because
        # managers are by definition working on the real model.
        GlobalUserModel = apps.get_model(self.model._meta.app_label, self.model._meta.object_name)
        username = GlobalUserModel.normalize_username(username)
        user = self.model(username=username, email=email, **extra_fields)
        user.password = make_password(password)
        user.save(using=self._db)
        return user

点进make_password的源码我们开始分析

def make_password(password, salt=None, hasher='default'):
    if password is None:
        return UNUSABLE_PASSWORD_PREFIX + get_random_string(UNUSABLE_PASSWORD_SUFFIX_LENGTH)
    '根据我们的尝试,没传密码的时候就是触发了第一行的if判断,通过查看几个变量和方法的内容,可以简单判断出他就是产生了一串随机字符串'
    if not isinstance(password, (bytes, str)):
        raise TypeError(
            'Password must be a string or bytes, got %s.'
            % type(password).__qualname__
        )
        '根据这里的if判断,我们可以简单看出他是用来判断密码的数据类型的,如果不是字符串或是二进制,就报错'
        
    hasher = get_hasher(hasher)
    salt = salt or hasher.salt()
    return hasher.encode(password, salt)

	'然后这里的get_hasher我看源码也只是一知半解,具体的作用差不多就是对我们传入的密码进行加密,通过注释得知这里的参数就是用来判断是否指定了加密用的某个东西,没有就用默认的,因此hasher就是返回了一个加密用的东西,然后salt我不太明白,但是现在已经不影响我理解整体意思了,最后的return出去的内容就是对密码进行加密并且转换成二进制的内容,'
    
    def get_hasher(algorithm='default'):
    """
    Return an instance of a loaded password hasher.

    If algorithm is 'default', return the default hasher. Lazily import hashers
    specified in the project's settings file if needed.
    """
    if hasattr(algorithm, 'algorithm'):
        return algorithm

    elif algorithm == 'default':
        return get_hashers()[0]

    else:
        hashers = get_hashers_by_algorithm()
        try:
            return hashers[algorithm]
        except KeyError:
            raise ValueError("Unknown password hashing algorithm '%s'. "
                             "Did you specify it in the PASSWORD_HASHERS "
                             "setting?" % algorithm)
            
            
            '通过会议encode的知识,我们可以得知这个salt其实是用于指定字符编码类型的,那我们可以产生一个模糊认知,我们只用了salt这个东西,就完成了密码的加密,并指定了转换成二进制时的编码类型'

讲解完了密码的加密,自然得有密码的校验,这里老师也做了提示,check_password方法就是用于校验密码的(回头有空再写,感觉博客来不及了)。

3、用户表密码忘了怎么办

方法一:新增一个用户,创建完成后把这个用户的密码复制到数据库中忘记密码的账号的密码的位置(建议用完改回去,不然知道密码的人用不了了解释起来也麻烦)

方式二:直接去掉认证,让这个用户直接可以登陆(更不推荐了)

6.1 签发(登录)

当我们在使用的时候只需要在路由层中导入obtain_jwt_token这个类,而他就是对应的视图类,因此他内部也是有对应的处理post请求的post方法的,因此我们需要研究他内部的post方法

from rest_framework_jwt.views import obtain_jwt_token

在查找的时候我们发现他并不是直接跳转到对应的方法上去了,他只是一个变量名

obtain_jwt_token = ObtainJSONWebToken.as_view()
refresh_jwt_token = RefreshJSONWebToken.as_view()
verify_jwt_token = VerifyJSONWebToken.as_view()

这里不用管他执行了as_view方法(虽然我们也明白作用),点击前面的这个关键名称的源码,我们可以发现他内部是空的,但是他继承了一个父类

class ObtainJSONWebToken(JSONWebTokenAPIView):
    """
    API View that receives a POST with a user's username and password.

    Returns a JSON Web Token that can be used for authenticated requests.
    """
    serializer_class = JSONWebTokenSerializer

这个父类中就有定义一个post方法

class JSONWebTokenAPIView(APIView):
    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)
            '这里的jwt_response_payload_handler方法我们之前看过,就是用来定制返回的信息的格式的'
            response = Response(response_data)
            '这里我们也可以看出来他就是产生response对象'
            
            '然后这里的if,目前用不到,他通过判断jwt的配置文件中的JWT_AUTH_COOKIE是否存在,来判断项目是否是前后端结合的,前后端结合的时候就会用上这里的代码'
            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,因此接下去我们要去序列化类中研究。

ps:之前我们校验的方式是直接在视图函数内的方法(比如post等方法)中校验,现在就相当于是中间多了一个步骤,拉到序列化类中去校验了,别的其实并没什么打的区别。(将来也会经常出现这样的情况)

在上面我们看到序列化类定义在执行as_view方法的那个类中

class ObtainJSONWebToken(JSONWebTokenAPIView):
    """
    API View that receives a POST with a user's username and password.

    Returns a JSON Web Token that can be used for authenticated requests.
    """
    serializer_class = JSONWebTokenSerializer
# 如何得到user,如何签发的token----》在序列化类的全局钩子中得到的user和签发的token
	-JSONWebTokenSerializer---全局钩子---validate方法
    	#这里的attrs就是前端传入,校验过后的数据---》{"username":"lqz","password":"lqz1e2345"}
        def validate(self, attrs):
        credentials = {
            # self.username_field: attrs.get(self.username_field),
            '上面的代码只是多做了一个动态匹配,我们写成下面的形式也是一样的,当我们改了User表之后他会自动匹配'
            '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)
'后面的else部分就是进行报错'


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


## 了解:
	'上面有一段代码是对用户进行校验,检测是否为活跃用户,他用的方法变成了一格下划线,是因为进行了重命名,这个方法叫做翻译函数,当然前提是对drf这个app进行注册'
	# 翻译函数,只要做了国际化,放的英文,会翻译成该国语言(配置文件配置的)
	from django.utils.translation import ugettext as _
	msg = _('Unable to log in with provided credentials.')


image

6.2 认证(认证类)

之前学习怎么使用的时候我们是直接导入了jwt的认证类,因此我们应该去研究他

# JSONWebTokenAuthentication---->父类BaseJSONWebTokenAuthentication----》authenticate方法
from rest_framework_jwt.authentication import JSONWebTokenAuthentication

但是进入他的源码我们没有找到authenticate方法,这时候我们就要去看他的父类

class JSONWebTokenAuthentication(BaseJSONWebTokenAuthentication):
    www_authenticate_realm = 'api'

    def get_jwt_value(self, request):
        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

        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]

    def authenticate_header(self, request):
        return '{0} realm="{1}"'.format(api_settings.JWT_AUTH_HEADER_PREFIX, self.www_authenticate_realm)

接着我们就在他的父类BaseJSONWebTokenAuthentication中找到了authenticate方法

class BaseJSONWebTokenAuthentication(BaseAuthentication):

    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(这也是之前我们为什么要返回用户和token)
        return (user, jwt_value)
    
    
    
# 在上面的方法中我们通过jwt_value = self.get_jwt_value(request)得到了token,但是我们想到我们在传token的时候,前面是jwt加一个空格才拼接的token,因此我们需要研究这个方法

'不过查找的时候并不在当前类中,我们需要从外面往里面找(可以导入from rest_framework_jwt.authentication import JSONWebTokenAuthentication然后再点进源码查找),因为虽然是认证类在运行,但是我们是通过视图类产生对象,由这个对象来执行方法的'
    def get_jwt_value(self, request):
        # 拿到了前端请求头中传入的 jwt dasdfasdfasdfa
        # auth=[jwt,asdfasdfasdf]这里就相当于对接收的字符串根据空格进行了切割
        auth = get_authorization_header(request).split()
        # 'jwt',这里就是把开头的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:
            '这里是对比字符开头的字母是不是jwt。是跟配置文件中的信息进行对比的,之前也讲过'
            return None
        
        
		'这里下面两个if判断就是对我们传入的字符串格式进行校验,只有存在一个空格的时候才是符合条件的'
        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,不会校验,一定配合权限类使用

七、自定义用户表实现jwt的签发和认证

7.1 签发

首先我们需要创建一个自定义的用户表,因为只是用于测试的,所以我们不用写的很复杂,简单写一点就可以进行数据库迁移

models.py

from django.db import models

# Create your models here.
class UserInfo(models.Model):
    username = models.CharField(max_length=32)
    password = models.CharField(max_length=32)

接着我们回忆一下jwt的签发和认证过程,他是在对应的序列化类中用全局钩子获取user和token的,并对他们进行校验的,因此我们如果在视图类中写类似post等方法的时候可以考虑不学他分开写,我们把校验的过程也写进来。

urls.py

from django.contrib import admin
from django.urls import path, include
from rest_framework_jwt.views import obtain_jwt_token
from rest_framework.routers import SimpleRouter
from app01 import views


router = SimpleRouter()
router.register('user', views.UserView, 'user')

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/v1/', include(router.urls)),
    path('login/', obtain_jwt_token)
]

views.py

from django.shortcuts import render

from rest_framework.decorators import action
from rest_framework.viewsets import ViewSet
from rest_framework.response import Response

from .models import UserInfo

from rest_framework_jwt.settings import api_settings
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
'这里的两个配置是因为序列化类中校验的时候用到了,所以我们也要拿来用'
# from rest_framework_jwt.utils import jwt_payload_handler
'这是JWT_PAYLOAD_HANDLER指向的方法,我们可以这样调用,然后点进去看他的源码,通过他的源码我们可以发现他就是用来定义荷载的格式的'
'而JWT_ENCODE_HANDLER,和接下去会用到的JWT_DECODE_HANDLER也可以用这样的方式查看,然后再去看他们的源码'

class UserView(ViewSet):
    @action(methods=['POST'], detail=False)
    def login(self, request, *args, **kwargs):
        username = request.data.get('username')
        password = request.data.get('password')
        user = UserInfo.objects.filter(username=username, password=password).first()
        if user:
            # 登录成功,签发token
            # 通过user得到payload
            payload = jwt_payload_handler(user)
            # 通过payload得到token
            token = jwt_encode_handler(payload)
            return Response({'code': 1000, 'msg': '登录成功', 'token': token})
        else:
            return Response({'code': 1001, 'msg': '用户名或密码错误'})

这里的密码返回格式也有很多的玄机,比如如果我们把返回信息分开,变得更具体,这样别人就可以通过不断尝试来判断是否有这个用户,类似的东西也有很多

测试结果:

image

7.2 认证

在上面分析的过程中我们发现JSONWebTokenAuthentication这个认证类的父类没什么内容,但是他的父类继承了BaseAuthentication,这个类算是基类。

而现在我们需要自定义认证类,因此我们需要重新写一个认证类。

authentication.py(认证类)

from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
import jwt
from .models import UserInfo
from rest_framework_jwt.settings import api_settings

jwt_decode_handler = api_settings.JWT_DECODE_HANDLER


class JsonWebTokenAuthentication(BaseAuthentication):
    '这里需要重写authenticate方法,因为最后是authenticate方法返回的用户对象和token,内部代码也是模仿源码中的authenticate方法编写'
    def authenticate(self, request):
        # 取出token----》在请求头中,就叫token
        token = request.META.get('HTTP_TOKEN')
        '源码中有一段if代码用于判断是否传入了token,如果没传就不校验了,这个没什么意思,我们就不写了'
        if token:
            try:
                '这里的代码基本上照搬源码中的代码'
                '这里就是通过token获得荷载'
                payload = jwt_decode_handler(token)
                # 得到当前登录用户----》
                user = UserInfo.objects.get(pk=payload.get('user_id'))
                '这里查找的时候,我们需要自己去token的荷载中确认一下字段名称,因此不能直接写pk来查找'
                # 只要访问一次需要登录的接口,就会去UserInfo表中查一次用户---》可以优化
                '我们用下面的代码来替换这里的代码,就可以暂时不去数据库中查询,等到用到的时候再去查询的效果'
                # user=UserInfo(id=payload.get('user_id'),username=payload.get('username'))
                # user={'id':payload.get('user_id')}
                '同时这里我们把所有的代码都放到异常捕获中,来避免报错,主要是避免为登陆导致的报错'

                return user, token
            except jwt.ExpiredSignature:
                raise AuthenticationFailed('token过期')
            except jwt.DecodeError:
                raise AuthenticationFailed('token认证失败')

            except jwt.InvalidTokenError:
                raise AuthenticationFailed('token无效')
            except Exception as e:
                raise AuthenticationFailed('未知异常')

        raise AuthenticationFailed('token没有传,认证失败')

当我们重写了这个认证类之后,我们就不需要使用权限类来校验是否登陆了,这时候我们再写一个视图类来使用这个认证类顺带检测一下

views.py中需要添加一个视图类

from .authentication import JsonWebTokenAuthentication
class TestView(ViewSet):
    authentication_classes = [JsonWebTokenAuthentication]

    @action(methods=['GET'], detail=False)
    def test1(self, request):
        return Response('ok')

urls.py

添加一条路由配置

router.register('test', views.TestView, 'test')

ps:

如果我们写代码的时间太长,而且配置中的过期时间依然是默认的五分钟,token就会过期,这里我们可以再次登陆获取token,或是修改配置中的token过期时间

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

测试结果(需要先用原来的视图类登陆,然后用token来测试,这时候因为我们自定的时候去掉了开头的jwt加空格,所以直接传token就好了):

image


posted @ 2023-05-11 17:04  致丶幻  阅读(36)  评论(0编辑  收藏  举报