JWT認證(5星)
token發展史
在用戶注冊或登錄后,我們想記錄用戶的登錄狀態,或者為用戶創建身份認證的憑證。我們不再使用Session認證機制,而使用Json Web Token(本質就是token)認證機制。
構成和工作原理
JWT的構成
JWT就是一段字符串,由三段信息構成的,將這三段信息文本用.
鏈接一起就構成了Jwt字符串。就像這樣:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
第一部分我們稱它為頭部(header),第二部分我們稱其為荷載(payload, 類似於飛機上承載的物品),第三部分是簽證(signature).
header(頭部)
jwt的頭部承載兩部分信息:
- 聲明類型,這里是jwt
- 聲明加密的算法 通常直接使用 HMAC SHA256
完整的頭部就像下面這樣的JSON:
{
'typ': 'JWT',
'alg': 'HS256'
}
然后將頭部進行base64加密(該加密是可以對稱解密的),構成了第一部分.
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
payload(荷載)
荷載就是存放類似用戶信息,過期時間,簽發時間...
{
"userid": "1",
"name": "John Doe",
"exp": 1214356
}
然后將其進行base64加密,得到JWT的第二部分。
eyJ1c2VyaWQiOiAiMSIsICJuYW1lIjogIkpvaG4gRG9lIiwgImV4cCI6IDEyMTQzNTZ9
signature(簽證)
JWT的第三部分是一個簽證信息,這個簽證信息由三部分組成:
- header (base64解密后加密算法加密后的)
- payload (base64解密后加密算法加密后的)
- secret(密鑰=加鹽)
這個部分需要base64加密后的header和base64加密后的payload使用.
連接組成的字符串,然后通過header中聲明的加密方式進行加鹽secret
組合加密,然后用base64進行加密,最后就構成了jwt的第三部分。
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
將這三部分用.
連接成一個完整的字符串,構成了最終的jwt:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyaWQiOiAiMSIsICJuYW1lIjogIkpvaG4gRG9lIiwgImV4cCI6IDEyMTQzNTZ9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
注意:secret是保存在服務器端的,jwt的簽發生成也是在服務器端的,secret就是用來進行jwt的簽發和jwt的驗證,所以,它就是你服務端的私鑰,在任何場景都不應該流露出去。一旦客戶端得知這個secret, 那就意味着客戶端是可以自我簽發jwt了。
關於簽發和核驗JWT,我們可以使用Django REST framework JWT擴展來完成。
文檔網站:http://getblimp.github.io/django-rest-framework-jwt/
補充base64編碼解碼
import base64
import json
payload = {
"userid": "1",
"name": "John Doe",
"exp": 1214356
}
json_payload = json.dumps(payload)
# 編碼
res = base64.b64encode(json_payload.encode('utf8'))
print(res)
# 解碼
res2 = json.loads(base64.b64decode(res))
print(res2)
# b'eyJ1c2VyaWQiOiAiMSIsICJuYW1lIjogIkpvaG4gRG9lIiwgImV4cCI6IDEyMTQzNTZ9'
# {'userid': '1', 'name': 'John Doe', 'exp': 1214356}
本質原理
jwt認證算法:簽發與校驗
1)jwt分三段式:頭.體.簽名 (head.payload.sgin)
2)頭和體是可逆加密,讓服務器可以反解出user對象;簽名是不可逆加密,保證整個token的安全性的(base64反解出的是hash加密后的密文)
3)頭體簽名三部分,都是采用json格式的字符串,進行加密,可逆加密一般采用base64算法,不可逆加密一般采用hash(md5)算法
4)頭中的內容是基本信息:公司信息、項目組信息、token采用的加密方式信息
{
"company": "公司信息",
...
}
5)體中的內容是關鍵信息:用戶主鍵、用戶名、簽發時客戶端信息(設備號、地址)、過期時間
{
"user_id": 1,
...
}
6)簽名中的內容是安全信息:頭的加密結果 + 體的加密結果 + 服務器不對外公開的安全碼(對整個字典進行md5加密)
{
"head": "頭的加密字符串",
"payload": "體的加密字符串",
"secret": "安全碼"
}
簽發:根據登錄請求提交來的 賬號 + 密碼 + 設備信息 簽發 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就能訪問登錄的用戶
注:登錄接口需要做 認證 + 權限 兩個局部禁用
drf-jwt安裝和簡單使用(2星)
安裝
pip3 install djangorestframework-jwt
簡單使用
簽發
# 1 創建超級用戶
python3 manage.py createsuperuser
# 解釋下為什么要創建超級用戶:因為djangorestframework-jwt認證是基於django的auth里的user表作關聯的,所以驗證的數據也必須源自於這張表
# 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
認證
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.permissions import IsAuthenticated
class BookAPIView(ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookModelSerializer
# 必須用這個認證類
authentication_classes = [JSONWebTokenAuthentication, ]
# 還要配合這個權限
permission_classes = [IsAuthenticated, ]
在postman里
JWT使用auth表簽發token,自定制返回格式(3星)
配置setting.py
JWT_AUTH ={
# token的過期時間
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7),
# 如果不自定義,返回的格式是固定的,只有token字段
# 這里把下面自定制的函數注冊進來
'JWT_RESPONSE_PAYLOAD_HANDLER': 'app01.utils.jwt_response_payload_handler',
}
自定制的py文件內
def jwt_response_payload_handler(token, user=None, request=None):
return {
'code': 1000,
'msg': '登陸成功',
'username': user.username,
'token': token
}
這時登陸時返回的格式就變成了:
djangorestframework-jwt模塊源碼分析(2星)
簽發token
ObtainJSONWebToken.as_view()--->ObtainJSONWebToken---->post方法
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
if serializer.is_valid(): # 驗證用戶登錄和簽發token,都在序列化類的validate方法中完成的
user = serializer.object.get('user') or request.user
token = serializer.object.get('token')
response_data = jwt_response_payload_handler(token, user, request)
response = Response(response_data)
# 返回了咱們自定指的格式
'''
{
'code':100,
'msg':'登錄成功',
'username':user.username,
'token': token,
}
'''
return response
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
# 全局鈎子函數
def validate(self, attrs):
credentials = {
self.username_field: attrs.get(self.username_field),
'password': attrs.get('password')
}
if all(credentials.values()):
# 根據用戶名密碼去auth的user表校驗,是否存在
user = authenticate(**credentials)
if user:
if not user.is_active:
msg = _('User account is disabled.')
raise serializers.ValidationError(msg)
# 生成payload
payload = jwt_payload_handler(user)
return {
'token': jwt_encode_handler(payload), # 通過payload生成token
'user': user
}
else:
# 不在拋異常,前端就看到信息了
raise serializers.ValidationError(msg)
else:
raise serializers.ValidationError(msg)
認證
JWT使用自定義User表,手動簽發token,自定義認證類(5星)
簽發token
重點在於
1.通過用戶輸入的用戶名和密碼去數據庫中查出該用戶
2.獲取到的用戶信息生成荷載(payload),jwt模塊提供了
3.通過荷載來生成toekn,jwt模塊提供了
4.把含有token串的字典返回給前端
from rest_framework.viewsets import ViewSet, ViewSetMixin
from rest_framework.generics import ListAPIView
from rest_framework.decorators import action
from .models import UserInfo, Book
from rest_framework.response import Response
from rest_framework_jwt.settings import api_settings
from .serializer import BookSerializer, UserInfoSerializer
from .authentcate import MyAuthentication
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
class UserAPIView(ViewSet):
@action(methods=['POST', ], detail=False)
def login(self, request):
back_dic = {'code': 100, 'msg': '登陸成功'}
username = request.data.get('username')
password = request.data.get('password')
user = UserInfo.objects.filter(username=username, password=password).first()
if user:
# 獲取荷載 直接用jwt模塊提供的,缺什么導什么
payload = jwt_payload_handler(user)
# 獲取token串 直接用jwt模塊提供的,缺什么導什么
token = jwt_encode_handler(payload)
back_dic['token'] = token
back_dic['username'] = username
else:
back_dic['code'] = 101
back_dic['msg'] = '用戶名或密碼錯誤'
return Response(back_dic)
自定義認證類
因為認證類要重寫authenticate方法,所以重點就是在authenticate方法中寫下面邏輯:
1.取出客戶端傳入的token(后端自己規定),看是攜帶在請求頭中,還是在請求地址中
2.驗證token中的簽名(jwt模塊提供了)
3.通過payload得到當前登陸的用戶對象(jwt模塊提供了)
4.返回user對象和token(或者是其他參數)
import jwt
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
from rest_framework_jwt.settings import api_settings
from .models import UserInfo
jwt_decode_handler = api_settings.JWT_DECODE_HANDLER
class MyAuthentication(BaseAuthentication):
def authenticate(self, request):
# 獲取前端傳的token串
jwt_value = request.META.get('HTTP_TOKEN')
if not jwt_value:
raise AuthenticationFailed('未攜帶token')
try:
# 獲取荷載 直接用jwt模塊提供的,缺什么導什么
payload = jwt_decode_handler(jwt_value)
except jwt.ExpiredSignature:
msg = 'token已過期'
raise AuthenticationFailed(msg)
except jwt.DecodeError:
msg = 'token被篡改'
raise AuthenticationFailed(msg)
except jwt.InvalidTokenError:
raise AuthenticationFailed('未知錯誤')
# 獲取用戶對象 用自定義的User表獲取對象
user = UserInfo.objects.filter(pk=payload['user_id']).first()
# 上面的方法每次認證都要查數據庫,下面有兩種方法做優化,減少數據庫壓力
# 這是實例化得到user對象,沒有去數據庫查表,提高了性能,但是只能取出你傳的字段數據
user=User(id=payload.get('user_id'),username=payload.get('username'))
# 直接組織成字典,因為我們后續主要用的是用戶id,視圖類中按字典取值就行了
user={'id':payload.get('user_id'),'username':payload.get('username')}
# 把對象和token返回
return user, jwt_value
登陸邏輯寫在序列化類里(以后這種常寫)
在views.py中
class UserAPIView(ViewSet):
@action(methods=['POST', ], detail=False)
def login(self, request):
back_dic = {'code': 100, 'msg': '登陸成功'}
# 調用序列化類傳入參數獲得序列化類的對象(所有的邏輯都在全局鈎子函數里實現的)
# 可以把context={'request':request}傳入,那么在序列化類中就可以獲取request對象
ser = UserInfoSerializer(data=request.data)
# 如果驗證通過說明已經走完字段校驗及鈎子函數
if ser.is_valid():
# 通過在鈎子函數中對context字典中放的數據獲取用戶名和token並返回
username = ser.context['username']
token = ser.context['token']
back_dic['username'] = username
back_dic['token'] = token
else:
# 返回錯誤信息
back_dic['code'] = 101
back_dic['msg'] = ser.errors
return Response(back_dic)
在序列化類.py中
from .models import Book, UserInfo
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from rest_framework_jwt.settings import api_settings
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
class UserInfoSerializer(serializers.ModelSerializer):
class Meta:
model = UserInfo
fields = ['id', 'username', 'password']
# 字段本身的校驗
username = serializers.CharField(max_length=10, min_length=3)
password = serializers.CharField(max_length=10, min_length=3)
# 全局鈎子函數(核心)
def validate(self, attrs):
username = attrs.get('username')
password = attrs.get('password')
user = UserInfo.objects.filter(username=username, password=password).first()
if not user:
raise ValidationError('用戶名或密碼錯誤')
# 獲取荷載 直接用jwt模塊提供的,缺什么導什么
payload = jwt_payload_handler(user)
# 獲取token 直接用jwt模塊提供的,缺什么導什么
token = jwt_encode_handler(payload)
# context字典是與視圖函數溝通的橋梁,這里放,那里取,那里放,這里取
self.context['username'] = username
self.context['token'] = token
return attrs
補充: context字典是視圖類與序列化類溝通的橋梁
在views.py中
class UserAPIView(ViewSet):
@action(methods=['POST', ], detail=False)
def login(self, request):
# 可以把context={'request':request}傳入,那么在序列化類中就可以獲取request對象
ser = UserInfoSerializer(data=request.data, context={'request':request})
if ser.is_valid():
...
在序列化類.py中
# 如果視圖函數中傳了reqeust,也可以取出
def validate(self, attrs):
request = self.context['request']
print(request.method)
...
多功能登陸
邏輯:
1.獲取用戶提交的用戶名和密碼
2.因為用戶名可能是手機、郵箱、用戶名,所以用正則進行判斷
3.校驗成功后簽發token
普通版
from rest_framework.viewsets import ViewSet, ViewSetMixin
from rest_framework.generics import ListAPIView
from rest_framework.decorators import action
from .models import UserInfo, Book
from rest_framework.response import Response
from rest_framework_jwt.settings import api_settings
from .serializer import BookSerializer, UserInfoSerializer
from .authentcate import MyAuthentication
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
import re
class UserAPIView(ViewSet):
@action(methods=['post', ], detail=False)
def login(self, request):
back_dic = {'code': 100, 'msg': '登陸成功'}
username = request.data.get('username')
password = request.data.get('password')
# 用正則判斷到底是哪種登陸方式
re_phone = re.compile('^1([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])\d{8}$')
re_email = re.compile('^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$')
if re_phone.search(username):
user = UserInfo.objects.filter(phone=username, password=password).first()
elif re_email.search(username):
user = UserInfo.objects.filter(email=username, password=password).first()
else:
user = UserInfo.objects.filter(username=username, password=password).first()
if not user:
back_dic['code'] = 101
back_dic['msg'] = '用戶名或密碼錯誤'
else:
# 獲取荷載 直接用jwt模塊提供的,缺什么導什么
payload = jwt_payload_handler(user)
# 獲取token 直接用jwt模塊提供的,缺什么導什么
token = jwt_encode_handler(payload)
back_dic['username'] = user.username
back_dic['token'] = token
return Response(back_dic)
進階版(邏輯寫在序列化類)
在views.py中
from rest_framework.viewsets import ViewSet, ViewSetMixin
from rest_framework.generics import ListAPIView
from rest_framework.decorators import action
from .models import UserInfo, Book
from rest_framework.response import Response
from rest_framework_jwt.settings import api_settings
from .serializer import BookSerializer, UserInfoSerializer
from .authentcate import MyAuthentication
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
import re
class UserAPIView(ViewSet):
@action(methods=['post', ], detail=False)
def login(self, request):
back_dic = {'code': 100, 'msg': '登陸成功'}
# 調用序列化類傳入參數獲得序列化類的對象(所有的邏輯都在全局鈎子函數里實現的)
# 可以把context={'request':request}傳入,那么在序列化類中就可以獲取request對象
ser = UserInfoSerializer(data=request.data)
# 如果驗證通過說明已經走完字段校驗及鈎子函數
if ser.is_valid():
# 通過在鈎子函數中對context字典中放的數據獲取用戶名和token並返回
username = ser.context['username']
token = ser.context['token']
back_dic['username'] = username
back_dic['token'] = token
else:
back_dic['code'] = 101
back_dic['msg'] = ser.errors
return Response(back_dic)
在序列化類.py中
from .models import Book, UserInfo
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from rest_framework_jwt.settings import api_settings
import re
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
class UserInfoSerializer(serializers.ModelSerializer):
class Meta:
model = UserInfo
fields = ['id', 'username', 'phone', 'email', 'password']
# 不重寫的話會因為User表的username字段本身是unique的,所以用戶傳的username肯定在User表內存在,所以肯定會報錯
username = serializers.CharField()
password = serializers.CharField(max_length=10, min_length=3)
# 獲取user對象的函數(拆分有助於擴展)
def _get_user(self, attrs):
username = attrs.get('username')
password = attrs.get('password')
# 用正則判斷到底是哪種登陸方式
re_phone = re.compile('^1([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])\d{8}$')
re_email = re.compile('^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$')
if re_phone.search(username):
user = UserInfo.objects.filter(phone=username, password=password).first()
elif re_email.search(username):
user = UserInfo.objects.filter(email=username, password=password).first()
else:
user = UserInfo.objects.filter(username=username, password=password).first()
if not user:
raise ValidationError('用戶名或密碼錯誤')
return user
def validate(self, attrs):
user = self._get_user(attrs)
# 獲取荷載 直接用jwt模塊提供的,缺什么導什么
payload = jwt_payload_handler(user)
# 獲取token 直接用jwt模塊提供的,缺什么導什么
token = jwt_encode_handler(payload)
# context字典是與視圖函數溝通的橋梁,這里放,那里取,那里放,這里取
self.context['username'] = user.username
self.context['token'] = token
return attrs