Django(65)jwt認證原理


前言

帶着問題學習是最有目的性的,我們先提出以下幾個問題,看看通過這篇博客的講解,能解決問題嗎?

  1. 什么是JWT?
  2. 為什么要用JWT?它有什么優勢?
  3. JWT的認證流程是怎樣的?
  4. JWT的工作原理?

我們帶着4個問題進入學習
 

1.什么是JWT?

JWT全稱Json Web Token,JWT 是一種開發的行業標准 RFC 7519 ,用於安全的表示雙方之間的聲明。目前,JWT廣泛應用在系統的用戶認證方面,特別是現在前后端分離項目。
 

2.為什么要使用JWT?它有什么優勢?

用戶登錄認證方式分為傳統的token登錄方式和JWT 方式,傳統的方式又分為session登錄和緩存登錄
 

2.1 session登錄

"""
接收到登錄請求, 1.得到用戶 2.產生token 3.記錄到session表 4.返回token

接收需要認證信息的請求, 1.拿到token 2.數據庫校驗 3.確定登錄用戶 4.返回認證后信息           與數據庫session表交互
"""

 

2.2 緩存登錄

"""
接收到登錄請求, 1.得到用戶 2.產生token 3.記錄到緩存 4.返回token

接收需要認證信息的請求, 1.拿到token 2.緩存校驗 3.確定登錄用戶 4.返回認證后信息            用戶登錄信息緩存存儲
"""

 

2.3 JWT方式

"""
接收到登錄請求, 1.得到用戶 2.根據用戶產生有用戶信息的token 3.返回token

接收需要認證信息的請求, 1.拿到token 2.檢驗token是否合法,校驗出用戶 3.返回認證后信息
"""

 

2.4 JWT優點

  1. 服務器不需要存儲tokentoken交給每一個客戶端自己存儲,服務器壓力小
  2. 服務器存儲的是 簽發和校驗token兩段算法,簽發認證的效率高
  3. 算法完成各集群服務器同步成本低,路由項目完成集群部署(適應高並發)
     

2.5 JWT特點

  1. token一定在服務器產生,且在服務器校驗
  2. token一定參與網絡傳輸
  3. token攜帶的信息存在能被反解不能被反解的多部分組成
     

3.JWT組成以及加密原理

JWT是由頭部header、載荷payload、簽名signature,三段式組成,用.進行拼接,例如官網的這段字符串

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

每一部分都是一個json字典加密形參的字符串,頭部和載荷采用的是base64url加密(前台后台都可以解密),簽名采用hash256不可逆加密
注意:base64url加密是先做base64加密,然后再將字符串中的 - 替代 + _替代 /
 

3.1 Header

頭部包含了兩部分,token 類型和采用的加密算法

{
  "alg": "HS256",
  "typ": "JWT"
}
  • typ: (Type)類型,指明類型是JWT
  • alg: (Algorithm)算法,必須是JWS支持的算法,主要是HS256RS256

它會使用 base64url編碼組成 JWT 結構的第一部分
 

3.2 Payload

這部分就是我們存放信息的地方了,你可以把用戶ID等信息放在這里,JWT規范里面對這部分有進行了比較詳細的介紹,JWT 規定了7個官方字段,供選用

  • iss (issuer):簽發人
  • exp (expiration time):過期時間,時間戳
  • sub (subject):主題
  • aud (audience):受眾
  • nbf (Not Before):生效時間,時間戳
  • iat (Issued At):簽發時間,時間戳
  • jti (JWT ID):編號

常用的有issiatexpaudsub

{
  "sub": "1234567890",
  "name": "John Doe",
  "id": 1,
  "iat": 1516239022
}

同樣的,它會使用base64url編碼組成 JWT 結構的第二部分
 

3.3 Signature

前面兩部分都是使用base64url進行編碼的,前端可以解開知道里面的信息。Signature需要使用編碼后的 headerpayload 以及我們提供的一個密鑰,這個密鑰只有服務器才知道,不能泄露給用戶,然后使用 header 中指定的簽名算法(HS256)進行簽名。

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

算出簽名以后,把 HeaderPayloadSignature 三個部分拼成一個字符串,每個部分之間用"點"(.)分隔,就可以返回給用戶。

簽名的目的
最后一步簽名的過程,實際上是對頭部以及負載內容進行簽名,防止內容被篡改。如果有人對頭部以及負載的內容解碼之后進行修改,再進行編碼,最后加上之前的簽名組合形成新的JWT的話,那么服務器端會判斷出新的頭部和負載形成的簽名和JWT附帶上的簽名是不一樣的。如果要對新的頭部和負載進行簽名,在不知道服務器加密時用的密鑰的話,得出來的簽名也是不一樣的。
 

4.解密原理

1.對token進行切割
2.對第二段進行base64url解密,並獲取payload信息,檢測exp是否過期
3.將第1,2部分密文拼接起來,再次執行HS256加密
將加密后的密文 = base64解密(第三段字符串)
如果相等則通過,不相等則失敗
 

5.JWT的使用方式

  客戶端收到服務器返回的 JWT,可以儲存在 Cookie 里面,也可以儲存在 localStorage
  此后,客戶端每次與服務器通信,都要帶上這個 JWT。你可以把它放在Cookie里面自動發送,但是這樣不能跨域,所以更好的做法是放在HTTP請求的頭信息Authorization字段里面。

  1. 首先,前端通過Web表單將自己的用戶名和密碼發送到后端的接口。這一過程一般是一個HTTP POST請求。建議的方式是通過SSL加密的傳輸(https協議),從而避免敏感信息被嗅探。

  2. 后端核對用戶名和密碼成功后,將用戶的id等其他信息作為JWT Payload(負載),將其與頭部分別進行Base64編碼拼接后簽名,形成一個JWT。形成的JWT就是一個形同aaa.bbb.ccc的字符串。

  3. 后端將JWT字符串作為登錄成功的返回結果返回給前端。前端可以將返回的結果保存在localStoragesessionStorage上,退出登錄時前端刪除保存的JWT即可。

  4. 前端在每次請求時將JWT放入HTTP Header中的Authorization位。(解決XSSXSRF問題)

  5. 后端檢查是否存在,如存在驗證JWT的有效性。例如,檢查簽名是否正確;檢查Token是否過期;檢查Token的接收方是否是自己(可選)。
     

6.JWT代碼演示

首先我們需要安裝JWT

pip3 install PyJWT==1.7.1

然后創建一個新的文件jwt_auth,名字隨便取,寫一個簽發token的方法和校驗token的方法

import datetime
import jwt

salt = "iv%x6xo7l7_u9bf_u!9#g#m*)*=ej@bek5)(@u3kh*72+unjv="


def create_token():
    """
    自定義token
    """
    # 過期時間
    expire_time = datetime.datetime.utcnow() + datetime.timedelta(days=7)
    # 構造headers
    headers = {
        'typ': 'jwt',
        'alg': 'HS256'
    }
    # 構造payload
    payload = {
        "userId": 1,
        "exp": expire_time
    }
    result = jwt.encode(payload=payload, key=salt, algorithm="HS256", headers=headers).decode("utf-8")
    return result


def parse_payload(token):
    """
    對token進行校驗並獲取payload
    """
    try:
        verified_payload = jwt.decode(token, key=salt)
        return verified_payload
    except jwt.ExpiredSignatureError:
        print('token已失效')
    except jwt.DecodeError:
        print('token認證失敗')
    except jwt.InvalidTokenError:
        print('非法的token')


if __name__ == '__main__':
    token = create_token()
    print(token)
    print(parse_payload(token))

  我們上面寫了一個創建token的方法和校驗token的方法,然后我們執行這個腳本,結果如下

token:eyJ0eXAiOiJqd3QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEsImV4cCI6MTYyNDYwOTAzNX0.VyjHR6xn94nImEsaIqVE_g84WY_88XuzVHhbqEk-XbM
校驗結果:{'userId': 1, 'exp': 1624609035}

  可以看到,我們可以正常簽發和校驗token了,實際開發過程中,我們會把salt換成settings.py文件下的SECRET_KEY,然后把useId不要寫死換成user.pk即可
 

7.djangorestframework-jwt

  以上我們都是使用的PyJWT,而DRF有個第三方庫djangorestframework-jwt,幫我們更加方便的使用JWT,它是基於PyJWT==1.7.1進行再次封裝的。最新的官網(http://jpadilla.github.io/django-rest-framework-jwt/)
 

7.1安裝命令

pip3 install djangorestframework-jwt

 

8.實戰案例

我們做一個用戶登錄的需求,用戶登錄可以使用以下3種方式

  • 賬號密碼登錄
  • 手機號密碼登錄
  • 郵箱密碼登錄

且需要自己自定義JWT認證,認證的格式為header請求頭中的AUTHORIZATION字段的值為jwt token的形式,然后后端取出token,通過算法檢查出token是否合法
 

8.1前置准備工作

創建項目jwt_demo,然后創建個app名字為api,接着配置好數據庫,然后在models.py文件中創建MyUser模型

from django.db import models
from django.contrib.auth.models import AbstractUser
class MyUser(AbstractUser):
    phone = models.CharField(verbose_name='手機號碼', max_length=11, null=True, unique=True)

這樣User表中就有了phone字段,並且在settings.py文件中設置默認User模型AUTH_USER_MODEL = "api.MyUser"
接着在api中創建serializers.py文件,編寫如下序列化

from django.contrib.auth import get_user_model
from rest_framework import serializers
from rest_framework_jwt.settings import api_settings


User = get_user_model()  # 獲取用戶模型


class LoginSerializer(serializers.ModelSerializer):
    # 設置自定義的反序列化字段usr,pwd
    usr = serializers.CharField(write_only=True)
    pwd = serializers.CharField(write_only=True)

    class Meta:
        model = User
        fields = ['username', 'email', 'phone', 'usr', 'pwd']
        extra_kwargs = {
            "username": {
                "read_only": True
            },
            "email": {
                "read_only": True
            },
            "phone": {
                "read_only": True
            }
        }

  我們在序列化的時候,讓前台傳的字段不再是User表中的username這些,而是自定義的usrpwdusr字段的值可以是用戶名或郵箱或手機號,這樣一來就實現了3種登錄方式
 
編寫完序列化類,我們來完成視圖的工作

import re
from django.contrib.auth import get_user_model
from django.contrib.auth.hashers import check_password
from rest_framework.views import APIView
from rest_framework_jwt.settings import api_settings
from api.utils.response import APIResponse


User = get_user_model()


class LoginView(APIView):
    """
    登陸視圖,用戶名與密碼匹配返回token
    """
    authentication_classes = []
    permission_classes = []

    def post(self, request, *args, **kwargs):
        try:
            # 獲取前台穿的usr和pwd字段
            usr = request.data.get("usr")
            pwd = request.data.get("pwd")
        except KeyError:
            return APIResponse(data_status=10002, data_msg="請求數據非法")
        if re.match(r"1[35678]\d{9}", usr):
            # 正則匹配手機號
            user = User.objects.filter(phone=usr).first()
        elif re.match(r'^[0-9a-zA-Z_]{0,19}@[0-9a-zA-Z]{1,13}\.[com,cn,net]{1,3}$', usr):
            # 正則匹配郵箱
            user = User.objects.filter(email=usr).first()
        else:
            # 用戶名
            user = User.objects.filter(username=usr).first()
        if not user:
            return APIResponse(data_status=10002, data_msg="該用戶未注冊")
        if user.is_active == 0:
            return APIResponse(data_status=10002, data_msg="用戶被禁用")
        if not check_password(pwd, user.password):
            return APIResponse(data_status=10002, data_msg="用戶名或密碼錯誤")
        
        # 調用第三方的JWT_PAYLOAD_HANDLER和JWT_ENCODE_HANDLER,這里也可以自定義該方法
        jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
        jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
        
        # 通過user解析出payload
        payload = jwt_payload_handler(user)
        # 通過payload生成token
        token = jwt_encode_handler(payload)
        return APIResponse(token=token, results={"user": user.username})


class TestView(APIView):
    def get(self, request, *args, **kwargs):
        return APIResponse()

視圖中我們創建了2個類視圖,第一個是登錄視圖,用來登錄后返回token,第二個類視圖是為了測試登錄成功后,以后訪問視圖都需要在請求頭中攜帶token,否則權限驗證失敗。
最后我們配置路由即可

urlpatterns = [
    path('login/', views.LoginView.as_view()),
    path('test/', views.TestView.as_view())
]

 

8.2自定義權限驗證

我們創建一個auth.py文件,編寫自定義權限

import jwt
from django.contrib.auth import get_user_model
from rest_framework.authentication import get_authorization_header
from rest_framework_jwt.authentication import jwt_decode_handler, BaseJSONWebTokenAuthentication, \
    jwt_get_username_from_payload
from rest_framework import exceptions


User = get_user_model()  # 獲取用戶模型


class JWTAuthentication(BaseJSONWebTokenAuthentication):
    keyword = "JWT"

    def authenticate(self, request):
        # 獲取請求頭字符串,分割成列表
        auth = get_authorization_header(request).split()
        if not auth:
            msg = "未獲取到Authorization請求頭"
            raise exceptions.AuthenticationFailed(msg)
        if auth[0].lower() != self.keyword.lower().encode():
            msg = "Authorization請求頭中認證方式錯誤"
            raise exceptions.AuthenticationFailed(msg)
        if len(auth) == 1:
            msg = "非法Authorization請求頭"
            raise exceptions.AuthenticationFailed(msg)
        elif len(auth) > 2:
            raise exceptions.AuthenticationFailed({"message": "無效的授權頭。憑據字符串''不應包含空格"})
        try:
            jwt_token = auth[1]
            payload = jwt_decode_handler(jwt_token)
        except jwt.ExpiredSignature:
            msg = 'token已失效'
            raise exceptions.AuthenticationFailed(msg)
        except jwt.DecodeError:
            msg = '簽名解析失敗'
            raise exceptions.AuthenticationFailed(msg)
        except jwt.InvalidTokenError:
            raise exceptions.AuthenticationFailed()

        user = self.authenticate_credentials(payload)

        return user, jwt_token

    def authenticate_credentials(self, payload):
        """
        Returns an active user that matches the payload's user id and email.
        """
        User = get_user_model()
        username = jwt_get_username_from_payload(payload)

        if not username:
            msg = _('Invalid payload.')
            raise exceptions.AuthenticationFailed(msg)

        try:
            user = User.objects.get_by_natural_key(username)
        except User.DoesNotExist:
            msg = '用戶不存在'
            raise exceptions.AuthenticationFailed(msg)

        if not user.is_active:
            msg = '用戶已禁用'
            raise exceptions.AuthenticationFailed(msg)

        return user

最后我們在settings.py文件中配置下即可

REST_FRAMEWORK = {
    # 自定義的認證類
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'api.auth.JWTAuthentication',
    ),
    # 使用drf的權限驗證
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
    ],
}

JWT_AUTH = {
    # token的過期時間設置,默認是5分鍾過期
    'JWT_EXPIRATION_DELTA': datetime.timedelta(seconds=300),
}

最后我們通過python manage createsuperuser,創建超級用戶,usernameadmin,'password'為admin123phone13345678901email100100100@qq.com
 

9.測試自定義的token權限

我們使用apifox工具進行接口測試,首先使用post請求訪問http://127.0.0.1:8000/api/login/

9.1手機號登錄


 

9.2郵箱登錄


 

9.3賬號密碼登錄


 

9.4攜帶token登錄

登錄成功后,我們拿着token去訪問視圖,我們在header中添加AUTHORIZATION字段

我們發現是可以登錄成功的,最后如果你想驗證過期時間,你可以把token中的第二段字符串,使用base64解密,就能看到時間戳


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM