1. 程式人生 > 實用技巧 >drf-JWT認證

drf-JWT認證

一 JWT認證

在使用者註冊或登入後,我們想記錄使用者的登入狀態,或者為使用者建立身份認證的憑證。我們不再使用Session認證機制,而使用Json Web Token(本質就是token)認證機制。

Json web token (JWT), 是為了在網路應用環境間傳遞宣告而執行的一種基於JSON的開放標準((RFC 7519).該token被設計為緊湊且安全的,特別適用於分散式站點的單點登入(SSO)場景。JWT的宣告一般被用來在身份提供者和服務提供者間傳遞被認證的使用者身份資訊,以便於從資源伺服器獲取資源,也可以增加一些額外的其它業務邏輯所必須的宣告資訊,該token也可直接被用於認證,也可被加密。

1.1 構成和工作原理

JWT的構成

JWT就是一段字串,由三段資訊構成的,將這三段資訊文字用.連結一起就構成了Jwt字串。就像這樣:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

第一部分我們稱它為頭部(header),第二部分我們稱其為載荷(payload, 類似於飛機上承載的物品),第三部分是簽證(signature).

1.1.1 header

jwt的頭部承載兩部分資訊:

  • 宣告型別,這裡是jwt
  • 宣告加密的演算法 通常直接使用 HMAC SHA256

完整的頭部就像下面這樣的JSON:

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

然後將頭部進行base64加密(該加密是可以對稱解密的),構成了第一部分.

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

1.1.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

1.1.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/

1.2 本質原理

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就能訪問登入的使用者

注:登入介面需要做 認證 + 許可權 兩個區域性禁用
"""

1.2.1 補充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安裝和簡單使用

2.1 官網
http://getblimp.github.io/django-rest-framework-jwt/
2.2 安裝
pip install djangorestframework-jwt
2.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空格)

多方式登入簽發token

# 序列化器類中實現
from rest_framework import serializers
from api import models
import re
from rest_framework_jwt.utils import jwt_payload_handler, jwt_encode_handler
from rest_framework.exceptions import ValidationError


class LoginModelSerializer(serializers.ModelSerializer):
    username = serializers.CharField()
    class Meta:
        model = models.User
        fields = ['username', 'password']

    def validate(self, attrs):
        # 在這寫邏輯
        print(self.context)
        username = attrs.get('username')
        password = attrs.get('password')
        # 通過判斷,username資料不同,查詢欄位不一樣
        # 正則匹配,如果是手機號
        if re.match('^1[3-9][0-9]{9}$', username):
            user = models.User.objects.filter(mobile=username).first()
        elif re.match('^.+@.+$', username):
            user = models.User.objects.filter(email=username).first()
        else:
            user = models.User.objects.filter(username=username).first()
        if user:  # 存在使用者
            # 校驗密碼,因為是密文,要用check_password
            if user.check_password(password):
                # 簽發token
                payload = jwt_payload_handler(user)  # 把user傳入,得到payload
                token = jwt_encode_handler(payload)  # 把payload傳入,得到token
                self.context['token'] = token
                self.context['username'] = user.username
                return attrs
            else:
                raise ValidationError('密碼錯誤')
        else:
            raise ValidationError('使用者不存在')
'''
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER

payload = jwt_payload_handler(user) # 把user傳入,得到payload
token = jwt_encode_handler(payload) # 把payload傳入,得到token

'''
# 邏輯在檢視類中寫
import re
from api import models
from rest_framework_jwt.utils import jwt_payload_handler, jwt_encode_handler


class Login3View(ViewSet):
    def login3(self, request, *args, **kwargs):
        username = request.data.get('username')
        password = request.data.get('password')
        # 通過判斷,username資料不同,查詢欄位不一樣
        # 正則匹配,如果是手機號
        if re.match('^1[3-9][0-9]{9}$', username):
            user = models.User.objects.filter(mobile=username).first()
        elif re.match('^.+@.+$', username):
            user = models.User.objects.filter(email=username).first()
        else:
            user = models.User.objects.filter(username=username).first()
        if user:  # 存在使用者
            # 校驗密碼,因為是密文,要用check_password
            if user.check_password(password):
                # 簽發token
                payload = jwt_payload_handler(user)  # 把user傳入,得到payload
                token = jwt_encode_handler(payload)  # 把payload傳入,得到token
                return Response()

自定義基於jwt的許可權類

繼承BaseAuthentication

# 自定義基於jwt的許可權類
from rest_framework_jwt.authentication import BaseAuthentication, BaseJSONWebTokenAuthentication, \
    JSONWebTokenAuthentication
from api import models
from rest_framework_jwt.authentication import jwt_decode_handler
from rest_framework_jwt.utils import jwt_decode_handler
import jwt
from rest_framework.exceptions import AuthenticationFailed
class MyJwtAuthentication(BaseAuthentication):

    def authenticate(self, request):
        jwt_value = request.META.get('HTTP_AUTHORIZATION')
        if jwt_value:
            try:
                # jwt提供了通過三段token,取出payload的方法,並且有校驗功能
                payload = jwt_decode_handler(jwt_value)
            except jwt.ExpiredSignature:
                raise AuthenticationFailed('簽名過期')
            except jwt.InvalidTokenError:
                raise AuthenticationFailed('使用者非法')
            except Exception as e:
                raise AuthenticationFailed(str(e))
            # 因為payload就是使用者資訊的字典
            print(payload)
            # return payload,jwt_value
            # 需要得到user物件,
            # 第一種,去資料庫查
            # user = models.User.objects.get(pk=payload.get('user_id'))
            # return user,jwt_value
            # 第二種不查庫
            user = models.User(pk=payload.get('user_id'),username=payload.get('username'))
            return user, jwt_value
        else:
            raise AuthenticationFailed('您沒有攜帶認證資訊')
#全域性使用
# setting.py
REST_FRAMEWORK = {
    # 認證模組
    'DEFAULT_AUTHENTICATION_CLASSES': (
        # 寫自定義認證的路徑
        'users.app_auth.MyJwtAuthentication',
    ),
}
#區域性禁用或使用
# 區域性禁用
authentication_classes = []
# 區域性啟用
authentication_classes = [MyJwtAuthentication]

繼承BaseJSONWebTokenAuthentication

class MyJwtAuthentication(BaseJSONWebTokenAuthentication):

    def authenticate(self, request):
        jwt_value = request.META.get('HTTP_AUTHORIZATION')
        if jwt_value:
            try:
                # jwt提供了通過三段token,取出payload的方法,並且有校驗功能
                payload = jwt_decode_handler(jwt_value)
            except jwt.ExpiredSignature:
                raise AuthenticationFailed('簽名過期')
            except jwt.InvalidTokenError:
                raise AuthenticationFailed('使用者非法')
            except Exception as e:
                raise AuthenticationFailed(str(e))
            user = self.authenticate_credentials(payload)
            return user, jwt_value
        else:
            raise AuthenticationFailed('您沒有攜帶認證資訊')
#全域性使用
# setting.py
REST_FRAMEWORK = {
    # 認證模組
    'DEFAULT_AUTHENTICATION_CLASSES': (
        # 寫自定義認證的路徑
        'users.app_auth.MyJwtAuthentication',
    ),
}
#區域性禁用或使用
# 區域性禁用
authentication_classes = []
# 區域性啟用
authentication_classes = [MyJwtAuthentication]

自定義jwt返回格式

def my_jwt_response_payload_handler(token, user=None, request=None):
    return {
        'token': token,
        'msg': '登入成功',
        'status': 100,
        'username': user.username
    }
settings.py配置:
JWT_AUTH={
     # 自定義認證結果:如果不自定義,返回的格式是固定的,只有token欄位
    'JWT_RESPONSE_PAYLOAD_HANDLER':'app02.utils.my_jwt_response_payload_handler',
}

jwt的配置引數

# jwt的配置
import datetime
JWT_AUTH={
    # 自定義認證結果:如果不自定義,返回的格式是固定的,只有token欄位
    'JWT_RESPONSE_PAYLOAD_HANDLER':'app02.utils.my_jwt_response_payload_handler',
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7), # 過期時間,手動配置
}