【15.0】登陆注册功能
【一】多方式登陆
【1】思路分析
(1)接口设计
-
接口描述
- 用户登录接口
-
请求URL
/api/v1/user/userinfo/mul_login/
-
请求方式
- POST
-
Body请求参数(application/json)
参数名 | 必选 | 类型 | 说明 |
---|---|---|---|
username | 是 | string | 用户名(支持用户名/邮箱/手机号) |
password | 是 | string | 密码 |
- 返回示例
- 500认证成功
- 401 认证失败
{
"code": 100,
"msg": "请求成功",
"username": "admin",
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNjkxNTY0MTE1LCJlbWFpbCI6IiJ9.RYxs3D6EjOfzLKGgF-QAav6k0sC_tCPWUd6tskGdlhI",
"icon": "http://127.0.0.1:8000/media/icon/default.png"
}
- 要求
- 校验参数是否为空
- 校验账号密码是否正确
- 登陆之后返回token
- 支持用户名多字段登陆,用户名可以使用手机号/邮箱/用户名登陆
【2】代码实现
- 视图层
luffyCity\luffyCity\apps\user\views.py
from rest_framework.viewsets import ViewSet, GenericViewSet
from rest_framework.decorators import action
from luffyCity.apps.user.models import User
from luffyCity.apps.user.serializers.mul_login_serializer import MulLoginSerializer
from luffyCity.utils.common_response import CommonResponse
from rest_framework.exceptions import APIException
# Create your views here.
class UserView(GenericViewSet):
'''
验证手机号接口
get请求
与数据库交互但不需要序列化
继承 ViewSet 自动生成路由
'''
# 序列化类
serializer_class = MulLoginSerializer
# 校验手机号
@action(methods=['GET'], detail=False)
def check_mobile(self, request, *args, **kwargs):
'''
get 请求 携带在地址参数
:param request:
:param args:
:param kwargs:
:return:
'''
try:
mobile = request.query_params.get('mobile', None)
User.objects.get(mobile=mobile) # 有且只有一条才不会报错
return CommonResponse(msg="手机号存在")
except Exception as e:
raise APIException("手机号不存在")
# 多方式登陆 --- 序列化类校验数据
@action(methods=['POST'], detail=False)
def mul_login(self, request, *args, **kwargs):
# 校验逻辑 --- 序列化类
ser = self.get_serializer(data=request.data)
# raise_exception:如果有错误,主动抛出异常,被全局异常捕获
# is_valid : 触发字段的校验规则,局部钩子/全局钩子(全局钩子中写验证逻辑,签发token)
ser.is_valid(raise_exception=True)
username = ser.context.get('username')
token = ser.context.get('token')
icon = ser.context.get('icon')
return CommonResponse(username=username, token=token, icon=icon)
- 序列化类
luffyCity\luffyCity\apps\user\serializers\mul_login_serializer.py
luffyCity\luffyCity\utils\common_settings.py
BACKEND_URL = 'http://127.0.0.1:8000'
- 路由
luffyCity\luffyCity\apps\user\urls.py
from django.urls import path
from .views import UserView
from rest_framework.routers import SimpleRouter
router = SimpleRouter()
router.register('userinfo', UserView, 'userinfo')
urlpatterns = [
]
urlpatterns += router.urls
【二】验证码接口封装成包
【1】验证码发送主函数
luffyCity\luffyCity\libs\SMS_TencentCloud_Sender\SMS_Ten_Send.py
【2】验证码发送配置文件
luffyCity\luffyCity\libs\SMS_TencentCloud_Sender\settings.py
【3】验证码包的初始化文件
luffyCity\luffyCity\libs\SMS_TencentCloud_Sender\__init__.py
【4】验证码发送接口的风险问题
验证码发送接口的风险问题主要是安全性和频率限制两个方面。
- 首先,短信验证码的安全性是一个重要的问题。
- 因为验证码通常用于身份验证或重要操作的确认,如果验证码泄露或被劫持,可能导致用户账号被盗或遭受其他安全威胁。
(1)提高安全性
数据加密:
- 在验证码发送接口中,可以引入加密机制
- 例如携带一个特定的加密串,使用对称或非对称加密算法将验证码数据进行加密,以防止数据被未授权人员获取并使用。
防止重放攻击:
- 为了防止恶意用户重复使用已经获取到的验证码进行验证,可以在验证码数据中加入一些唯一标识符或时间戳,并在验证过程中验证其有效性。
- 这样可以有效防止验证码的重放攻击。
(2)频率限制
- 其次,频率限制也是必要的。通过限制发送验证码的频率,可以降低恶意用户滥用接口或对系统造成过大负载的风险。
IP限制:
- 可以通过限制同一个IP地址在一段时间内发送验证码的次数,来限制恶意用户的行为。对于大流量代理服务器问题,可以考虑使用代理池技术,即在后台维护一个可用的代理IP池,通过轮询使用不同的IP地址发送验证码短信,以平均负载和降低单个IP的频率。
手机号限制:
- 同一个手机号在一定时间内发送验证码的次数也需要进行限制,以防止恶意用户通过滥用接口对用户账号进行猜测或攻击。可以设置一个合理的时间间隔,在此时间间隔内只允许发送有限次数的验证码。
【三】短信验证登录
【1】主视图函数
from django.shortcuts import render, HttpResponse
from rest_framework.viewsets import ViewSet, GenericViewSet
from rest_framework.decorators import action
from luffyCity.apps.user.models import User
from luffyCity.apps.user.serializers.mul_login_serializer import MulLoginSerializer, SmsLoginSerializer
from luffyCity.utils.common_response import CommonResponse
from rest_framework.exceptions import APIException
from luffyCity.libs.SMS_TencentCloud_Sender import get_verify_code, tencent_sms_main
from django.core.cache import cache
# Create your views here.
class UserView(GenericViewSet):
'''
验证手机号接口
get请求
与数据库交互但不需要序列化
继承 ViewSet 自动生成路由
'''
# 序列化类
serializer_class = MulLoginSerializer
# 校验手机号
@action(methods=['GET'], detail=False)
def check_mobile(self, request, *args, **kwargs):
'''
get 请求 携带在地址参数
:param request:
:param args:
:param kwargs:
:return:
'''
try:
mobile = request.query_params.get('mobile', None)
User.objects.get(mobile=mobile) # 有且只有一条才不会报错
return CommonResponse(msg="手机号存在")
except Exception as e:
raise APIException("手机号不存在")
def _common_login(self, request, *args, **kwargs):
# 校验逻辑 --- 序列化类
ser = self.get_serializer(data=request.data)
# raise_exception:如果有错误,主动抛出异常,被全局异常捕获
# is_valid : 触发字段的校验规则,局部钩子/全局钩子(全局钩子中写验证逻辑,签发token)
ser.is_valid(raise_exception=True)
username = ser.context.get('username')
token = ser.context.get('token')
icon = ser.context.get('icon')
return CommonResponse(username=username, token=token, icon=icon)
# 多方式登陆 --- 序列化类校验数据
@action(methods=['POST'], detail=False)
def mul_login(self, request, *args, **kwargs):
return self._common_login(request, *args, **kwargs)
@action(methods=['GET'], detail=False)
def send_sms(self, request, *args, **kwargs):
# 前端把需要发送验证码的手机号传入,携带在地址栏中
mobile = request.query_params.get('mobile', None)
code = get_verify_code(4) # 存储验证码,放到缓存内
cache.set(f'sms_code_{mobile}', code)
if mobile and tencent_sms_main(verify_code=code, tag_phone=mobile):
return CommonResponse(msg="发送验证码成功")
raise APIException("请输入手机号")
def get_serializer_class(self):
if self.action == 'sms_login':
return SmsLoginSerializer
else:
return self.serializer_class
@action(methods=['POST'], detail=False)
def sms_login(self, request, *args, **kwargs):
return self._common_login(request, *args, **kwargs)
【短信发送功能优化】异步处理
原来的发送短信,是同步
- 前端输入手机号---》点击发送短信---》前端发送ajax请求----》到咱们后端接口---》取出手机号----》调用腾讯发送短信---》腾讯去发短信---》发完后----》回复给我们后端发送成功---》我们后端收到发送成功---》给我们前端返回发送成功
把腾讯发送短信的过程,变成异步
- 前端输入手机号---》点击发送短信---》前端发送ajax请求----》到咱们后端接口---》取出手机号----》开启线程,去调用腾讯短信发送(异步)---》我们后端继续往后走----》直接返回给前端,告诉前端短信已发送
-另一条发短信线程线程会去发送短信,至于是否成功,我们不管了
@action(methods=['GET'], detail=False)
def send_sms(self, request, *args, **kwargs):
# 前端把需要发送验证码的手机号传入,携带在地址栏中
mobile = request.query_params.get('mobile', None)
code = get_verify_code(4) # 存储验证码,放到缓存内
cache.set(f'sms_code_{mobile}', code)
if mobile:
# 开启线程处理短信
# tencent_sms_main(verify_code=code, tag_phone=mobile)
t = Thread(target=tencent_sms_main, args=(code, mobile,))
t.start()
return CommonResponse(msg="验证码已发送")
raise APIException("请输入手机号")
【2】序列化校验
【四】注册功能
【2】功能实现
- 视图函数
luffyCity\luffyCity\apps\user\views.py
from threading import Thread
from django.shortcuts import render, HttpResponse
from rest_framework.viewsets import ViewSet, GenericViewSet
from rest_framework.mixins import CreateModelMixin
from rest_framework.decorators import action
from luffyCity.apps.user.models import User
from luffyCity.apps.user.serializers.mul_login_serializer import MulLoginSerializer, SmsLoginSerializer, \
UserRegisterSerializer
from luffyCity.utils.common_response import CommonResponse
from rest_framework.exceptions import APIException
from luffyCity.libs.SMS_TencentCloud_Sender import get_verify_code, tencent_sms_main
from django.core.cache import cache
# Create your views here.
class UserView(GenericViewSet, CreateModelMixin):
'''
验证手机号接口
get请求
与数据库交互但不需要序列化
继承 ViewSet 自动生成路由
'''
# 序列化类
serializer_class = MulLoginSerializer
# 校验手机号
@action(methods=['GET'], detail=False)
def check_mobile(self, request, *args, **kwargs):
'''
get 请求 携带在地址参数
:param request:
:param args:
:param kwargs:
:return:
'''
try:
mobile = request.query_params.get('mobile', None)
User.objects.get(mobile=mobile) # 有且只有一条才不会报错
return CommonResponse(msg="手机号存在")
except Exception as e:
raise APIException("手机号不存在")
def _common_login(self, request, *args, **kwargs):
# 校验逻辑 --- 序列化类
ser = self.get_serializer(data=request.data)
# raise_exception:如果有错误,主动抛出异常,被全局异常捕获
# is_valid : 触发字段的校验规则,局部钩子/全局钩子(全局钩子中写验证逻辑,签发token)
ser.is_valid(raise_exception=True)
username = ser.context.get('username')
token = ser.context.get('token')
icon = ser.context.get('icon')
return CommonResponse(username=username, token=token, icon=icon)
# 多方式登陆 --- 序列化类校验数据
@action(methods=['POST'], detail=False)
def mul_login(self, request, *args, **kwargs):
return self._common_login(request, *args, **kwargs)
# @action(methods=['GET'], detail=False)
# def send_sms(self, request, *args, **kwargs):
#
# # 前端把需要发送验证码的手机号传入,携带在地址栏中
# mobile = request.query_params.get('mobile', None)
# code = get_verify_code(4) # 存储验证码,放到缓存内
# cache.set(f'sms_code_{mobile}', code)
# if mobile and tencent_sms_main(verify_code=code, tag_phone=mobile):
# return CommonResponse(msg="发送验证码成功")
# raise APIException("请输入手机号")
@action(methods=['GET'], detail=False)
def send_sms(self, request, *args, **kwargs):
# 前端把需要发送验证码的手机号传入,携带在地址栏中
mobile = request.query_params.get('mobile', None)
code = get_verify_code(4) # 存储验证码,放到缓存内
cache.set(f'sms_code_{mobile}', code)
if mobile:
# 开启线程处理短信
# tencent_sms_main(verify_code=code, tag_phone=mobile)
t = Thread(target=tencent_sms_main, args=(code, mobile,))
t.start()
return CommonResponse(msg="验证码已发送")
raise APIException("请输入手机号")
def get_serializer_class(self):
if self.action == 'sms_login':
return SmsLoginSerializer
elif self.action == 'register' or self.action == 'create':
return UserRegisterSerializer
else:
return self.serializer_class
@action(methods=['POST'], detail=False)
def sms_login(self, request, *args, **kwargs):
return self._common_login(request, *args, **kwargs)
# 自己写的 访问:127.0.0.1:8000/api/v1/user/userinfo/register/ --->post请求即可
@action(methods=['POST'], detail=False)
def register(self, request, *args, **kwargs):
ser = self.get_serializer(data=request.data)
ser.is_valid(raise_exception=True)
ser.save()
# super().create(request, *args, **kwargs) # 只要这样写,又会走序列化
return CommonResponse(msg='注册成功')
# 不自己写了,只要继承CreateModelMixin,访问:127.0.0.1:8000/api/v1/user/userinfo --->post请求即可
# 这个我们不用写,它有 只要post请求过来,就会执行create
# def create(self, request, *args, **kwargs):
# serializer = self.get_serializer(data=request.data) # 第一个错误 UserRegisterSerializer
# serializer.is_valid(raise_exception=True) #执行三个校验:字段自己,局部钩子,全局
# self.perform_create(serializer)
# # 序列化要调用它,只要调用serializer.data ,就会走序列化,只要走序列化,会把create返回的user对象 来使用UserRegisterSerializer类做序列化
# return CommonResponse(msg='注册成功') #不走序列化了,序列类中得write_only 也就不用了
- 序列化类
luffyCity\luffyCity\apps\user\serializers\mul_login_serializer.py
# 注册:1 校验数据 2 保存 3 序列化用不要?存疑
class UserRegisterSerializer(serializers.ModelSerializer):
code = serializers.CharField(max_length=4, min_length=4, write_only=True)
class Meta:
model = User
fields = ['mobile', 'password', 'code'] # code 不是数据库的字段,需要重写
# 如果要限制密码强度,需要写个局部钩子
def _check_code(self, attrs):
mobile = attrs.get('mobile')
code = attrs.get('code')
old_code = cache.get('send_sms_code_%s' % mobile)
if not (code == old_code or (settings.DEBUG and code == '9999')): # 第二个错误:debug忘了设为True
raise APIException("验证码错误")
def _pre_save(self, attrs): # {mobile:122222,code:8888,password:123}
attrs.pop('code')
attrs['username'] = attrs.get('mobile') # 默认用户名就是手机号 可以随机生成用户名 随机生成有意义的名字( Faker)
def validate(self, attrs):
# 写逻辑
# 1 校验验证码是否正确
self._check_code(attrs)
# 2 入口前准备 ---》code不是数据库字段,不能入库,username是数据库字段必填,这里没有,写成默认
self._pre_save(attrs)
return attrs
def create(self, validated_data): # {mobile:122222,password:123,username:名字}
# 为什么要重写create? 因为密码人家是加密的,如果不写用的是
# User.objects.create(**validated_data) # 密码是铭文,必须重写
user = User.objects.create_user(**validated_data) # 保存成功,密码就是加密的
return user