Django+React全棧開發:自定義驗證與受權

前言

以前看到有很多人提問有關Django定製User的問題,正好教程準備講到RESTOAuth,那麼在這裏就先說一下有關REST framework,這裏就講一下有關定製User模型以及REST framework驗證受權相關的問題,不過在後續教程的實際應用中仍是採用第三方登陸的方式作驗證受權,事實上在個人博客上已經不打算作用戶功能了(感受仍是在QQ羣交流比較好吧)。對這部分不感興趣的能夠直接跳過了。前端

自定義User

Django原生的User模型已經足夠知足通常小網站的需求,可是有時候不可避免要對用戶模型作一些定製,官方文檔給出了四種方法python

  • proxy model
  • OneToOneField
  • 繼承AbstractUser
  • 繼承AbstractBaseUser

前兩個方法適用於只要擴展用戶信息或增長一些處理方法而和身份驗證無關,然後二者則適用於對於身份驗證有定製需求。sql

繼承AbstractUser

Django官方文檔對於如何定製用戶模型有着詳盡的解釋,這裏僅僅講講我在某次實踐中是如何使用的。shell

首先咱們能夠新建一個Django app,咱們能夠把驗證受權相關的功能都放在這裏,假定命名爲core。假如咱們須要多種分級的等級標識,而不只僅是原生User模型的is_staff字段指示用戶是不是管理員,例如須要三重等級,能夠像下面這樣編寫代碼:數據庫

# core/models.py
class User(AbstractUser):
    Level_Set = (
        (0, 'Super User'),
        (1, 'Normal User'),
        (2, 'Internship'),
    )
    level = models.IntegerField(choices=Level_Set, default=2)

    class Meta:
        ordering = ('date_joined',)

以後須要在項目的settings.py文件中加入:django

# 字符串內容是「app名.模型名」
AUTH_USER_MODEL = 'core.User'

能夠在core/admin.py中註冊咱們的定製模型:後端

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import User

admin.site.register(User, UserAdmin)

有一點須要注意,Django的模型須要遷移操做,對於定製的User,最好在項目剛剛開始的時候,在你尚未執行第一次python manage.py migrate的時候完成上述操做,固然若是還在開發階段,即便以前執行過遷移操做,也能夠經過刪除項目中全部migrations文件夾以及sqlite文件來初始化。api

此後,若是你的模型中有須要自定義用戶模型作外鍵的需求,例如文章與文章做者,能夠參考以下設置:瀏覽器

from django.conf import settings
from django.db import models

class Article(models.Model):
    author = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
    )

任何須要使用到咱們自定義用戶模型的地方均可以這樣操做。安全

序列化

能夠參考以下代碼:

# core/serializers.py
from rest_framework import serializers
from core.models import User


class UserSerializer(serializers.HyperlinkedModelSerializer):
    password = serializers.CharField(style={'input_type': 'password'}, label='密碼', write_only=True)

    class Meta:
        model = User
        fields = ['url', 'id', 'username', 'password', 'email', 'level', 'is_active', 'date_joined']

這裏咱們設置password字段時加入了write_only=True這個參數,這樣咱們的view視圖將只會在處理POSTPUTPATCH請求時(若是你容許這些請求的話)寫入密碼而不會在返回用戶列表或詳情信息時顯示密碼

接下來能夠寫個簡單的視圖試試:

# 別忘了引入咱們自定義的模型與序列化器
class UserViewSet(viewsets.ModelViewSet):
    queryset = User.objects.get_queryset()
    serializer_class = UserSerializer

還記得怎麼在urls.py經過router註冊視圖嗎?可是若是你使用了多個app,那麼在不一樣app中註冊會產生衝突,一個解決辦法是後端只使用一個app而不是不一樣功能拆分到不一樣app,或者能夠作以下嘗試:

# app1的urls.py
from . import views

routeList = (
    (r'users', views.UserViewSet),
)

# app2的urls.py
from . import views


routeList = (
    (r'articles', views.ArticleViewSet),
    ......
)

# 項目級urls.py
from app1.urls import routeList as app1Urls
from container.urls import routeList as app2Urls

routeList = app1Urls + app2Urls

router = DefaultRouter()

for route in routeList:
    router.register(route[0], route[1])

urlpatterns = [
    path('', include(router.urls)),
    path('api-auth/', include('rest_framework.urls'))
    .......
]

如今嘗試使用POST請求建立一個新用戶吧,最簡單的方法是直接用瀏覽器打開訪問127.0.0.1:8000/users/。接着使用新建的帳戶密碼驗證登陸,你會發現驗證失敗。

爲了安全起見,咱們設置的密碼會通過加密處理再放入數據庫,一樣,驗證用戶密碼時,也會對密碼加密再比對密文,這樣即便是擁有查看數據庫權限的人也沒法查看用戶密碼的明文。可是這裏咱們的視圖沒有對密碼進行加密就被存入了數據庫,而用戶驗證時倒是用的Django自身的API,比對的是密文,也就是驗證時你提交的密碼被加密,而數據庫中的密碼卻沒有加密,這樣就出現了沒法匹配的現象。

能夠經過覆寫ViewSetcreate方法來修復這個bug:

from django.contrib.auth.hashers import make_password
......


class UserViewSet(viewsets.ModelViewSet):
    ......
    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        serializer.validated_data['password'] = make_password(serializer.validated_data['password'])
        self.perform_create(serializer)
        headers = self.get_success_headers(serializer.data)
        return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

這裏調用Django提供的make_password函數來生成正確的加密的密碼。

既然是編寫REST風格的API,那麼建議對於用戶的增長、修改、刪除都使用這個視圖。對於用戶改密碼的需求,能夠在序列化器中添加一個old_password字段,並設置爲當前密碼,同時要改寫視圖類的partial_update方法。如下是一個我用來實現超管直接修改全部用戶密碼的需求(不要問我爲何會有這種需求~)的方式:

class UserViewSet(viewsets.ModelViewSet):
    ......
    def partial_update(self, request, *args, **kwargs):
        if 'password' in request.data:
            request.data['password'] = make_password(request.data['password'])
        kwargs['partial'] = True
        return self.update(request, *args, **kwargs)

經過設置partial參數爲True並將內容傳遞給update來實現僅針對密碼部分更新。

自定義Token驗證

常規狀況下咱們經過用戶的用戶名與密碼來識別用戶身份,最基礎的方法是每次請求都須要用戶名及密碼,可是這極有可能暴露敏感信息,通常不採用。比較常見的方式是基於OAuthSession以及Token的驗證方式。REST framework爲咱們提供了可用的TokenAPI,這裏介紹一下在此基礎上作一些擴展。固然通常狀況下,其實有着開箱可用的第三方庫,如django-rest-knox,可是在學習時咱們能夠重複造點輪子來加深理解。

Token類

簡單的說,基於Token的驗證就是客戶端發送用戶密碼,服務端建立一個與用戶相對應的隨機字符串,以後客戶端每次請求時在請求頭中加上這段字符串,便可經過驗證。

爲了使用REST framework提供的Token咱們須要在settings.py中註冊:

INSTALLED_APPS = [
    ...
    'rest_framework.authtoken'
]

若是你已經建立過用戶,可使用命令python manage.py shell,按以下操做:

>>> from core.models import User
>>> from rest_framework.authtoken.models import Token

>>> for user in User.objects.all():
>>>     Token.objects.get_or_create(user=user)

同時修改core/models.py,經過Django的信號機制,在每次新建用戶時爲其建立Token:

......

@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_auth_token(sender, instance=None, created=False, **kwargs):
    # 接收用戶建立信號,每次新建用戶後自動建立token
    if created:
        Token.objects.create(user=instance)

接下來修改你須要添加權限的視圖:

from rest_framework.authentication import TokenAuthentication
from rest_framework.permissions import IsAuthenticated

......

class ArticleViewSet(viewsets.ModelViewSet):
    authentication_classes = [TokenAuthentication]
    permission_classes = [permissions.IsAuthenticated]
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer

經過authentication_classes指定要使用的驗證類,有關permission_classes的內容下節在說。如今咱們設置一下項目的urls.py

from rest_framework.authtoken import views


urlpatterns = [
    ......
    path('api-token-auth/', views.obtain_auth_token),
]

如今向該接口發送POST請求提交用戶密碼,將會獲得Token,僅在將該Token放在請求頭headers中,纔可獲得articles的正確響應,使用命令行工具httpie調試的示例以下:

$ http POST http://127.0.0.1:8000/api-token-auth/ username="user" password="password"                           
HTTP/1.1 200 OK
......

{
    "token": "bed522b6f41b962b5c829598e990b9f058518c9d"
}

$ http http://127.0.0.1:8000/articles/ 'Authorization: Token bed522b6f41b962b5c829598e990b9f058518c9d'

你能夠嘗試一下不帶Authorization這一串會獲得什麼響應。

Token過時

可是REST framework自帶的Token有着不小的缺陷,最典型的一點是這個Token沒有過時機制,這意味着若是有誰截獲了你的Token,就能夠無限制的使用,安全風險實在太大。下面咱們來試試擴展一下原生的Token驗證,新建core/authentication.py

import datetime
from django.conf import settings
from django.core.cache import cache
from rest_framework.authentication import TokenAuthentication
from rest_framework import exceptions
from django.utils.translation import ugettext_lazy as _

# 記得要在settings.py中設置REST_FRAMEWORK_TOKEN_EXPIRE_MINUTES變量
# 這是爲了方便之後調節過時時間,例如給該變量賦值爲60,則爲一小時過時
EXPIRE_MINUTES = getattr(settings, 'REST_FRAMEWORK_TOKEN_EXPIRE_MINUTES', 1)


class ExpiringTokenAuthentication(TokenAuthentication):
    """
    Setup token expired time
    """
    def authenticate_credentials(self, key):
        model = self.get_model()
        # 利用Django的cache減小數據庫操做
        cache_user = cache.get(key)
        if cache_user:
            return cache_user, key

        try:
            token = model.objects.select_related('user').get(key=key)
        except model.DoesNotExist:
            raise exceptions.AuthenticationFailed(_("無效令牌"))

        if not token.user.is_active:
            raise exceptions.AuthenticationFailed(_("用戶被禁用"))

        time_now = datetime.datetime.now()

        if token.created < time_now - datetime.timedelta(minutes=EXPIRE_MINUTES):
            token.delete()
            raise exceptions.AuthenticationFailed(_("認證信息已過時"))

        if token:
            # EXPIRE_MINUTES * 60 because the param is seconds
            cache.set(key, token.user, EXPIRE_MINUTES * 60)

        return token.user, token

同時咱們能夠修改core/views.py,定製驗證視圖,若是當前Token沒有過時則返回cache中的Token,不然建立新Token:

from rest_framework.authtoken.views import ObtainAuthToken

......

class ObtainExpiringAuthToken(ObtainAuthToken):
    # 別忘了from rest_framework.authentication import BasicAuthentication
    # 這是經過post用戶名密碼獲取token的視圖,可不能採起token驗證哦
    authentication_classes = [BasicAuthentication]

    def post(self, request, *args, **kwargs):
        serializer = self.serializer_class(data=request.data)
        if serializer.is_valid():
            user = serializer.validated_data['user']
            token, created = Token.objects.get_or_create(user=user)
            time_now = datetime.datetime.now()

            if created or (token.created < time_now - datetime.timedelta(minutes=EXPIRE_MINUTES)):
                token.delete()
                token = Token.objects.create(user=user)
                token.created = time_now
                token.save()
            # 這裏能夠定製返回信息
            context = {
                'id': user.id,
                'username': user.username,
                'token': token.key
            }

            return Response(context)
        else:
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

這樣咱們要修改urls.py以啓用咱們新的驗證視圖:

from core.views import ObtainExpiringAuthToken


urlpatterns = [
    ......
    path('api-token-auth/', ObtainExpiringAuthToken.as_view()),
]

如今你能夠修改settings.py中的REST_FRAMEWORK_TOKEN_EXPIRE_MINUTES變量爲1來看看Token過時的效果。

定製permission

既然有了驗證,也就是對用戶的身份進行識別是管理員、普通用戶,仍是未登陸用戶,那麼確定要針對不一樣類型的用戶給予不一樣權限,不然整個驗證過程就失去了意義。事實上咱們以前在articlesAPI中已經使用了REST framework提供的IsAuthenticated權限,指定只有通過登陸驗證的用戶能夠訪問。如今讓咱們設置一個基於用戶級別的權限吧,新建core/permissions.py

from rest_framework import permissions


class AdministratorLevel(permissions.BasePermission):
    # 客戶端向服務端發送請求後,此方法被調用,根據返回的布爾值決定用戶是否擁有權限
    def has_permission(self, request, view):
        if request.user.is_authenticated:
            if request.method in permissions.SAFE_METHODS:
                return True
            # 普通管理員可修改數據
            elif request.method.upper() in ('POST', 'PUT', 'PATCH') and request.user.level == 1:
                return True
            # 超級管理員擁有全部權限
            elif request.user.level == 0:
                return True
            else:
                return False
        return False

如今能夠修改articles API的視圖,用咱們自定義的權限類替換掉以前的IsAuthenticated,而且新建多個不一樣等級的用戶,試試它們的權限吧。

Throttling

顧名思義,throttling起到節流做用,它和permissions有些相似,但能夠用來限制客戶端的請求頻率。

例如,咱們想要用戶的一個Token在一小時內過時,但只要用戶保持活躍,那麼在較長的一段時間內沒必要重複登陸。能夠添加一個經過舊Token獲取新Token的接口,由前端判斷若是用戶在活躍狀態下,那麼能夠在用戶不知道的狀況下獲取新的Token。

# core/views.py
from rest_framework.views import APIView

......

class TokenForToken(APIView):
    authentication_classes = [ExpiringTokenAuthentication]
    permission_classes = [permissions.IsAuthenticated]

    def get(self, request, format=None):
        user = request.user
        # 這裏有個小bug,留給讀者去思考了
        token, created = Token.objects.get_or_create(user=user)
        time_now = datetime.datetime.now()
        token.delete()
        token = Token.objects.create(user=user)
        token.created = time_now
        token.save()
        return Response({'token': token.key}

urls.py中註冊此視圖,咱們就能夠用舊的Token來替換新的Token,可是若是你想要限制用戶使用此方法的次數,則能夠設置Throttling。以下修改settings.py

REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': [
        'rest_framework.throttling.UserRateThrottle'
    ],
    'DEFAULT_THROTTLE_RATES': {
        'user': '10/day'
    }
}

接着在core/views.py中修改:

from rest_framework.throttling import UserRateThrottle

......
class TokenForToken(APIView):
    authentication_classes = [ExpiringTokenAuthentication]
    permission_classes = [permissions.IsAuthenticated]
    throttle_classes = [UserRateThrottle]

    ......

這樣能夠限制每一個用戶天天最多請求10次。更多throttling的用法請查看REST framework官方文檔。


歡迎關注個人公衆號「公子政的宅平常」,原創技術文章第一時間推送。

二維碼

相關文章
相關標籤/搜索