[Django REST framework - JWT認證、token刷新機制、多方式登錄]
JWT認證
官網:https://github.com/jpadilla/django-rest-framework-jwt
在用戶注冊或登錄后,我們想記錄用戶的登錄狀態,或者為用戶創建身份認證的憑證。我們不再使用Session認證機制,而使用Json Web Token(本質就是token)認證機制.
jwt:json web token,前后端的認證方式,有三段:頭,荷載,簽名,區別於session,不需要在服務端存儲信息,還能保證認證的安全
Json web token (JWT), 是為了在網絡應用環境間傳遞聲明而執行的一種基於JSON的開放標准((RFC 7519).該token被設計為緊湊且安全的,特別適用於分布式站點的單點登錄(SSO)場景。JWT的聲明一般被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息,以便於從資源服務器獲取資源,也可以增加一些額外的其它業務邏輯所必須的聲明信息,該token也可直接被用於認證,也可被加密。
jwt優勢
1)沒有數據庫寫操作,高效
2)服務器不存token,低耗
3)簽發校驗都是算法,集群
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就能訪問登錄的用戶
注:登錄接口需要做 認證 + 權限 兩個局部禁用
"""
drf-jwt框架基本使用
安裝(終端)
>: pip install djangorestframework-jwt
簽發token(登錄接口):視圖類已經寫好了,配置一下路由就行(urls.py)
# urls.py
urlpatterns = [
# ...
url('^login/$', ObtainJSONWebToken.as_view()),
]
# Postman請求:/login/,提供username和password即可
校驗token(認證組件):認證類已經寫好了,全局配置一下認證組件就行了(settings.py)
# drf-jwt的配置
import datetime
JWT_AUTH = {
# 配置過期時間
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7),
}
# drf配置(把配置放在最下方)
REST_FRAMEWORK = {
# 自定義三大認證配置類們
'DEFAULT_AUTHENTICATION_CLASSES': ['rest_framework_jwt.authentication.JSONWebTokenAuthentication'],
# 'DEFAULT_PERMISSION_CLASSES': [],
# 'DEFAULT_THROTTLE_CLASSES': [],
}
設置需要登錄才能訪問的接口進行測試(views.py)
from rest_framework.permissions import IsAuthenticated
class UserCenterViewSet(GenericViewSet, mixins.RetrieveModelMixin):
# 也可局部配置認證
authentication_classes = [JSONWebTokenAuthentication]
# 設置必須登錄才能訪問的權限類
permission_classes = [IsAuthenticated, ]
queryset = models.User.objects.filter(is_active=True).all()
serializer_class = serializers.UserCenterSerializer
測試訪問登錄認證接口(Postman)
"""
1)用 {"username": "你的用戶", "password": "你的密碼"} 訪問/login/ 接口等到 token 字符串
2)在請求頭用 Authorization 攜帶 "jwt 登錄得到的token" 訪問 /book/ 接口訪問個人中心
"""
token刷新機制(了解)
drf-jwt直接提供刷新功能
"""
1)運用在像12306這樣極少數安全性要求高的網站
2)第一個token由登錄簽發
3)之后的所有正常邏輯,都需要發送兩次請求,第一次是刷新token的請求,第二次是正常邏輯的請求
"""
settings.py
import datetime
JWT_AUTH = {
# 配置過期時間
'JWT_EXPIRATION_DELTA': datetime.timedelta(minutes=5),
# 是否可刷新
'JWT_ALLOW_REFRESH': True,
# 刷新過期時間
'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7),
}
urls.py
from rest_framework_jwt.views import ObtainJSONWebToken, RefreshJSONWebToken
urlpatterns = [
url('^login/$', ObtainJSONWebToken.as_view()), # 登錄簽發token接口
url('^refresh/$', RefreshJSONWebToken.as_view()), # 刷新toekn接口
]
Postman
# 接口:/refresh/
# 方法:post
# 數據:{"token": "登錄簽發的token"}
基於jwt的認證類(重寫認證類)
1 重點邏輯authenticate方法中
-取出客戶端傳入的token(后端自己規定,寫道接口文檔中過了),請求頭中,請求地址。。。
-驗證jwt的簽名(模塊提供了)
-通過payload得到當前登錄用戶對象(模塊提供了)
-return user,token
from rest_framework.exceptions import AuthenticationFailed
from rest_framework_jwt.settings import api_settings
import jwt
# from rest_framework_jwt.utils import jwt_decode_handler
jwt_decode_handler = api_settings.JWT_DECODE_HANDLER
jwt_get_username_from_payload = api_settings.JWT_PAYLOAD_GET_USERNAME_HANDLER
from rest_framework_jwt.authentication import BaseJSONWebTokenAuthentication
from django.contrib.auth.models import User
# BaseJSONWebTokenAuthentication
class JWTAuthentication(BaseJSONWebTokenAuthentication):
# def authenticate_credentials(self, payload):
# username = jwt_get_username_from_payload(payload)
# if not username:
# raise AuthenticationFailed()
# try:
# user = User.objects.get_by_natural_key(username)
# except User.DoesNotExist:
# msg = 'Invalid signature.'
# raise AuthenticationFailed(msg)
#
# if not user.is_active:
# msg ='User account is disabled.'
# raise AuthenticationFailed(msg)
# return user
def authenticate(self, request):
print(request.META)
# token=request.query_params.get('HTTP_AUTHORIZATION',None)
token=request.META.get('HTTP_AUTHORIZATION',None)
if token:
# 校驗token是不是過期了,是不是合法,
try:
payload = jwt_decode_handler(token)
print(payload)
except jwt.ExpiredSignature:
raise AuthenticationFailed('token過期')
except jwt.DecodeError:
raise AuthenticationFailed('token認證失敗')
except jwt.InvalidTokenError:
raise AuthenticationFailed('token不合法')
else:
raise AuthenticationFailed('token沒有攜帶')
'''
# 三種方案得到user
-繼承BaseJSONWebTokenAuthentication,self.authenticate_credentials
-直接把BaseJSONWebTokenAuthentication,authenticate_credentials方法拿出來,放到自己類中
-完全自己寫
'''
user = self.authenticate_credentials(payload)
return (user, token)
3 基於自定義User表,簽發token(5星)
3.2 路由
from rest_framework.routers import DefaultRouter,SimpleRouter
router=SimpleRouter()
router.register('books',views.BookView)
router.register('user',views.UserInfoView,basename='user')
urlpatterns = [
path('', include(router.urls)),
]
3.2 視圖類
from rest_framework.viewsets import ViewSet
from rest_framework.decorators import action
from rest_framework.response import Response
from .models import User
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 UserInfoView(ViewSet):
@action(methods=['POST'],detail=False)
def login(self,request):
username=request.data.get('username')
password=request.data.get('password')
res={'code':'100','msg':'登錄成功'}
user=User.objects.filter(username=username,password=password).first()
if user:
# 登錄成功,生成token,提供了(去找)
payload = jwt_payload_handler(user)
token=jwt_encode_handler(payload)
res['token']=token
else:
res['code']=101
res['msg']='用戶名或密碼錯誤'
return Response(res)
3.3 認證類
from .models import User
class JWTMyUserAuthentication(BaseAuthentication):
def authenticate(self, request):
token=request.META.get('HTTP_AUTHORIZATION',None)
if token:
try:
payload = jwt_decode_handler(token)
print(payload)
except jwt.ExpiredSignature:
raise AuthenticationFailed('token過期')
except jwt.DecodeError:
raise AuthenticationFailed('token認證失敗')
except jwt.InvalidTokenError:
raise AuthenticationFailed('token不合法')
else:
raise AuthenticationFailed('token沒有攜帶')
user=User.objects.get(pk=payload.get('user_id'))
# user=User(id=payload.get('user_id'),username=payload.get('username'))
# 優化,減少數據庫壓力()
# user={'id':payload.get('user_id'),'username':payload.get('username')}
return (user, token)
3.4 Book接口
from .auth import JWTMyUserAuthentication
class BookView(ViewSetMixin,ListAPIView,CreateAPIView):
queryset = Books.objects.all()
serializer_class = BookSerializer
authentication_classes = [JWTMyUserAuthentication,]
def list(self, request, *args, **kwargs):
print(request.user['id'])
return super().list(request, *args, **kwargs)
4 多方式登錄(重點)
1 使用用戶名,郵箱,手機號+密碼都能登錄成功
2 可以使用auth 的user表,也可以自定義用戶表
3 擴寫auth的user表,要么不用,要用一定要在項目開始就使用(沒有遷移之前)
4 如果已經遷移了(正常是不能再使用了),如果還想用,解決方案:
-刪庫
-刪除遷移記錄(app的遷移記錄,auth app的遷移記錄(源碼中),admin app的遷移記錄(源碼中))
4.1 視圖類
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.viewsets import ViewSet
from .serizlizer import UserSerializer
class UserInfoView(ViewSet):
# 登錄接口,要取消所有的認證與權限規則,也就是要做局部禁用操作(空配置)
authentication_classes = []
permission_classes = []
# 需要和mixins結合使用,繼承GenericViewSet,不需要則繼承ViewSet
# 為什么繼承視圖集,不去繼承工具視圖或視圖基類,因為視圖集可以自定義路由映射:
# 可以做到get映射get,get映射list,還可以做到自定義(靈活)
@action(methods=['POST'], detail=False)
def login(self, request):
# 把認證邏輯和簽發token邏輯,放到序列化類中寫
res = {'code': 100, 'msg': '登錄成功', 'token': None}
# ser=UserSerializer(data=request.data,context={'request':request})
ser = UserSerializer(data=request.data)
if ser.is_valid():
# 拿到存放到context中的 token返回
res['token'] = ser.context['token']
else:
res['code'] = 101
res['msg'] = ser.errors
return Response(res)
4.2 序列化類
from .models import User
import re
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 UserSerializer(serializers.ModelSerializer):
# 登錄請求,走的是post方法,默認post方法完成的是create入庫校驗,所以唯一約束的字段,會進行數據庫唯一校驗,導致邏輯相悖
# 需要覆蓋系統字段,自定義校驗規則,就可以避免完成多余的不必要校驗,如唯一字段校驗
username=serializers.CharField() # 一定要重寫username字段
class Meta:
model=User
# 結合前台登錄布局:采用賬號密碼登錄,或手機密碼登錄,布局一致,所以不管賬號還是手機號,都用username字段提交的
fields=['username','password']
def validate(self, attrs):
# 1、在全局鈎子中,才能提供提供的所需數據,整體校驗得到user
user=self._get_user(attrs) # 4、拿到返回值user
# 5、簽發token 調用方法執行
token=self._get_token(user)
# 8、將token存放到context屬性中,傳給外鍵視圖類使用
self.context['token']=token
return attrs
def _get_user(self,attrs): # 2、執行此方法
username = attrs.get('username')# username:可能是手機號,可能是郵箱,可能是用戶名
password = attrs.get('password')
# 使用正則去匹配,手機號,郵箱或者其他
if re.match(r'^1[3-9][0-9]{9}$', username):
user=User.objects.filter(phone=username).first()
elif re.match(r'^.+@.+$', username):
user = User.objects.filter(email=username).first()
else:
user = User.objects.filter(username=username).first()
if user:
if user.check_password(password):
return user # 3、返回user
else:
raise ValidationError('密碼錯誤')
else:
raise ValidationError('用戶不存在')
def _get_token(self,user):
# 6、再就可以調用簽發token算法(drf-jwt框架提供的),將user信息轉換為token
payload = jwt_payload_handler(user)
token=jwt_encode_handler(payload)
return token # 7、返回token