前端我使用的是vben-admin(悄悄说一下,好难用。。),对原生的登录页进行了修改。
本文主要讲一下后端实现。
参考文档:
django+celery使用阿里云短信服务异步发送注册验证码_小泽十一章的博客-CSDN博客
django-实现登录短信验证_子钦加油的博客-CSDN博客
在完成测试前,可以先不搭建celery,celery主要是一个异步任务机制。
1. 准备条件
- Redis,需要本地搭建一个Redis服务,Redis主要是一个键值对数据库。
- 开通阿里的短信服务,并安装对应的sdk
2. 短信登录逻辑及实现
1)前端form表单中输入电话号码后,点击发送,触发后端的发送验证码功能。2)后端先解析电话号码是否合理,以及数据库中是否存在该电话号码所对应的用户。
如果存在,则随机生成一个验证码,并使用阿里云的短信发送接口将验证码发送到该手机。
并将手机号:验证码键值对存入Redis。
3)前端用户收到短信后,在表单中填入验证码,点击登录按钮后触发后端的登录api。
这时后端将收到的验证码,与Redis库中存的验证码进行比对,如果一致则合理,返回token及用户信息到前端。
2.1 代码:
settings.py
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://redis-ip:6379/0",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
}
}
}
# 将session缓存在Redis中
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "default"
SESSION_COOKIE_AGE = 60 * 60 * 12 # 12小时
SESSION_SAVE_EVERY_REQUEST = True
SESSION_EXPIRE_AT_BROWSER_CLOSE = True # 关闭浏览器,则COOKIE失效
url.py
from django.urls import include, path
from rest_framework import routers
from . import views
router = routers.DefaultRouter()
router.register(r'auth', views.AuthViewSet, basename='auth')
urlpatterns = [
path('', include(router.urls)),
]
views.py
import time
import traceback
import random
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.forms.models import model_to_dict
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from rest_framework import permissions, status
from rest_framework.authtoken.models import Token
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.viewsets import ViewSet, ModelViewSet, ReadOnlyModelViewSet
from phonenumbers import is_valid_number, parse as parse_number
from django.core.cache import cache
from utils.aliyun import Aliyun
from .serializers import (
SendSMSCodeSerializer,
MobileLoginSerializer,
)
class AuthViewSet(ViewSet):
@action(detail=False, methods=['post'])
def send_sms_code(self, request, *args, **kwargs):
serializer = SendSMSCodeSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
mobile = serializer.validated_data['mobile']
# 生成随机验证码,发送验证码并将其保存到数据库中
stored_sms = random.randrange(1000, 9999)
Aliyun.ali_send_sms(mobile, stored_sms)
cache.set(mobile, stored_sms) # 设置mobile:sms键值对,有效期为60s
return Response({'detail': 'Verification code sent successfully'}, status=status.HTTP_200_OK)
@action(detail=False, methods=['post'])
def mobile_login(self, request, *args, **kwargs):
serializer = MobileLoginSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.validated_data['user']
token, created = Token.objects.get_or_create(user=user)
if time.time() - token.created.timestamp() > settings.TOKEN_EXPIRE_SECONDS:
Token.objects.filter(user_id=user.id).delete()
token, created = Token.objects.get_or_create(user=user)
return Response({'token': token.key,
'userId': user.username,})
serializers.py 中要创建序列化类并定义验证函数:
import re
from django.contrib.auth import authenticate, get_user_model
from django.contrib.auth.models import Group
from django.db.models.fields.files import FieldFile
from django.forms.models import model_to_dict
from rest_framework import serializers
from django.core.cache import cache
import phonenumbers
User = get_user_model()
class MobileLoginSerializer(serializers.Serializer):
mobile = serializers.CharField(write_only=True)
sms = serializers.CharField(write_only=True)
def validate(self, attrs):
mobile = attrs.get('mobile')
sms = attrs.get('sms')
# 从数据库中获取存储的验证码, 检查存储的验证码与输入的验证码是否匹配
if cache.has_key(mobile):
stored_sms = cache.get(mobile)
else:
serializers.ValidationError("未找到验证码")
if mobile and sms:
if str(stored_sms) != str(sms):
raise serializers.ValidationError("验证码错误, sms:{}, stored_sms: {}".format(sms, stored_sms))
user = User._default_manager.get(mobile=mobile)
if not user:
msg = f"Access denied: wrong mobile: {mobile} or sms: {sms}."
raise serializers.ValidationError(msg)
else:
msg = 'Both "mobile" and "sms" are required.'
raise serializers.ValidationError(msg)
attrs['user'] = user
return attrs
class SendSMSCodeSerializer(serializers.Serializer):
mobile = serializers.CharField(write_only=True)
def validate(self, attrs):
mobile = attrs.get('mobile')
mobile_string = phonenumbers.parse('+86{}'.format(mobile), None)
if not phonenumbers.is_valid_number(mobile_string):
raise serializers.ValidationError('号码不存在: {}'.format(mobile))
if mobile:
user = User._default_manager.get(mobile=mobile)
if not user:
msg = f"未找到该号码相关的注册用户"
raise serializers.ValidationError(msg)
else:
msg = '手机号码错误, 请重新输入'
raise serializers.ValidationError(msg)
return attrs
3. 阿里云短信发送接口
先登录阿里云,支付宝扫码登录即可,直接搜索栏搜索短信服务,点击进入:
搜索短信服务->概览->右下角点击AccessKey, 刚开始可申请一段时间的免费试用。
为了防止token过大导致的权限问题,阿里推荐使用RAM子账号。
点击后,会生成对应的'accessKeyId', 'accessKeySecret',调用的时候传入即可,我是写在了Django的settings里面。
短信签名,模板都是需要申请的,目前暂时使用的是阿里测试签名和短信模板。
send_sms_request = dysmsapi_20170525_models.SendSmsRequest(
sign_name='阿里云短信测试',
template_code='SMS_154950909',
phone_numbers='159*****888',
template_param='{"code":"1234"}'
)
短信服务_SDK中心-阿里云OpenAPI开发者门户 (aliyun.com)
有很多版本,我们Django应该选择python版本,安装的python package为:
pip install alibabacloud_dysmsapi20170525==2.0.23
3.1 代码:
settings.py
ACCESS_KEY_ID = 'xxx'
ACCESS_KEY_SECRET = 'xxx'
utils/aliyun.py
# -*- coding: utf-8 -*-
# This file is auto-generated, don't edit it. Thanks.
import sys
import json
from django.conf import settings
from alibabacloud_dysmsapi20170525.client import Client as Dysmsapi20170525Client
from alibabacloud_tea_openapi import models as open_api_models
from alibabacloud_dysmsapi20170525 import models as dysmsapi_20170525_models
from alibabacloud_tea_util import models as util_models
from alibabacloud_tea_util.client import Client as UtilClient
class Aliyun:
def __init__(self):
pass
@staticmethod
def create_client(
access_key_id: str,
access_key_secret: str,
) -> Dysmsapi20170525Client:
"""
使用AK&SK初始化账号Client
@param access_key_id:
@param access_key_secret:
@return: Client
@throws Exception
"""
config = open_api_models.Config(
# 必填,您的 AccessKey ID,
access_key_id=access_key_id,
# 必填,您的 AccessKey Secret,
access_key_secret=access_key_secret
)
# 访问的域名
config.endpoint = f'dysmsapi.aliyuncs.com'
return Dysmsapi20170525Client(config)
@staticmethod
def ali_send_sms(mobile, sms) -> None:
# 工程代码泄露可能会导致AccessKey泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考,建议使用更安全的 STS 方式,更多鉴权访问方式请参见:https://help.aliyun.com/document_detail/378659.html
# client = Aliyun.create_client(settings.ACCESS_KEY_ID, settings.ACCESS_KEY_SECRET)
client = Aliyun.create_client(settings.ACCESS_KEY_ID, settings.ACCESS_KEY_SECRET)
send_sms_request = dysmsapi_20170525_models.SendSmsRequest(
sign_name='阿里云短信测试',
template_code='SMS_154950909',
phone_numbers=mobile,
template_param=json.dumps({"code":sms})
)
try:
# 复制代码运行请自行打印 API 的返回值
client.send_sms_with_options(send_sms_request, util_models.RuntimeOptions())
except Exception as error:
# 如有需要,请打印 error
UtilClient.assert_as_string(error.message)
@staticmethod
async def ali_send_sms_async(mobile, sms) -> None:
# 工程代码泄露可能会导致AccessKey泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考,建议使用更安全的 STS 方式,更多鉴权访问方式请参见:https://help.aliyun.com/document_detail/378659.html
client = Aliyun.create_client(settings.ACCESS_KEY_ID, settings.ACCESS_KEY_SECRET)
send_sms_request = dysmsapi_20170525_models.SendSmsRequest(
sign_name='阿里云短信测试',
template_code='SMS_154950909',
phone_numbers=mobile,
template_param=json.dumps({"code":sms})
)
try:
# 复制代码运行请自行打印 API 的返回值
await client.send_sms_with_options_async(send_sms_request, util_models.RuntimeOptions())
except Exception as error:
# 如有需要,请打印 error
UtilClient.assert_as_string(error.message)