Token 與 JWT認證

爲什麼用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字段裏面。

  1. 首先,前端通過Web表單將自己的用戶名和密碼發送到後端的接口。這一過程一般是一個HTTP POST請求。建議的方式是通過SSL加密的傳輸(https協議),從而避免敏感信息被嗅探。

  2. 後端覈對用戶名和密碼成功後,將用戶的id等其他信息作爲JWT Payload(負載),將其與頭部分別進行Base64編碼拼接後簽名,形成一個JWT。形成的JWT就是一個形同lll.zzz.xxx的字符串。

  3. 後端將JWT字符串作爲登錄成功的返回結果返回給前端。前端可以將返回的結果保存在localStorage或sessionStorage上,退出登錄時前端刪除保存的JWT即可。

  4. 前端在每次請求時將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返回

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章