爲什麼用Token
先了解身份驗證的演變:
隨着交互式Web應用的興起,像在線購物網站,需要登錄的網站等等,遇到的一個問題就是身份驗證會話管理,必須記住登錄的用戶,也就是必須吧每個用戶區分開,因爲HTTP請求是無狀態的,所以解決辦法就是給每個人發送一個回話標識(sessionid)隨機字符串,每次大家向我發起HTTP請求的時候,把這個字符串給一併捎過來, 這樣我就能區分開誰是誰了.
這樣就產生以下問題:
- 1.客戶端在自己的cookie中存儲自己的sessionid,但服務端卻要存儲所有登錄用戶的sessionid,給服務器造成了巨大的壓力
- 2.CORS(跨域資源共享):當我們需要讓數據跨多臺移動設備上使用時,跨域資源的共享會是一個讓人頭疼的問題。在使用Ajax抓取另一個域的資源,就可以會出現禁止請求的情況。
- 3.CSRF(跨站請求僞造):用戶在訪問銀行網站時,他們很容易受到跨站請求僞造的攻擊,並且能夠被利用其訪問其他的網站。
- 4.服務器集羣部署問題: 當用戶在A服務器登錄,A服務器通過session記錄了客戶,但是當用戶再次訪問服務端,而這次的請求被分配到了B服務器,但B服務器沒有session記錄,所以 出現問題
基於這些問題,我們有必要去尋求一種更有行之有效的方法。
Token介紹
比如韓梅梅同學登錄了網站,服務端給她發送了令牌(token),裏面包含了user_id,下一次韓梅梅同學再次通過HTTP請求服務端的時候,只需要把token放在HTTP的請求頭中一併帶過就可以了,當然這樣和session沒什麼區別,所以 人們想出了一個辦法,簽名,
簽名就是利用SHA256算法,再加上一個只有服務端知道的祕鑰,對數據做一個簽名,把這個簽名和數據一起作爲token發個客戶端,由於祕鑰只有服務端知道,所以token不會被僞造
當韓梅梅再次通過HTTP請求服務端的時候,服務端在請求頭中拿到token,通過解密算法拿到祕鑰,與服務端的沒藥進行對比,若一致,則證明登錄過了,並且可以直接取到韓梅梅的user_id.
注意: Token中的數據是明文保存的,所以不能再其中保存敏感信息
這樣一來,服務端就不需要保存session了,只生產token,有一套token解密代碼就可以了
在Web領域基於Token的身份驗證隨處可見。在大多數使用Web API的互聯網公司中,tokens 是多用戶下處理認證的最佳方式
Token身份驗證的流程:
- 1.用戶通過用戶名和密碼發送請求。
- 2.程序驗證。
- 3.程序返回一個簽名的token 給客戶端。
- 4.客戶端儲存token,並且每次用於每次發送請求。
- 5.服務端驗證token並返回數據。
優點
1.服務端不再需要存儲session,token交給每一個客戶端自己存儲,解決存儲壓力問題
2.請求中發送token而不再是發送cookie能夠防止CSRF(跨站請求僞造)
3.更安全,祕鑰只有服務端知道
4.解決集羣身份驗證問題,服務端不需要存儲session,只需要祕鑰和簽發與校驗token兩端代碼即可
注意
1.token一定在服務器產生,且在服務器校驗
2.token一定參與網絡傳輸
3.token攜帶的信息存在 能被反解 與 不能被反解 的多部分組成
JWT認證
jwt token採用三段式:頭部.載荷.簽名
每一部分都是一個json字典加密形參的字符串
頭部和載荷採用的是base64可逆加密(前臺後臺都可以解密)
簽名採用hash256不可逆加密(後臺校驗採用碰撞校驗)
各部分字典的內容:
- 頭部:基礎信息 - 公司信息、項目組信息、可逆加密採用的算法
- 載荷:有用但非私密的信息 - 用戶可公開信息、過期時間
- 簽名:頭部+載荷+祕鑰 不可逆加密後的結果
注:服務器jwt簽名加密祕鑰一定不能泄露
簽發token:固定的頭部信息加密.當前的登陸用戶與過期時間加密.頭部+載荷+祕鑰生成不可逆加密
校驗token:頭部可校驗也可以不校驗,載荷校驗出用戶與過期時間,頭部+載荷+祕鑰完成碰撞檢測校驗token是否被篡改
客戶端收到服務器返回的 JWT,可以儲存在 Cookie 裏面,也可以儲存在 localStorage。
此後,客戶端每次與服務器通信,都要帶上這個 JWT。你可以把它放在 Cookie 裏面自動發送,但是這樣不能跨域,所以更好的做法是放在 HTTP 請求的頭信息HTTP_AUTHORIZATION
字段裏面。
-
首先,前端通過Web表單將自己的用戶名和密碼發送到後端的接口。這一過程一般是一個HTTP POST請求。建議的方式是通過SSL加密的傳輸(https協議),從而避免敏感信息被嗅探。
-
後端覈對用戶名和密碼成功後,將用戶的id等其他信息作爲JWT Payload(負載),將其與頭部分別進行Base64編碼拼接後簽名,形成一個JWT。形成的JWT就是一個形同lll.zzz.xxx的字符串。
-
後端將JWT字符串作爲登錄成功的返回結果返回給前端。前端可以將返回的結果保存在localStorage或sessionStorage上,退出登錄時前端刪除保存的JWT即可。
-
前端在每次請求時將JWT放入HTTP Header中的
HTTP_AUTHORIZATION
位。(解決XSS和XSRF問題)
5.後端檢查是否存在,如存在驗證JWT的有效性。例如,檢查簽名是否正確;檢查Token是否過期;檢查Token的接收方是否是自己(可選)。
REST framework 中使用 JWT認證
使用django-rest-framework-jwt這個庫來幫助我們簡單的使用jwt進行身份驗證
並解決一些前後端分離而產生的跨域問題
安裝
pip3 install djangorestframework-jwt
使用:
簽發token
- urls.py
登錄接口 簽發token
# ObtainJSONWebToken是JWT自帶的認證類,就是通過username和password得到user對象然後簽發token
from rest_framework_jwt.views import ObtainJSONWebToken, obtain_jwt_token
urlpatterns = [
# url(r'^jogin/$', ObtainJSONWebToken.as_view()),
url(r'^jogin/$', obtain_jwt_token), # 登錄接口url
]
校驗token
全局或局部配置drf-jwt的認證類 JSONWebTokenAuthentication
- settings.py
全局配置
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated', # 權限組件 過濾掉遊客
),
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
),
- views.py
局部配置
from rest_framework.views import APIView
from utils.response import APIResponse
from rest_framework.permissions import IsAuthenticated
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
class UserDetail(APIView):
authentication_classes = [JSONWebTokenAuthentication] # jwt-token校驗request.user
permission_classes = [IsAuthenticated] # 結合權限組件篩選掉遊客
def get(self, request, *args, **kwargs):
return APIResponse(results={'username': request.user.username})
# 路由
url(r'^user/detail/$', views.UserDetail.as_view()),
簽發token
- 源碼入口
前提:給一個局部禁用了所有 認證與權限 的視圖類發送用戶信息得到token,其實就是登錄接口
1)rest_framework_jwt.views.ObtainJSONWebToken 的 父類 JSONWebTokenAPIView 的 post 方法
接受有username、password的post請求
2)post方法將請求數據交給 rest_framework_jwt.serializer.JSONWebTokenSerializer 處理
完成數據的校驗,會走序列化類的 全局鉤子校驗規則,校驗得到登錄用戶並簽發token存儲在序列化對象中
- 核心源碼
rest_framework_jwt.serializer.JSONWebTokenSerializer的validate(self, attrs)方法
def validate(self, attrs):
# 賬號密碼字典
credentials = {
self.username_field: attrs.get(self.username_field),
'password': attrs.get('password')
}
if all(credentials.values()):
# 簽發token第1步:用賬號密碼得到user對象
user = authenticate(**credentials)
if user:
if not user.is_active:
msg = _('User account is disabled.')
raise serializers.ValidationError(msg)
# 簽發token第2步:通過user得到payload,payload包含着用戶信息與過期時間
payload = jwt_payload_handler(user)
# 在視圖類中,可以通過 序列化對象.object.get('user'或者'token') 拿到user和token
return {
# 簽發token第3步:通過payload簽發出token
'token': jwt_encode_handler(payload),
'user': user
}
else:
msg = _('Unable to log in with provided credentials.')
raise serializers.ValidationError(msg)
else:
msg = _('Must include "{username_field}" and "password".')
msg = msg.format(username_field=self.username_field)
raise serializers.ValidationError(msg)
手動簽發token邏輯
1)通過username、password得到user對象
2)通過user對象生成payload:jwt_payload_handler(user) => payload
from rest_framework_jwt.serializers import jwt_payload_handler
3)通過payload簽發token:jwt_encode_handler(payload) => token
from rest_framework_jwt.serializers import jwt_encode_handler
校驗token
-
源碼入口
前提:訪問一個配置了jwt認證規則的視圖類,就需要提交認證字符串token,在認證類中完成token的校驗
rest_framework_jwt.authentication.JSONWebTokenAuthentication 的 父類 BaseJSONWebTokenAuthentication 的 authenticate 方法
請求頭拿認證信息jwt-token => 通過反爬小規則確定有用的token => payload => user
- 核心源碼
rest_framework_jwt.authentication.BaseJSONWebTokenAuthentication的authenticate(self, request)方法
def authenticate(self, request):
# 帶有反爬小規則的獲取token:前臺必須按 "jwt token字符串" 方式提交
# 校驗user第1步:從請求頭 HTTP_AUTHORIZATION 中拿token,並提取
jwt_value = self.get_jwt_value(request)
# 遊客
if jwt_value is None:
return None
# 校驗
try:
# 校驗user第2步:token => payload
payload = jwt_decode_handler(jwt_value)
except jwt.ExpiredSignature:
msg = _('Signature has expired.')
raise exceptions.AuthenticationFailed(msg)
except jwt.DecodeError:
msg = _('Error decoding signature.')
raise exceptions.AuthenticationFailed(msg)
except jwt.InvalidTokenError:
raise exceptions.AuthenticationFailed()
# 校驗user第3步:token => payload
user = self.authenticate_credentials(payload)
return (user, jwt_value)
手動校驗token邏輯
1)從請求頭中獲取token
2)根據token解析出payload:jwt_decode_handler(token) => payloay
from rest_framework_jwt.authentication import jwt_decode_handler
3)根據payload解析出user:self.authenticate_credentials(payload) => user
繼承drf-jwt的BaseJSONWebTokenAuthentication,拿到父級的authenticate_credentials方法
自定義drf-jwt配置
- settings.py
# 自定義 drf-jwt 配置
import datetime
JWT_AUTH = {
# user => payload
'JWT_PAYLOAD_HANDLER':
'rest_framework_jwt.utils.jwt_payload_handler',
# payload => token
'JWT_ENCODE_HANDLER':
'rest_framework_jwt.utils.jwt_encode_handler',
# token => payload
'JWT_DECODE_HANDLER':
'rest_framework_jwt.utils.jwt_decode_handler',
# token過期時間
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7),
# token刷新的過期時間
'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=7),
# 反爬小措施前綴
'JWT_AUTH_HEADER_PREFIX': 'JWT', # 默認token前要加上JWT,可以修改成其他,也可以手動創建校驗規則
}
案例:實現多方式登陸簽發token
-
models.py
from django.db import models
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
mobile = models.CharField(max_length=11, unique=True)
class Meta:
db_table = 'api_user'
verbose_name = '用戶表'
verbose_name_plural = verbose_name
def __str__(self):
return self.username
-
serializers.py
1) 前臺提交多種登錄信息都採用一個key,所以後臺可以自定義反序列化字段進行對應
2) 序列化類要處理序列化與反序列化,要在fields中設置model綁定的Model類所有使用到的字段
3) 區分序列化字段與反序列化字段 read_only | write_only
4) 在自定義校驗規則中(局部鉤子、全局鉤子)校驗數據是否合法、確定登錄的用戶、根據用戶簽發token
5) 將登錄的用戶與簽發的token保存在序列化類對象中 返回給前端
from rest_framework import serializers
from . import models
import re
from rest_framework_jwt.serializers import jwt_payload_handler
from rest_framework_jwt.serializers import jwt_encode_handler
class UserModelSerializer(serializers.ModelSerializer):
# 自定義反序列字段:
# 1.一定要設置write_only,只參與反序列化,不會與model類字段映射
# 2.要在fields中註冊自定義反序列化字段
# 3.自定義反序列化字段不要與model類表中的字段一樣
usr = serializers.CharField(write_only=True)
pwd = serializers.CharField(write_only=True)
class Meta:
model = models.User
fields = ['usr', 'pwd', 'username', 'mobile', 'email']
# 系統校驗規則
extra_kwargs = {
'username': {'read_only': True},
'mobile': { 'read_only': True},
'email': {'read_only': True},
}
def validate(self, attrs):
usr = attrs.get('usr')
pwd = attrs.get('pwd')
# 多方式登錄:各分支處理得到該方式下對應的用戶
if re.match(r'.+@.+', usr):
user_query = models.User.objects.filter(email=usr)
elif re.match(r'1[3-9][0-9]{9}', usr):
user_query = models.User.objects.filter(mobile=usr)
else:
user_query = models.User.objects.filter(username=usr)
user_obj = user_query.first()
# 根據user_obj校驗密碼是否正確
if user_obj and user_obj.check_password(pwd):
# 簽發token:得到登錄用戶,簽發token並存儲在實例化對象中
payload = jwt_payload_handler(user_obj)
token = jwt_encode_handler(payload)
# 將當前用戶與簽發的token都保存在序列化對象中
self.user = user_obj
self.token = token
return attrs
raise serializers.ValidationError({'data':'數據有誤'})
-
views.py
實現多方式登陸簽發token:賬號、手機號、郵箱等登陸
1) 禁用認證與權限組件
2) 拿到前臺登錄信息,交給序列化類
3) 序列化類校驗得到登錄用戶與token存放在序列化對象中
4) 取出登錄用戶與token返回給前臺
from . import serializers, models
from utils.response import APIResponse
class LoginAPIView(APIView):
# 1) 禁用認證與權限組件
authentication_classes = []
permission_classes = []
def post(self, request, *args, **kwargs):
# 2) 拿到前臺登錄信息,交給序列化類,規則:賬號用usr傳,密碼用pwd傳
user_ser = serializers.UserModelSerializer(data=request.data)
# 3) 序列化類校驗得到登錄用戶與token,存放在序列化對象中
user_ser.is_valid(raise_exception=True)
# 4) 取出登錄用戶與token返回給前臺
return APIResponse(token=user_ser.token, results=serializers.UserModelSerializer(user_ser.user).data)
總結:
自定義簽發token本質就是獲取前端post請求發來的登錄條件(username,email,mobile...)和密碼
自定義反序列類,反序列化登錄條件和密碼,校驗通過後調用jwt_payload_handler,jwt_encode_handler完成簽發
案例:自定義認證反爬規則的認證類
-
authentications.py
import jwt
from rest_framework_jwt.authentication import BaseJSONWebTokenAuthentication
from rest_framework_jwt.authentication import jwt_decode_handler
from rest_framework.exceptions import AuthenticationFailed
# 自定義token校驗規則
# 1.繼承BaseJSONWebTokenAuthentication
class JWTAuthentication(BaseJSONWebTokenAuthentication):
# 2.重寫authentication方法
def authenticate(self, request):
# 3.獲取token
jwt_token = request.META.get('HTTP_AUTHORIZATION')
# 4.自定義校驗規則 如: auth token jwt
token = self.parse_jwt_token(jwt_token)
if token is None:
return None # 遊客
# 5.根據token拿到payload | token => payload
try:
payload = jwt_decode_handler(token)
except jwt.ExpiredSignature:
raise AuthenticationFailed('token已過期')
except:
raise AuthenticationFailed('非法用戶')
# 6.根據payload拿到user | payload => user
user = self.authenticate_credentials(payload)
return user
# 規則方法
def parse_jwt_token(self, jwt_token):
# 按空格切分
tokens = jwt_token.split()
# 校驗規則
if len(tokens) != 3 or tokens[0].lower() != 'auth' or tokens[2].lower() != 'jwt':
return None # 遊客
return tokens[1]
-
views.py
from rest_framework.views import APIView
from utils.response import APIResponse
# 必須登錄後才能訪問 - 通過了認證權限組件
from rest_framework.permissions import IsAuthenticated
# 自定義jwt校驗規則
from .authentications import JWTAuthentication
class UserDetail(APIView):
# 局部配置認證組件,這裏的認證組件是自定義的
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
def get(self, request, *args, **kwargs):
return APIResponse(results={'username': request.user.username})
總結:
自定義token校驗規則
自定義校驗token本質是對token加的鹽進行校驗
1 在視圖類中配置自定義的token校驗器
2 自定義token校驗器,重寫authenticate方法
3 從請求頭中拿到token,按規則校驗
4 校驗成功在按步驟: token => payload => user 將user返回