本章將實現注冊、登錄,包括短信驗證碼在內的三個接口。
1. 登錄
用戶認證的兩種方式:
drf
:token
,保存在數據庫中,如果是分布式系統比較麻煩,且token
永久有效,無過期時間jwt
1.1 drf token 實現用戶認證
1、settings.py
:
INSTALLED_APPS = (
...
'rest_framework.authtoken'
)
migrate
生成一張 authtoken_token
數據表,用於存儲每個用戶的 token
。
2、配置路由:
from rest_framework.authtoken import views
urlpatterns = [
path('login/', views.obtain_auth_token),
]
3、xadmin
創建一個新的用戶,訪問 http://127.0.0.1:8000/login
,再 post
相應的用戶名和密碼,會返回一個 token
:
也可以使用 postman、postwoman
等軟件進行接口測試。
4、客戶端身份驗證:
令牌秘鑰包含在 Authorization HTTP header
中,並以 Token
為前綴(關鍵字),關鍵字修改只需子類化 TokenAuthentication
,並設置 keyword
類變量:
-
身份驗證成功,
TokenAuthentication
提供一些憑據:request.user
:Django User
實例對象request.auth
:rest_framework.authtoken.models.Token
實例對象
-
身份驗證失敗:響應
HTTP 401 Unauthorized
5、獲取 request.user 和 request.auth
,需在 settings
中添加:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication'
)
}
1.2 jwt 實現用戶認證
官網:http://getblimp.github.io/django-rest-framework-jwt/
1、安裝:
pipenv install djangorestframework-jwt
# pip install djangorestframework-jwt
2、配置 settings
:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
)
}
3、路由:
from rest_framework_jwt.views import obtain_jwt_token
path('login/', obtain_jwt_token), # jwt 的 token 認證
4、接口測試:http://127.0.0.1:8000/login
post
請求:
{
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImhqIiwiZXhwIjoxNTk0MzA5MDEwLCJlbWFpbCI6ImhqQHFxLmNvbSJ9.mcBE_FVwt5pOn2nuM6ynve3scA3i3ThiIdxINrO8oVE"
}
1.3 自定義用戶認證
jwt
默認采用用戶名和密碼進行驗證,要想支持手機驗證,需要自定義驗證。
1、settings.py
:
AUTHENTICATION_BACKENDS = (
'users.views.CustomBackend',
)
2、users/views.py
:
from django.contrib.auth import get_user_model
User = get_user_model()
class CustomBackend(ModelBackend):
"""自定義用戶驗證"""
def authenticate(self, request, username=None, password=None, **kwargs):
try:
# 使得用戶名和手機號都能登錄
user = User.objects.get(
Q(username=username) | Q(mobile=username)
)
if user.check_password(password):
return user
except Exception as e:
return None
3、設置 jwt
有效期時間:
# jwt 有效期限
JWT_AUTH = {
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7), # 也可以設置seconds=20
'JWT_AUTH_HEADER_PREFIX': 'JWT', # JWT跟前端保持一致,比如“token”這里設置成JWT
}
2. 短信驗證碼
2.1 注冊雲通訊
能夠發送短信驗證碼的雲通訊網站有很多,比如:
- 雲片網:
https://www.yunpian.com/
- 榮聯運:
https://www.yuntongxun.com/
注冊成功后都有提供免費的測試短信(一般為 10 條),這里采用的是雲片網。
新建 Python
包 apps/utils
,再新建 utils/yunpian.py
文件,用於給雲片網發送請求,最后通過雲片網給相應的手機發送短信驗證碼:
import requests
import json
class YunPian:
def __init__(self, api_key):
self.api_key = api_key
self.single_send_url = "https://sms.yunpian.com/v2/sms/single_send.json"
def send_sms(self, code, mobile):
# 需要傳遞的參數
params = {
"apikey": self.api_key,
"mobile": mobile,
"text": "【Hubery_Jun 生鮮超市】您的驗證碼是 {code},1 分鍾有效。如非本人操作,請忽略本短信".format(code=code)
}
print(params)
response = requests.post(self.single_send_url, data=params)
print(response.status_code, response.text)
re_dict = json.loads(response.text)
return re_dict
if __name__ == '__main__':
yun_pian = YunPian("xxx")
yun_pian.send_sms("2020", "手機號碼")
2.2 短信驗證碼接口
1、添加相應配置 settings
:
# 手機號碼正則表達式
REGEX_MOBILE = "^1[358]\d{9}$|^147\d{8}$|^176\d{8}$"
# 雲片網 APIKEY,注冊成功后在控制台可查看
APIKEY = "f84c2dc13c55xxxxx6e783ba65ab"
2、序列化數據(驗證手機號、驗證碼是否合法)users/serializers.py
:
import re
from datetime import datetime, timedelta
from django.contrib.auth import get_user_model
from rest_framework import serializers
from rest_framework.validators import UniqueValidator
from users.models import VerifyCode
from MxShop.settings import REGEX_MOBILE
class SmsSerializer(serializers.Serializer):
"""短信驗證"""
mobile = serializers.CharField(max_length=11)
def validate_mobile(self, mobile):
"""
手機號碼驗證:函數名必須為 validate_驗證字段名
:param mobile:
:return:
"""
# 手機號是否已注冊
if User.objects.filter(mobile=mobile).count():
raise serializers.ValidationError("用戶已存在")
# 手機號格式是否合法
if not re.match(REGEX_MOBILE, mobile):
raise serializers.ValidationError("手機號格式非法")
# 驗證碼頻率,1min 只能發一次
one_min_time = datetime.now() - timedelta(hours=0, minutes=1, seconds=0)
# 從數據庫中取出驗證碼,view 中生成驗證碼並發送成功后,會將驗證碼存儲到 VerifyCode 模型中,這里只需取出驗證時間即可
if VerifyCode.objects.filter(add_time__gt=one_min_time, mobile=mobile).count():
raise serializers.ValidationError("距離上一次發送未超過60s")
return mobile
3、視圖 users/views.py
:
class SmsCodeViewSet(CreateModelMixin, viewsets.GenericViewSet):
"""手機驗證碼"""
serializer_class = SmsSerializer
def generate_code(self):
"""生成四位數字的驗證碼"""
seeds = "1234567890"
random_str = []
for i in range(4):
random_str.append(choice(seeds))
return "".join(random_str)
def create(self, request, *args, **kwargs):
"""
創建驗證碼
發送驗證碼
發送成功將驗證碼、手機號存儲到模型中
"""
serializer = self.get_serializer(data=request.data)
# 驗證合法
serializer.is_valid(raise_exception=True)
mobile = serializer.validated_data["mobile"]
yun_pian = YunPian(APIKEY)
# 生成驗證碼
code = self.generate_code()
# 發送短信驗證碼
sms_status = yun_pian.send_sms(code=code, mobile=mobile)
# 這段不需要添加(臨時添加的)
# code_record = VerifyCode(code=code, mobile=mobile)
# code_record.save()
# 短信驗證碼發送失敗
if sms_status["code"] != 0:
return Response({
"mobile": sms_status["msg"]
}, status=status.HTTP_400_BAD_REQUEST)
else:
# 發送成功,保存到數據庫中
code_record = VerifyCode(code=code, mobile=mobile)
code_record.save()
return Response({
"mobile": mobile
}, status=status.HTTP_201_CREATED)
4、配置路由 MxShop/urls.py
:
router.register(r'code', SmsCodeViewSet, basename="code") # 短信驗證碼
5、接口測試:
3. 注冊
3.1 drf 注冊接口實現
前端 vue
中注冊支持用戶名和手機號進行注冊,所以需要添加對手機號的注冊實現。
1、修改模型 UserProfile mobile
字段:
# 修改之前
mobile = models.CharField("電話", max_length=11)
# 修改之后
mobile = models.CharField("電話", max_length=11, null=True, blank=True)
因為注冊用戶名可以是用戶名,也可以是手機號,而模型中 mobile
字段不能為空。
2、用戶注冊序列化 users/serializers.py
:
class UserSerializer(serializers.ModelSerializer):
"""用戶注冊"""
# UserProfile 中沒有 Code 字段,自定義一個
code = serializers.CharField(required=True, write_only=True, max_length=4, min_length=4, error_messages={
"blank": "請輸入驗證碼",
"required": "請輸入驗證碼",
"max_length": "驗證碼格式錯誤",
"min_length": "驗證碼格式錯誤"
}, help_text="驗證碼")
# 驗證用戶名
username = serializers.CharField(label="用戶名", help_text="用戶名", required=True, allow_blank=False,
validators=[UniqueValidator(queryset=User.objects.all(), message="用戶名已存在")])
def validate_code(self, code):
"""
驗證驗證碼
post 數據都保存在 initial_data 里,username 為用戶注冊的手機號,驗證碼
按添加時間倒序排序,為了后面驗證過期,錯誤等
:param code:
:return:
self.initial_data:{'password': 'abcd110139', 'username': '18674447633', 'code': '6188'}
"""
verify_records = VerifyCode.objects.filter(mobile=self.initial_data['username']).order_by('-add_time')
if verify_records:
# 最近的一個驗證碼
last_record = verify_records[0]
# 有效期為 5 min
five_mintues_ago = datetime.now() - timedelta(hours=0, minutes=5, seconds=0)
if five_mintues_ago > last_record.add_time:
raise serializers.ValidationError('驗證碼過期!')
if last_record.code != code:
raise serializers.ValidationError('驗證碼錯誤!')
else:
raise serializers.ValidationError('驗證碼錯誤!')
def validate(self, attrs):
"""
驗證所有字段,attr 為驗證合法后返回的 dict
:param self:
:param attrs:
:return:
"""
# 前端沒有傳 mobile 值到后端,添加進來
attrs['mobile'] = attrs['username']
# 模型中沒有 code 字段,驗證完之后,刪除
del attrs['code']
return attrs
class Meta:
model = User
# 前端顯示的字段
fields = ('username', 'code', 'mobile')
主要用於驗證驗證碼 code
字段(需要添加新字段),以及其他字段。
3、users/views.py
:
from users.serializers import SmsSerializer, UserSerializer
class UserCreateViewSet(CreateModelMixin, viewsets.GenericViewSet):
"""
創建用戶、注冊用戶
"""
serializer_class = UserSerializer
4、配置路由 MxShop/urls.py
:
from users.views import SmsCodeViewSet, UserCreateViewSet
router.register(r'users', UserCreateViewSet, basename='users') # 注冊
3.2 修改用戶密碼
給注冊接口添加密碼字段,並進行密文存儲(默認明文),有兩種方法:
- 重新
create()
方法 Django
信號量
3.2.1 重寫 create 方法
1、users/views.py
:
from users.serializers import SmsSerializer, UserSerializer
class UserCreateViewSet(CreateModelMixin, viewsets.GenericViewSet):
"""
創建用戶、注冊用戶
"""
serializer_class = UserSerializer
queryset = User.objects.all()
2、users/serializers.py
:
from rest_framework.validators import UniqueValidator
class UserSerializer(serializers.ModelSerializer):
"""用戶注冊"""
# 添加 password 字段
password = serializers.CharField(style={'input_type': 'password'}, label="密碼", write_only=True)
def create(self, validated_data):
"""密碼加密保存"""
user = super(UserSerializer, self).create(validated_data=validated_data)
user.set_password(validated_data['password'])
user.save()
return user
# fields 中添加 password 字段
class Meta:
model = User
fields = ('username', 'code', 'mobile', 'password')
3.2.2 信號量
1、新建 users/signals.py
:
from django.contrib.auth import get_user_model
from django.db.models.signals import post_save
from django.dispatch import receiver
User = get_user_model()
# post_save:信號的方法
# sender:接收信號的 model
@receiver(post_save, sender=User)
def create_user(sender, instance=None, created=False, **kwargs):
# 是否新建,update 不需要新建,只需更新
if created:
password = instance.password
instance.set_password(password)
instance.save() # instance 相當於 user 對象
2、重載配置 users/apps.py
:
from django.apps import AppConfig
# 項目啟動時運行
class UsersConfig(AppConfig):
name = 'users'
# 設置 app 名字為中文,admin 中
verbose_name = '用戶管理'
# 新增
def ready(self):
import users.signals
3.3 生成 token
生成 token
需要兩個步驟:payload 和 encode
:
users/views.py
:
class UserCreateViewSet(CreateModelMixin, viewsets.GenericViewSet):
"""
創建用戶、注冊用戶
"""
serializer_class = UserSerializer
queryset = User.objects.all()
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = self.perform_create(serializer)
re_dict = serializer.data
payload = jwt_payload_handler(user)
re_dict["token"] = jwt_encode_handler(payload)
re_dict["name"] = user.name if user.name else user.username
headers = self.get_success_headers(serializer.data)
return Response(re_dict, status=status.HTTP_201_CREATED, headers=headers)
def perform_create(self, serializer):
return serializer.save()
至此三個接口均已完成。
總結
- 不管是登錄還是注冊,
token
驗證,盡量采用jwt
- 注冊接口密碼默認是明文的,可以重寫
create
方法或使用信號量進行加密處理 - 雲通訊注冊成功后需要接入短信,否則會出現無可用簽名