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唯一性,(註冊、登錄和網站通用視圖除外)
首先創建自定義用戶模塊
- 要登陸就需要有用戶模塊,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
- 編寫用戶 註冊 登錄 和 修改密碼 方法, 註冊和登錄方法 用於用戶上線,修改密碼方法用於測試用戶的 合法性 和 唯一性 如下:
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測試用戶的 合法性 和 唯一性
- 完成上述步驟後,就可以通過postman進行測試了.
首先測試用戶合法性:
註冊賬號,
登錄並複製返回的token
粘貼token到請求頭,然後攜帶請求頭進行密碼修改,
提示密碼修改成功.
然後測試用戶唯一性:
再次登錄剛纔的賬號獲得一個新的token,但是我們不用這個token,
仍然使用剛纔的那個token進行密碼修改,這時會提示 ‘token比對失敗,非法操作,請勿跳過登錄方法!’,
這樣就確保了最新一次的登錄會覆蓋掉之前的登錄,保證了用戶的唯一性.