一 JWT認證
自帶的認證方式
token值放置請求頭里
key:AUTHORIZATION
value :JWT+空格+token 值
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework_jwt.authentication import get_authorization_header
class test(GenericViewSet):
authentication_classes = [JSONWebTokenAuthentication]
permission_classes = [IsAuthenticated]
def test(self,request):
return APIResponse(data=123456)
在用戶注冊或登錄后,我們想記錄用戶的登錄狀態,或者為用戶創建身份認證的憑證。我們不再使用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).
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')
|
二 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
|
python3 manage.py createsuperuser
|
三 實戰之使用Django auth的User表自動簽發
3.1 配置setting.py
1 2 3 4 5 6 7 8
|
import datetime JWT_AUTH = {
|
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
|
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的全局認證:
從頭部 Authorization 字段取出 JWT
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
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 import jwt from rest_framework_jwt.serializers import jwt_payload_handler,jwt_encode_handler class JSONWebTokenAuthentication(BaseJSONWebTokenAuthentication): def authenticate(self, request): jwt_value = get_authorization_header(request) if not 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) payload = jwt_payload_handler(user) token = jwt_encode_handler(payload) return user, jwt_value |
3.5 全局使用
1 2 3 4 5 6 7
|
REST_FRAMEWORK = {
|
3.6 局部啟用禁用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
authentication_classes = []
|
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
|
from rest_framework.viewsets import ViewSet from rest_framework.response import Response classLoginViewSet(ViewSet):
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})
classLoginSerializer(serializers.ModelSerializer):
username = serializers.CharField() classMeta: model = models.User
fields = ('username', 'password')
defvalidate(self, attrs):
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
|
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
|
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
|
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:
|
4.3 編寫認證組件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
from users.models import User from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed class MyJSONWebTokenAuthentication(BaseAuthentication): def authenticate(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_ID = payload.get(user_id) user = User.objects.filter(username=username).first() print(user)
return user, jwt_value
|
class LoginView(APIView):
def post(self,request):
authentication_classes = [
MyJSONWebTokenAuthentication
]
print(request.user)
return
4.4 登陸獲取token

4.5 編寫測試接口
1 2 3 4 5 6 7
|
from users.app_auth import JSONWebTokenAuthentication,MyJSONWebTokenAuthentication classOrderView(APIView):
authentication_classes = [MyJSONWebTokenAuthentication] defget(self,request): print(request.user) return CommonResponse('100', '成功',{'數據':'測試'})
|
4.6 測試
