DRF + jwt + mysql 防止用戶重複登錄

實現目標說明

用戶登錄最少需要做到如下兩點:
1.合法性,即 是他自己登錄的賬號(做法:token + https ,token驗證可以確定操作源是該用戶本人,https可以在很大程度上保證token在傳輸的過程中不被截獲篡改,是目前較爲安全的一種做法)
2.唯一性,即 不能對同一個賬號重複登錄(做法:服務器保存生成的token後再將其發送給用戶,每次用戶請求數據時,先驗證token是否相同,然後再驗證token是否有效。相同,則說明用戶唯一,有效則說明用戶合法。注意:就算傳入token和數據庫中的token相同,也不能說明用戶合法,因爲數據庫中的token可能已經過期無效了,如果token過期就讓用戶重新登錄,然後再次簽發和保存token。ps:token必須具有時效性,否則用戶數據被破解只是時間問題)
做法總結:
1.djangorestframework + djangorestframework-jwt 實現用戶的token登錄。全局驗證token有效性(註冊、登錄和網站通用視圖除外),並且只通過登錄視圖發放token給用戶。
2.通過mysql保存用戶token,確保單個賬號只有一個用戶在線。用戶的每次操作都需要對比請求頭中的token和數據庫中的token是否相同,全局驗證token唯一性,(註冊、登錄和網站通用視圖除外)

首先創建自定義用戶模塊

  1. 要登陸就需要有用戶模塊,django本身自帶auth用戶模塊,但不能滿足現在五花八門的用戶字段需求。好在django還提供了自定義用戶模塊的功能,創建方法如下:

輸入命令創建user

# 輸入命令
python manage.py startapp user

編寫自定義用戶

from django.db import models
from django.contrib.auth.models import AbstractUser

# 繼承 django 的 AbstractUser
class UserInfo(AbstractUser):
    """
    自定義的用戶模塊
    """
    nick_name = models.CharField(max_length=15, default="NoOne", verbose_name="暱稱")
    sign = models.TextField(max_length=100, default="要不寫點什麼,反正你挺閒的、、", verbose_name="簽名")
    token = models.CharField(max_length=300, null=True, blank=True, verbose_name="用戶認證token")

    class Meta:
        verbose_name = "用戶信息"
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.username

配置settings.py文件

# 在 settings.py 文件中添加如下內容,其中 UserInfo 是自己定義的用戶模塊名稱
AUTH_USER_MODEL = 'user.UserInfo'   # 採用自己定製的用戶model

# 在settings中註冊自定義的用戶模塊
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'rest_framework',
    'crispy_forms',
    'corsheaders',

    'apps.user',

]

註冊用戶到django admin

# 別忘記註冊到 django admin 後臺管理系統
# 如下兩種方式選一種就行

# 1 顯示所有字段
admin.site.register(UserModel)

# 2 顯示自定義的字段
# @admin.register(UserInfo)
# class CategoryAdmin(admin.ModelAdmin):
    # list_display = ('nick_name', 'sign', 'username', 'email')
    # fields = ('nick_name', 'sign', 'username', 'password', 'email')

最後 makemigrations 和 migrate 同步數據庫,然後打開數據庫管理工具就可以看到自定義的用戶表了。到此,自定義用戶模塊完成。

安裝配置djangorestframework-jwt模塊

2.安裝並配置token驗證模塊djangorestframework-jwt,如下:

輸入命令安裝

# 安裝
pip install djangorestframework-jwt

配置settings.py文件

# 配置
# DRF中間件
REST_FRAMEWORK = {

    'DEFAULT_PERMISSION_CLASSES': (
        # 提供的權限↓
        # AllowAny 允許所有用戶
        # IsAuthenticated 僅通過認證的用戶
        # IsAdminUser 僅管理員用戶
        # IsAuthenticatedOrReadOnly 認證的用戶可以完全操作,否則只能get讀取
        'rest_framework.permissions.IsAuthenticated',
    ),

    'DEFAULT_AUTHENTICATION_CLASSES': (
        # 認證方式
        # 此方法是自定義方法,用於檢查用戶是否重複登錄,後面會講
        'utils.my_authentication.LoginRepeatAuth',    
        # 此方法是 djangorestframework-jwt 的方法,用於檢查用戶token是否合法
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
    ),
}
JWT_AUTH = {
    # 指明Token的有效期
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1),
    'JWT_AUTH_HEADER_PREFIX': 'JWT',
}

編寫 檢查重複登錄 的方法

3.編寫檢查用戶重複登錄的方法:

from rest_framework import exceptions
from rest_framework.authentication import BaseAuthentication

from apps.user.models import UserInfo


class LoginRepeatAuth(BaseAuthentication):
    # authenticate必須在DRF認證內被重寫,request參數必須傳入
    def authenticate(self, request):
        '''
        將獲取的token去token表內進行比對,存在信息即驗證通過
            - 獲取token表內token更新時間,若超時則驗證失敗重新登陸(用於數據的清理)
            - 驗證通過:返回空 - 表示後續仍能繼續其他驗證
            = 驗證通過:返回認證用戶和當前數據記錄對象 - 後續不再進行驗證
                - 對應DRF內Request對象User類內_authenticate方法執行
                - from rest_framework.request import Request
        :param request:
        :return:
        '''
        # 數據放在header內傳輸,request.META獲取
        # meta查詢key值格式:HTTP_大寫字段名 例如:token - HTTP_TOKEN
        token = request.META.get('HTTP_AUTHORIZATION')
        # token = request.query_params.get('token')
        # 查找是否存在token值和請求頭中的token值相同的用戶
        user = UserInfo.objects.filter(token=token).first()
        if not user:
            # 如果沒有,就認爲是跳過了登錄階段的非法操作.
            # 因爲只有登錄方法會保存生成的token並返回給用戶,如果保存的token和用戶攜帶的token不同,說明用戶token被篡改,或有人嘗試破解用戶的token.
            # 此時應終止方法,並要求用戶重新登錄,以便保存並返回給用戶新的token
            raise exceptions.APIException('token比對失敗,非法操作,請勿跳過登錄方法!')
        # 查詢到對應用戶信息,認證通過
        # 此時如果接下來還有驗證方法,就返回 None
        # 如果接下來沒有驗證方法了,那麼返回 當前認證用戶 和 當前token記錄對象
        # 返回的數據可通過 request.user, request.auth進行獲取
        return None

編寫用戶 註冊 登錄 和 修改密碼 的view

  1. 編寫用戶 註冊 登錄修改密碼 方法, 註冊和登錄方法 用於用戶上線,修改密碼方法用於測試用戶的 合法性 和 唯一性 如下:
from rest_framework.views import APIView
from rest_framework.renderers import JSONRenderer
from rest_framework.response import Response
from rest_framework import status

from rest_framework_jwt.settings import api_settings

from .models import UserInfo


class RegisterView(APIView):
    """
    用戶註冊
    parm = [ username, password ]
    """
    renderer_classes = [JSONRenderer]   # json渲染器
    authentication_classes = []         # 此方法不驗證JWT
    permission_classes = []             # 此方法不設權限

    def post(self, request):
        username = request.data.get("username", 0)
        password = request.data.get("password", 0)
        if username and password:
            # 校驗註冊,名字不可重複
            user = UserInfo.objects.filter(username=username).first()
            if user:
                content = {'msg': '用戶已存在'}
                return Response(content, status=status.HTTP_400_BAD_REQUEST)
            else:
                # 註冊成功,創建用戶
                UserInfo.objects.create_user(
                    username=username,
                    password=password
                )
                content = {'msg': '註冊成功'}
                return Response(content, status=status.HTTP_201_CREATED)
        content = {'msg': '賬號或密碼不能爲空'}
        return Response(content, status=status.HTTP_403_FORBIDDEN)


class LoginView(APIView):
    """
    用戶登錄
    parm = [ username, password]
    """
    renderer_classes = [JSONRenderer]   # json渲染器
    authentication_classes = []         # 此方法不驗證JWT
    permission_classes = []             # 此方法不設權限

    def post(self, request):

        # 登錄的業務邏輯 start
        username = request.data.get("username", 0)
        password = request.data.get("password", 0)
        if not username or not password:
            content = {'msg': '輸入的賬號或密碼有誤'}
            return Response(content, status=status.HTTP_400_BAD_REQUEST)
        else:
            user = UserInfo.objects.filter(username=username).first()
            if not user or not user.check_password(password):
                content = {'msg': '輸入的賬號或密碼有誤'}
                return Response(content, status=status.HTTP_400_BAD_REQUEST)
            else:

                # 生成token的業務邏輯 start
                jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
                jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
                payload = jwt_payload_handler(user)
                token = jwt_encode_handler(payload)

                token_with_JWT = "JWT " + token     # 爲token添加JWT頭後保存到數據庫
                user.token = token_with_JWT
                user.save()
                # 生成token的業務邏輯 end

                content = {'Authorization': token_with_JWT}
                return Response(content, status=status.HTTP_200_OK)
        # 登錄的業務邏輯 end


class ChangePassWord(APIView):
    """
    用戶改密
    parm = [ username, password, new_password ]
    """
    renderer_classes = [JSONRenderer]  # json渲染器

    def post(self, request):
        username = request.data.get("username", 0)
        old_password = request.data.get("old_password", 0)
        new_password = request.data.get("new_password", 0)
        if username and old_password and new_password:
            # 校驗用戶名和密碼
            user = UserInfo.objects.filter(username=username).first()
            if user and user.check_password(old_password):
                # 校驗成功,保存新密碼
                user.set_password(new_password)
                user.save()
                content = {'msg': '密碼修改成功'}
                return Response(content, status=status.HTTP_205_RESET_CONTENT)
            else:
                content = {'msg': '原密碼輸入有誤'}
                return Response(content, status=status.HTTP_400_BAD_REQUEST)
        content = {'msg': '賬號或密碼不能爲空'}
        return Response(content, status=status.HTTP_400_BAD_REQUEST)

配置三個視圖的url

# 別忘記將視圖的url配置好,此處的 DEV_NAME 是我定義的應用名,可以去掉

# 用戶登錄(JWT唯一獲取接口)
path(DEV_NAME + 'login/', LoginView.as_view()),

# 用戶註冊
path(DEV_NAME + 'register/', RegisterView.as_view()),

# 用戶修改密碼
path(DEV_NAME + 'change_pw/', ChangePassWord.as_view()),

通過postman測試用戶的 合法性 和 唯一性

  1. 完成上述步驟後,就可以通過postman進行測試了.

首先測試用戶合法性:
註冊賬號,
登錄並複製返回的token
粘貼token到請求頭,然後攜帶請求頭進行密碼修改,
提示密碼修改成功.

然後測試用戶唯一性:
再次登錄剛纔的賬號獲得一個新的token,但是我們不用這個token,
仍然使用剛纔的那個token進行密碼修改,這時會提示 ‘token比對失敗,非法操作,請勿跳過登錄方法!’,
這樣就確保了最新一次的登錄會覆蓋掉之前的登錄,保證了用戶的唯一性.

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