Django+JWT實現Token認證

  基於Token的鑑權機制愈來愈多的用在了項目中,尤爲是對於純後端只對外提供API沒有web頁面的項目,例如咱們一般所講的先後端分離架構中的純後端服務,只提供API給前端,前端經過API提供的數據對頁面進行渲染展現或增長修改等,咱們知道HTTP是一種無狀態的協議,也就是說後端服務並不知道是誰發來的請求,那麼如何校驗請求的合法性呢?這就須要經過一些方式對請求進行鑑權了前端

  先來看看傳統的登陸鑑權跟基於Token的鑑權有什麼區別python

  以Django的帳號密碼登陸爲例來講明傳統的驗證鑑權方式是怎麼工做的,當咱們登陸頁面輸入帳號密碼提交表單後,會發送請求給服務器,服務器對發送過來的帳號密碼進行驗證鑑權,驗證鑑權經過後,把用戶信息記錄在服務器端(django_session表中),同時返回給瀏覽器一個sessionid用來惟一標識這個用戶,瀏覽器將sessionid保存在cookie中,以後瀏覽器的每次請求都一併將sessionid發送給服務器,服務器根據sessionid與記錄的信息作對比以驗證身份web

  Token的鑑權方式就清晰不少了,客戶端用本身的帳號密碼進行登陸,服務端驗證鑑權,驗證鑑權經過生成Token返回給客戶端,以後客戶端每次請求都將Token放在header裏一併發送,服務端收到請求時校驗Token以肯定訪問者身份算法

  session的主要目的是給無狀態的HTTP協議添加狀態保持,一般在瀏覽器做爲客戶端的狀況下比較通用。而Token的主要目的是爲了鑑權,同時又不須要考慮CSRF防禦以及跨域的問題,因此更多的用在專門給第三方提供API的狀況下,客戶端請求不管是瀏覽器發起仍是其餘的程序發起都能很好的支持。因此目前基於Token的鑑權機制幾乎已經成了先後端分離架構或者對外提供API訪問的鑑權標準,獲得普遍使用django

  JSON Web Token(JWT)是目前Token鑑權機制下最流行的方案,網上關於JWT的介紹有不少,這裏不細說,只講下Django如何利用JWT實現對API的認證鑑權,搜了幾乎全部的文章都是說JWT如何結合DRF使用的,若是你的項目沒有用到DRF框架,也不想僅僅爲了鑑權API就引入龐大複雜的DRF框架,那麼能夠接着往下看json

  個人需求以下:
    1.  同一個view函數既給前端頁面提供數據,又對外提供API服務,要同時知足基於帳號密碼的驗證和JWT驗證
    2.  項目用了Django默認的權限系統,既能對帳號密碼登陸的進行權限校驗,又能對基於JWT的請求進行權限校驗後端

 

PyJWT介紹

  要實現上邊的需求1,咱們首先得引入JWT模塊,python下有現成的PyJWT模塊能夠直接用,先看下JWT的簡單用法api

安裝PyJWT

$ pip install pyjwt

 

利用PyJWT生成Token

>>> import jwt
>>> encoded_jwt = jwt.encode({'username':'運維咖啡吧','site':'https://ops-coffee.cn'},'secret_key',algorithm='HS256')

  這裏傳了三部份內容給JWT:

    第一部分是一個Json對象,稱爲Payload,主要用來存放有效的信息,例如用戶名,過時時間等等全部你想要傳遞的信息跨域

    第二部分是一個祕鑰字串,這個祕鑰主要用在下文Signature簽名中,服務端用來校驗Token合法性,這個祕鑰只有服務端知道,不能泄露瀏覽器

    第三部分指定了Signature簽名的算法

查看生成的Token

>>> print(encoded_jwt)
b'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6Ilx1OGZkMFx1N2VmNFx1NTQ5Nlx1NTU2MVx1NTQyNyIsInNpdGUiOiJodHRwczovL29wcy1jb2ZmZWUuY24ifQ.fIpSXy476r9F9i7GhdYFNkd-2Ndz8uKLgJPcd84BkJ4'

  JWT生成的Token是一個用兩個點(.)分割的長字符串

  點分割成的三部分分別是Header頭部,Payload負載,Signature簽名:Header.Payload.Signature

  JWT是不加密的,任何人均可以讀的到其中的信息,其中第一部分Header和第二部分Payload只是對原始輸入的信息轉成了base64編碼,第三部分Signature是用header+payload+secret_key進行加密的結果

  能夠直接用base64對Header和Payload進行解碼獲得相應的信息

>>> import base64
>>> base64.b64decode('eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9')
b'{"typ":"JWT","alg":"HS256"}'

>>> base64.b64decode('eyJ1c2VybmFtZSI6Ilx1OGZkMFx1N2VmNFx1NTQ5Nlx1NTU2MVx1NTQyNyIsInNpdGUiOiJodHRwczovL29wcy1jb2ZmZWUuY24ifQ==')

 


# 這裏最後加=的緣由是base64解碼對傳入的參數長度不是2的對象,須要再參數最後加上一個或兩個等號=

  由於JWT不會對結果進行加密,因此不要保存敏感信息在Header或者Payload中,服務端也主要依靠最後的Signature來驗證Token是否有效以及有無被篡改

解密Token

>>> jwt.decode(encoded_jwt,'secret_key',algorithms=['HS256'])
{'username': 'qiqi', 'site': 'https://ops-coffee.cn'}

 

 

服務端在有祕鑰的狀況下能夠直接對JWT生成的Token進行解密,解密成功說明Token正確,且數據沒有被篡改

固然咱們前文說了JWT並無對數據進行加密,若是沒有secret_key也能夠直接獲取到Payload裏邊的數據,只是缺乏了簽名算法沒法驗證數據是否準確,pyjwt也提供了直接獲取Payload數據的方法,以下

>>> jwt.decode(encoded_jwt, verify=False)
{'username': 'qiqi', 'site': 'https://ops-coffee.cn'}

 

Django案例

Django要兼容session認證的方式,還須要同時支持JWT,而且兩種驗證須要共用同一套權限系統,該如何處理呢?咱們能夠參考Django的解決方案:裝飾器,例如用來檢查用戶是否登陸的login_required和用來檢查用戶是否有權限的permission_required兩個裝飾器,咱們能夠本身實現一個裝飾器,檢查用戶的認證模式,同時認證完成後驗證用戶是否有權限操做

因而一個auth_permission_required的裝飾器產生了:

from django.conf import settings
from django.http import JsonResponse
from django.contrib.auth import get_user_model
from django.core.exceptions import PermissionDenied

UserModel = get_user_model()


def auth_permission_required(perm):
    def decorator(view_func):
        def _wrapped_view(request, *args, **kwargs):
            # 格式化權限
            perms = (perm,) if isinstance(perm, str) else perm

            if request.user.is_authenticated:
                # 正常登陸用戶判斷是否有權限
                if not request.user.has_perms(perms):
                    raise PermissionDenied
            else:
                try:
                    auth = request.META.get('HTTP_AUTHORIZATION').split()
                except AttributeError:
                    return JsonResponse({"code": 401, "message": "No authenticate header"})

                # 用戶經過API獲取數據驗證流程
                if auth[0].lower() == 'token':
                    try:
                        dict = jwt.decode(auth[1], settings.SECRET_KEY, algorithms=['HS256'])
                        username = dict.get('data').get('username')
                    except jwt.ExpiredSignatureError:
                        return JsonResponse({"status_code": 401, "message": "Token expired"})
                    except jwt.InvalidTokenError:
                        return JsonResponse({"status_code": 401, "message": "Invalid token"})
                    except Exception as e:
                        return JsonResponse({"status_code": 401, "message": "Can not get user object"})

                    try:
                        user = UserModel.objects.get(username=username)
                    except UserModel.DoesNotExist:
                        return JsonResponse({"status_code": 401, "message": "User Does not exist"})

                    if not user.is_active:
                        return JsonResponse({"status_code": 401, "message": "User inactive or deleted"})

                    # Token登陸的用戶判斷是否有權限
                    if not user.has_perms(perms):
                        return JsonResponse({"status_code": 403, "message": "PermissionDenied"})
                else:
                    return JsonResponse({"status_code": 401, "message": "Not support auth type"})

            return view_func(request, *args, **kwargs)

        return _wrapped_view

    return decorator

在view使用時就能夠用這個裝飾器來代替本來的login_requiredpermission_required裝飾器了

@auth_permission_required('account.select_user')
def user(request):
    if request.method == 'GET':
        _jsondata = {
            "user": "ops-coffee",
            "site": "https://ops-coffee.cn"
        }

        return JsonResponse({"state": 1, "message": _jsondata})
    else:
        return JsonResponse({"state": 0, "message": "Request method 'POST' not supported"})

咱們還須要一個生成用戶Token的方法,經過給User model添加一個token的靜態方法來處理

class User(AbstractBaseUser, PermissionsMixin):
    create_time = models.DateTimeField(auto_now_add=True, verbose_name='建立時間')
    update_time = models.DateTimeField(auto_now=True, verbose_name='更新時間')
    username = models.EmailField(max_length=255, unique=True, verbose_name='用戶名')
    fullname = models.CharField(max_length=64, null=True, verbose_name='中文名')
    phonenumber = models.CharField(max_length=16, null=True, unique=True, verbose_name='電話')
    is_active = models.BooleanField(default=True, verbose_name='激活狀態')

    objects = UserManager()

    USERNAME_FIELD = 'username'
    REQUIRED_FIELDS = []

    def __str__(self):
        return self.username

    @property
    def token(self):
        return self._generate_jwt_token()

    def _generate_jwt_token(self):
        token = jwt.encode({
            'exp': datetime.utcnow() + timedelta(days=1),
            'iat': datetime.utcnow(),
            'data': {
                'username': self.username
            }
        }, settings.SECRET_KEY, algorithm='HS256')

        return token.decode('utf-8')

    class Meta:
        default_permissions = ()

        permissions = (
            ("select_user", "查看用戶"),
            ("change_user", "修改用戶"),
            ("delete_user", "刪除用戶"),
        )

能夠直接經過用戶對象來生成Token:

>>> from accounts.models import User
>>> u = User.objects.get(username='admin@ops-coffee.cn')
>>> u.token
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1NDgyMjg3NzksImlhdCI6MTU0ODE0MjM3OSwiZGF0YSI6eyJ1c2VybmFtZSI6ImFkbWluQDE2My5jb20ifX0.akZNU7t_z2kwPxDJjmc-QxtNdICK0yhnwWmKxqqXKLw'

生成的Token給到客戶端,客戶端就能夠拿這個Token進行鑑權了

>>> import requests
>>> token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1NDgyMjg4MzgsImlhdCI6MTU0ODE0MjQzOCwiZGF0YSI6eyJ1c2VybmFtZSI6ImFkbWluQDE2My5jb20ifX0.oKc0SafgksMT9ZIhTACupUlz49Q5kI4oJA-B8-GHqLA'
>>>
>>> r = requests.get('http://localhost/api/user', headers={'Authorization': 'Token '+token})
>>> r.json()
{'username': 'admin@ops-coffee.cn', 'fullname': '七七', 'is_active': True}

這樣一個auth_permission_required方法就能夠搞定上邊的所有需求了,簡單好用。

相關文章
相關標籤/搜索