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

drf之JWT認證

一 JWT認證

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

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

1.1 構成和工作原理

JWT的構成

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

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

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

1.1.1 header

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

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

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

1
2
3
4
{
'typ': 'JWT',
'alg': 'HS256'
}

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

1
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

1.1.2 payload

載荷就是存放有效資訊的地方。這個名字像是特指飛機上承載的貨品,這些有效資訊包含三個部分

  • 標準中註冊的宣告
  • 公共的宣告
  • 私有的宣告

標準中註冊的宣告 (建議但不強制使用) :

  • iss: jwt簽發者
  • sub: jwt所面向的使用者
  • aud: 接收jwt的一方
  • exp: jwt的過期時間,這個過期時間必須要大於簽發時間
  • nbf: 定義在什麼時間之前,該jwt都是不可用的.
  • iat: jwt的簽發時間
  • jti: jwt的唯一身份標識,主要用來作為一次性token,從而回避時序攻擊。

公共的宣告 : 公共的宣告可以新增任何的資訊,一般新增使用者的相關資訊或其他業務需要的必要資訊.但不建議新增敏感資訊,因為該部分在客戶端可解密.

私有的宣告 : 私有宣告是提供者和消費者所共同定義的宣告,一般不建議存放敏感資訊,因為base64是對稱解密的,意味著該部分資訊可以歸類為明文資訊。

定義一個payload:

1
2
3
4
5
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}

然後將其進行base64加密,得到JWT的第二部分。

1
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

1.1.3 signature

JWT的第三部分是一個簽證資訊,這個簽證資訊由三部分組成:

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

這個部分需要base64加密後的header和base64加密後的payload使用.連線組成的字串,然後通過header中宣告的加密方式進行加鹽secret組合加密,然後就構成了jwt的第三部分。

1
2
3
4
// javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);

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

將這三部分用.連線成一個完整的字串,構成了最終的jwt:

1
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
"""
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
2
3
4
5
6
7
"""
1)用基本資訊儲存json字典,採用base64演算法加密得到 頭字串
2)用關鍵資訊儲存json字典,採用base64演算法加密得到 體字串
3)用頭、體加密字串再加安全碼資訊儲存json字典,採用hash md5演算法加密得到 簽名字串

賬號密碼就能根據User表得到user物件,形成的三段字串用 . 拼接成token返回給前臺
"""
校驗:根據客戶端帶token的請求 反解出 user 物件
1
2
3
4
5
"""
1)將token按 . 拆分為三段字串,第一段 頭加密字串 一般不需要做任何處理
2)第二段 體加密字串,要反解出使用者主鍵,通過主鍵從User表中就能得到登入使用者,過期時間和裝置資訊都是安全資訊,確保token沒過期,且時同一裝置來的
3)再用 第一段 + 第二段 + 伺服器安全碼 不可逆md5加密,與第三段 簽名字串 進行碰撞校驗,通過後才能代表第二段校驗得到的user物件就是合法的登入使用者
"""

drf專案的jwt認證開發流程(重點)

1
2
3
4
5
6
7
"""
1)用賬號密碼訪問登入介面,登入介面邏輯中呼叫 簽發token 演算法,得到token,返回給客戶端,客戶端自己存到cookies中

2)校驗token的演算法應該寫在認證類中(在認證類中呼叫),全域性配置給認證元件,所有檢視類請求,都會進行認證校驗,所以請求帶了token,就會反解出user物件,在檢視類中用request.user就能訪問登入的使用者

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

1.2.1 補充base64編碼解碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 官網
1
http://getblimp.github.io/django-rest-framework-jwt/
2.2 安裝
1
pip install djangorestframework-jwt
2.3 使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
# 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空格)

三 實戰之使用Django auth的User表自動簽發

3.1 配置setting.py

1
2
3
4
5
6
7
8
import datetime
JWT_AUTH = {
# 過期時間1天
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1),
# 自定義認證結果:見下方序列化user和自定義response
# 如果不自定義,返回的格式是固定的,只有token欄位
'JWT_RESPONSE_PAYLOAD_HANDLER': 'users.utils.jwt_response_payload_handler',
}

3.2 編寫序列化類ser.py

1
2
3
4
5
6
from rest_framework import serializers
from users import models
class UserModelSerializers(serializers.ModelSerializer):
classMeta:
model = models.UserInfo
fields = ['username']
3.3 自定義認證返回結果(setting中配置的)
1
2
3
4
5
6
7
8
9
10
11
#utils.py
from users.ser import UserModelSerializers
defjwt_response_payload_handler(token, user=None, request=None):
return {
'status': 0,
'msg': 'ok',
'data': {
'token': token,
'user': UserModelSerializers(user).data
}
}

3.4 基於drf-jwt的全域性認證:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#app_auth.py(自己建立)
from rest_framework.exceptions import AuthenticationFailed
from rest_framework_jwt.authentication import jwt_decode_handler
from rest_framework_jwt.authentication import get_authorization_header,jwt_get_username_from_payload
from rest_framework_jwt.authentication import BaseJSONWebTokenAuthentication

classJSONWebTokenAuthentication(BaseJSONWebTokenAuthentication):
defauthenticate(self, request):
jwt_value = get_authorization_header(request)

ifnot jwt_value:
raise AuthenticationFailed('Authorization 欄位是必須的')
try:
payload = jwt_decode_handler(jwt_value)
except jwt.ExpiredSignature:
raise AuthenticationFailed('簽名過期')
except jwt.InvalidTokenError:
raise AuthenticationFailed('非法使用者')
user = self.authenticate_credentials(payload)

return user, jwt_value

3.5 全域性使用

1
2
3
4
5
6
7
# setting.py
REST_FRAMEWORK = {
# 認證模組
'DEFAULT_AUTHENTICATION_CLASSES': (
'users.app_auth.JSONWebTokenAuthentication',
),
}

3.6 區域性啟用禁用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 區域性禁用
authentication_classes = []
# 區域性啟用
from user.authentications import JSONWebTokenAuthentication
authentication_classes = [JSONWebTokenAuthentication]
# 實際程式碼如下view.py
# 自定義Response
classCommonResponse(Response):
def__init__(self,status,msg,data='',*args,**kwargs):
dic={'status':status,'msg':msg,'data':data}
super().__init__(data=dic,*args,**kwargs)
# 測試訂單介面
from users.app_auth import JSONWebTokenAuthentication
classOrderView(APIView):
# authentication_classes = [JSONWebTokenAuthentication]
authentication_classes = []
defget(self,request):
return CommonResponse('100', '成功',{'資料':'測試'})

3.7 多方式登入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
## views.py
# 重點:自定義login,完成多方式登入
from rest_framework.viewsets import ViewSet
from rest_framework.response import Response
classLoginViewSet(ViewSet):
# 需要和mixins結合使用,繼承GenericViewSet,不需要則繼承ViewSet
# 為什麼繼承檢視集,不去繼承工具檢視或檢視基類,因為檢視集可以自定義路由對映:
# 可以做到get對映get,get對映list,還可以做到自定義(靈活)
deflogin(self, request, *args, **kwargs):
serializer = serializers.LoginSerializer(data=request.data, context={'request': request})
serializer.is_valid(raise_exception=True)
token = serializer.context.get('token')
return Response({"token": token})




## ser.py
# 重點:自定義login,完成多方式登入
classLoginSerializer(serializers.ModelSerializer):
# 登入請求,走的是post方法,預設post方法完成的是create入庫校驗,所以唯一約束的欄位,會進行資料庫唯一校驗,導致邏輯相悖
# 需要覆蓋系統欄位,自定義校驗規則,就可以避免完成多餘的不必要校驗,如唯一欄位校驗
username = serializers.CharField()
classMeta:
model = models.User
# 結合前臺登入佈局:採用賬號密碼登入,或手機密碼登入,佈局一致,所以不管賬號還是手機號,都用username欄位提交的
fields = ('username', 'password')

defvalidate(self, attrs):
# 在全域性鉤子中,才能提供提供的所需資料,整體校驗得到user
# 再就可以呼叫簽發token演算法,將user資訊轉換為token
# 將token存放到context屬性中,傳給外來鍵檢視類使用
user = self._get_user(attrs)
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
self.context['token'] = token
return attrs

# 多方式登入
def_get_user(self, attrs):
username = attrs.get('username')
password = attrs.get('password')
import re
if re.match(r'^1[3-9][0-9]{9}$', username):
# 手機登入
user = models.User.objects.filter(mobile=username, is_active=True).first()
elif re.match(r'^.+@.+$', username):
# 郵箱登入
user = models.User.objects.filter(email=username, is_active=True).first()
else:
# 賬號登入
user = models.User.objects.filter(username=username, is_active=True).first()
if user and user.check_password(password):
return user

raise ValidationError({'user': 'user error'})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# utils.py
import re
from .models import User
from django.contrib.auth.backends import ModelBackend
classJWTModelBackend(ModelBackend):
defauthenticate(self, request, username=None, password=None, **kwargs):
try:
if re.match(r'^1[3-9]\d{9}$', username):
user = User.objects.get(mobile=username)
else:
user = User.objects.get(username=username)
except User.DoesNotExist:
returnNone
if user.check_password(password) and self.user_can_authenticate(user):
return user

3.8 配置多方式登入

1
2
settings.py
AUTHENTICATION_BACKENDS = ['user.utils.JWTModelBackend']

四 實戰之自定義User表,手動簽發

4.1 手動簽發JWT:

1
2
3
4
5
6
7
# 可以擁有原生登入基於Model類user物件簽發JWT
from rest_framework_jwt.settings import api_settings
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER

payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)

4.2 編寫登陸檢視類

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# views.py
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 users.models import User
classLoginView(APIView):
authentication_classes = []
defpost(self,request):
username=request.data.get('username')
password=request.data.get('password')
user=User.objects.filter(username=username,password=password).first()
if user: # 能查到,登陸成功,手動簽發
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
return CommonResponse('100','登陸成功',data={'token':token})
else:
return CommonResponse('101', '登陸失敗')

4.3 編寫認證元件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# app_auth.py
from users.models import User
classMyJSONWebTokenAuthentication(BaseAuthentication):
defauthenticate(self, request):
jwt_value = get_authorization_header(request)

ifnot jwt_value:
raise AuthenticationFailed('Authorization 欄位是必須的')
try:
payload = jwt_decode_handler(jwt_value)
except jwt.ExpiredSignature:
raise AuthenticationFailed('簽名過期')
except jwt.InvalidTokenError:
raise AuthenticationFailed('非法使用者')
username = jwt_get_username_from_payload(payload)
print(username)
user = User.objects.filter(username=username).first()
print(user)

return user, jwt_value

4.4 登陸獲取token

4.5 編寫測試介面

1
2
3
4
5
6
7
from users.app_auth import JSONWebTokenAuthentication,MyJSONWebTokenAuthentication
classOrderView(APIView):
# authentication_classes = [JSONWebTokenAuthentication]
authentication_classes = [MyJSONWebTokenAuthentication]
defget(self,request):
print(request.user)
return CommonResponse('100', '成功',{'資料':'測試'})

4.6 測試