DRF之JWT認證
【一】JWT
- WT(JSON Web Token)是一種開放標準(RFC 7519),用於在網路上傳輸宣告的一種緊湊且自包含的方式。JWT 可以使用 HMAC 演算法或是使用 RSA 或 ECDSA 等公鑰/私鑰對進行簽名。通常,它用於在身份提供者和服務之間傳遞被認證的使用者身份資訊,以便於在使用者和服務之間安全地傳遞宣告。
- JWT 主要由三部分組成,用
.
分隔開:
- Header(頭部):包含了型別(typ)和所使用的演算法(alg)等資訊,通常是
{"alg": "HS256", "typ": "JWT"}
。
- Payload(負載):包含了要傳遞的宣告資訊,如使用者的身份、角色等,以及其他自定義的資料,例如
{"sub": "user123", "role": "admin"}
。
- Signature(簽名):將編碼後的 Header 和 Payload 以及一個秘密(對稱加密)或公鑰/私鑰(非對稱加密)一起進行簽名,以保證資料的完整性和真實性。
- JWT 的優勢在於它的緊湊性和自包含性,使得它適合在不同的環境中進行傳遞,如在 HTTP 請求頭、URL 引數或 POST 引數中。JWT 的缺點是一旦簽發後,其內容無法更改,也無法撤銷,除非設定了較短的過期時間並在服務端進行驗證。
- 在 Web 開發中,JWT 被廣泛用於實現使用者認證和授權機制,常見的應用場景包括單點登入(SSO)、API 認證等。
【二】SimpleJwt
【0】SimpleJwt原始碼
- SimpleJwt其實與drf類似,也是一個app,需要在django專案檔案中註冊
【0.1】登入
# 登入的入口
from rest_framework_simplejwt.views import token_obtain_pair
【0.2】認證
# 認證類的入口
from rest_framework_simplejwt.authentication import JWTAuthentication
【1】SimpleJwt配置檔案
# JWT配置
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5), # Access Token的有效期
'REFRESH_TOKEN_LIFETIME': timedelta(days=7), # Refresh Token的有效期
# 對於大部分情況,設定以上兩項就可以了,以下為預設配置專案,可根據需要進行調整
# 是否自動重新整理Refresh Token
'ROTATE_REFRESH_TOKENS': False,
# 重新整理Refresh Token時是否將舊Token加入黑名單,如果設定為False,則舊的重新整理令牌仍然可以用於獲取新的訪問令牌。需要將'rest_framework_simplejwt.token_blacklist'加入到'INSTALLED_APPS'的配置中
'BLACKLIST_AFTER_ROTATION': False,
'ALGORITHM': 'HS256', # 加密演算法
'SIGNING_KEY': settings.SECRET_KEY, # 簽名密匙,這裡使用Django的SECRET_KEY
# 如為True,則在每次使用訪問令牌進行身份驗證時,更新使用者最後登入時間
"UPDATE_LAST_LOGIN": False,
# 用於驗證JWT簽名的金鑰返回的內容。可以是字串形式的金鑰,也可以是一個字典。
"VERIFYING_KEY": "",
"AUDIENCE": None,# JWT中的"Audience"宣告,用於指定該JWT的預期接收者。
"ISSUER": None, # JWT中的"Issuer"宣告,用於指定該JWT的發行者。
"JSON_ENCODER": None, # 用於序列化JWT負載的JSON編碼器。預設為Django的JSON編碼器。
"JWK_URL": None, # 包含公鑰的URL,用於驗證JWT簽名。
"LEEWAY": 0, # 允許的時鐘偏差量,以秒為單位。用於在驗證JWT的過期時間和生效時間時考慮時鐘偏差。
# 用於指定JWT在HTTP請求頭中使用的身份驗證方案。預設為"Bearer"
"AUTH_HEADER_TYPES": ("Bearer",),
# 包含JWT的HTTP請求頭的名稱。預設為"HTTP_AUTHORIZATION"
"AUTH_HEADER_NAME": "HTTP_AUTHORIZATION",
# 使用者模型中用作使用者ID的欄位。預設為"id"。
"USER_ID_FIELD": "id",
# JWT負載中包含使用者ID的宣告。預設為"user_id"。
"USER_ID_CLAIM": "user_id",
# 用於指定使用者身份驗證規則的函式或方法。預設使用Django的預設身份驗證方法進行身份驗證。
"USER_AUTHENTICATION_RULE": "rest_framework_simplejwt.authentication.default_user_authentication_rule",
# 用於指定可以使用的令牌類。預設為"rest_framework_simplejwt.tokens.AccessToken"。
"AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",),
# JWT負載中包含令牌型別的宣告。預設為"token_type"。
"TOKEN_TYPE_CLAIM": "token_type",
# 用於指定可以使用的使用者模型類。預設為"rest_framework_simplejwt.models.TokenUser"。
"TOKEN_USER_CLASS": "rest_framework_simplejwt.models.TokenUser",
# JWT負載中包含JWT ID的宣告。預設為"jti"。
"JTI_CLAIM": "jti",
# 在使用滑動令牌時,JWT負載中包含重新整理令牌過期時間的宣告。預設為"refresh_exp"。
"SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp",
# 滑動令牌的生命週期。預設為5分鐘。
"SLIDING_TOKEN_LIFETIME": timedelta(minutes=5),
# 滑動令牌可以用於重新整理的時間段。預設為1天。
"SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1),
# 用於生成access和刷refresh的序列化器。
"TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainPairSerializer",
# 用於重新整理訪問令牌的序列化器。預設
"TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSerializer",
# 用於驗證令牌的序列化器。
"TOKEN_VERIFY_SERIALIZER": "rest_framework_simplejwt.serializers.TokenVerifySerializer",
# 用於列出或撤銷已失效JWT的序列化器。
"TOKEN_BLACKLIST_SERIALIZER": "rest_framework_simplejwt.serializers.TokenBlacklistSerializer",
# 用於生成滑動令牌的序列化器。
"SLIDING_TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainSlidingSerializer",
# 用於重新整理滑動令牌的序列化器。
"SLIDING_TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSlidingSerializer",
}
【2】基本使用
【2.1】使用SimpleJwt登入
- urls.py
token_obtain_pair
,路由中配置即可
from django.urls import path, include
# 匯入token_obtain_pair即可
from rest_framework_simplejwt.views import token_obtain_pair
urlpatterns = [
path('login/', token_obtain_pair),
]
【2.2】使用SimpleJwt認證
- 【注】進行認證時,攜帶的請求頭格式由配置檔案中配置
############# views.py ###########
from rest_framework_simplejwt.authentication import JWTAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import GenericViewSet
from .models import Book
from .ser import CommonSerializer
class CommonView(GenericViewSet):
queryset = Book.objects.all()
serializer_class = CommonSerializer
# 將 JWTAuthentication 配置到 authentication_classes 即可
authentication_classes = [JWTAuthentication]
# 【注】使用JWT預設的認證類,需要配合 IsAuthenticated 的許可權類
permission_classes = [IsAuthenticated]
【3】基於SimpleJwt自定製
【3.1】定製登入成功返回格式
- 繼承
TokenObtainPairSerializer
並重寫validate
方法並修改返回值來控制返回格式
【3.1.1】重寫validate
方法
【3.1.1.1】直接使用simple-jwt
的登入介面
############### urls.py #########
from rest_framework_simplejwt.views import token_obtain_pair
urlpatterns = [
path('login/', token_obtain_pair),
]
########### ser.py ##########
class LoginSerializer(TokenObtainPairSerializer):
'''
登入介面呼叫simple-jwt的序列化類,重寫validate定製返回格式
'''
def validate(self, attrs):
data = super().validate(attrs)
response = {
'status': 100,
'msg': '登入成功',
'token': data.get('access')
}
return response
【3.1.1.2】使用context
獲取token
- 自定義登入介面
- 在序列化類中進行校驗
- 由於序列化類繼承
TokenObtainPairSerializer
,可以直接呼叫simple-jwt
的get_token方法
獲得token 物件
- 將token 放在序列化類物件的
context屬性
中
- 在
view
中透過ser.contex
t獲取序列化類中放置的資料
class UserView(GenericViewSet):
serializer_class = LoginSerializer
authentication_classes = []
@action(methods=['POST'], detail=False)
def login(self, request):
ser = self.get_serializer(data=request.data)
if ser.is_valid():
token = ser.context['token']
refresh = ser.context['refresh']
return Response({'code': 100, 'msg': '登入成功', 'token': token, 'refresh': refresh})
else:
return Response({'code': 101, 'msg': ser.errors})
################ ser.py ############
class LoginSerializer(TokenObtainPairSerializer):
username = serializers.CharField()
password = serializers.CharField()
def validate(self, attrs):
username = attrs.get('username')
password = attrs.get('password')
user = User.objects.filter(username=username, password=password).first()
if not user:
raise ValidationError("使用者名稱或密碼錯誤")
token = self.get_token(user)
self.context['refresh'] = str(token)
self.context['token'] = str(token.access_token)
return attrs
【3.1.2】重寫data
方法
############ views.py ##########
class UserView(GenericViewSet):
# 區域性禁用認證和許可權
authentication_classes = []
permission_classes = []
serializer_class = RegisterSerializer
@action(methods=['POST'], detail=False)
def register(self, request):
ser = self.get_serializer(data=request.data)
ser.is_valid(raise_exception=True)
ser.save()
return Response(ser.data)
############### ser.py ###########
class RegisterSerializer(serializers.ModelSerializer):
class Meta:
model = UserInfo
fields ='__all__'
def create(self, validated_data):
user = UserInfo.objects.create_user(**validated_data)
return user
@property
def data(self):
# 重寫data方法定製返回格式
res = super().data
return {'code': 100, 'msg': '註冊成功', 'result': res}
【3.2】定製認證類
- 繼承
JWTAuthentication
並重寫authenticate
方法
- 由於繼承了
JWTAuthentication
,所以可以直接呼叫原來進行驗證的方法
from rest_framework_simplejwt.authentication import JWTAuthentication
from rest_framework.exceptions import AuthenticationFailed
class CommonAuthentication(JWTAuthentication):
def authenticate(self, request):
token = self.get_header(request)
if not token:
raise AuthenticationFailed("請先登入或攜帶token資訊")
payload = self.get_raw_token(token)
validated_token = self.get_validated_token(payload)
user = self.get_user(validated_token)
if not user.locked:
return user, validated_token
else:
raise AuthenticationFailed("當前使用者已凍結")
【4】多方式登陸
- 繼承
TokenObtainPairSerializer
並重寫validate
方法就能夠實現手動校驗
class LoginSerializer(TokenObtainPairSerializer):
username = serializers.CharField()
password = serializers.CharField()
def _get_user(self, username, password):
if re.match('^(13|14|15|18)[0-9]{9}$', username):
user = User.objects.filter(mobile=username, password=password).first()
elif re.match('^[a-z\d]+(\.[a-z\d]+)*@([\da-z](-[\da-z])?)+\.[a-z]+$', username):
user = User.objects.filter(email=username, password=password).first()
else:
user = User.objects.filter(username=username, password=password).first()
return user
def get_token(cls, user):
token = super().get_token(user)
token['name'] = user.username
return token
def validate(self, attrs):
username = attrs.get('username')
password = attrs.get('password')
user = self._get_user(username, password)
if not user:
raise ValidationError("使用者名稱或密碼錯誤")
token = self.get_token(user)
self.context['refresh'] = str(token)
self.context['token'] = str(token.access_token)
return attrs
【三】自定義JWT認證
- 基於base64、md5和django金鑰進行加密
- headers:使用base64對包含頭資訊的字典進行加密
- payload:載荷中包含使用者的使用者名稱和密碼及其他資訊
- Signature:簽名由經過base64加密過的headers和payload並加入django金鑰作為鹽,由md5進行加密
- 由於md5和base64,嚴格來說都不是加密演算法,同樣的資料經過同樣的加密方式獲取的資料一致
- 透過後端加密的資料與前端傳入的token進行比對
- 由於我們的django金鑰是絕對私密的,經過比對,就能校驗出該token是否由我們簽發的
################# 定義類 ##################
import hashlib
import json
import base64
import time
from django.conf import settings
class CommonJWT:
def __init__(self, header=None, salt=None, expired=180):
'''
初始化token的引數
'''
if not header:
# 如果不指定headers # 使用預設的headers
self.header = header = {'alg': 'md5', 'type': 'JWT'}
self.header = header # 可以拿到原始的頭資訊
# 類方法中使用經過base64編碼過的頭
self.token_headers = self._encode_base64(header)
if not salt:
# 如果不指定鹽 # 使用預設的鹽
salt = settings.SECRET_KEY # django的金鑰
self.salt = salt.encode(encoding='utf-8')
self.create_time = time.time()
self.expired = expired
@staticmethod
def _encode_base64(data: dict):
data_string = json.dumps(data).encode(encoding='utf8')
return base64.b64encode(data_string)
@staticmethod
def _decode_base64(data):
return base64.b64decode(data)
def _encrypt_md5(self, data):
md5 = hashlib.md5()
md5.update(data + self.salt)
return md5.hexdigest().encode(encoding='utf8')
def create_token(self, create_time=None, **kwargs):
if not create_time:
create_time = self.create_time
# 將傳遞的資料作為載荷建立
payload = kwargs
# create_time 因為後續驗證的時候需要將老的token時間再拿回來,所以create_time放在最後面,否則校驗的時候順序就會不對
payload.update({'expired': self.expired, 'create_time': create_time})
# 將資料進行base64編碼
token_payload = self._encode_base64(payload)
# 我的headers頭是在物件初始化時定義的,這裡可以直接呼叫
# 簽名由 頭 + 載荷 + 鹽 的資料拼接並使用摘要演算法完成構建
signature = self._encrypt_md5(data=self.token_headers + token_payload)
# 將 前面再次透過base64進行編碼
signature = base64.b64encode(signature)
# 使用【.】進行拼接 返回token
return b'.'.join((self.token_headers, token_payload, signature)) # bytes
def get_payload_from_token(self, token):
# 將token按【.】切分 # 分為三個部分
token_headers, token_payload, token_sign = token.split('.')
# 中間部分就是攜帶的荷載
payload = json.loads(self._decode_base64(token_payload))
return payload
################### 使用 ##############
# 登入簽發token
class UserView(ViewSetMixin, CreateAPIView):
queryset = UserInfo.objects.all()
serializer_class = UserInfoSerializer
authentication_classes = []
@action(methods=['post'], detail=False)
def login(self, request, *args, **kwargs):
username = request.data.get('username')
password = request.data.pop('password')
user = auth.authenticate(username=username, password=password)
if not user:
return Response({'code': 101, 'msg': '使用者名稱或密碼錯誤'})
token = CommonJWT().create_token(**request.data)
return Response({'code': 100, 'msg': '登入成功', 'token': token})
############### 認證類校驗token ##############
class UserAuthentication(BaseAuthentication):
def authenticate(self, request):
jwt = CommonJWT()
token = request.META.get('HTTP_TOKEN')
if not token:
raise AuthenticationFailed("請先登入")
payload = jwt.get_payload_from_token(token=token)
create_time = payload.pop('create_time')
expired = payload.get('expired')
# user = auth.authenticate(**payload)
# if not user:
# raise AuthenticationFailed('使用者名稱或密碼有誤')
username = payload.get('username')
user = UserInfo.objects.filter(username=username).first()
if not user:
raise AuthenticationFailed("token有誤")
if time.time() - create_time > expired:
raise AuthenticationFailed("當前token已過期")
true_token = jwt.create_token(create_time=create_time, **payload)
if token.encode('utf-8') == true_token:
return user, token
else:
raise AuthenticationFailed("請檢查token")