小程序学习-用户使用手机登录验证
案例:用户使用手机号,输入验证码登录,验证码保存60s,用户登录成功后,获取用户信息。
小程序前台
app.js
App({ /** * 当小程序初始化完成时,会触发 onLaunch(全局只触发一次) */ onLaunch: function () { var userInfo = wx.getStorageSync('userInfo'); if (userInfo) { this.globalData.userInfo = userInfo; } }, globalData: { userInfo: null, // {phone:xxx,token:xxxx} }, initUserInfo: function (res, localInfo) { var info = { token: res.token, phone: res.phone, nickName: localInfo.nickName, avatarUrl: localInfo.avatarUrl } // 1.去公共的app.js中调用globalData,在里面赋值。(在全局变量赋值) this.globalData.userInfo = info;//{phone:xxx,token:xxxx} // 2.在本地“cookie”中赋值 wx.setStorageSync("userInfo", info); }, delUserInfo: function () { this.globalData.userInfo = null; wx.removeStorageSync("userInfo") } })
本地存储操作
wx.getStorageSync('userInfo'); wx.setStorageSync('userInfo',"yy"); wx.removeStorageSync("userInfo")
home页面
home.wxml
<view class="container"> <view class="top-view"> <view class="user"> <view class="row"> <image class="avatar" wx:if="{{userInfo}}" src="{{userInfo.avatarUrl}}"></image> <image class="avatar" wx:else="{{userInfo}}" src="/images/hg.jpg"></image> <view class="name" wx:if="{{userInfo}}"> <view bindtap="onClickLogout">{{userInfo.nickName}}</view> </view> <view class="name" wx:else="{{userInfo}}"> <navigator url="/pages/auth/auth">登录</navigator> </view> </view> <view class="site">查看个人主页</view> </view> </view> </view>
home.wxss
.top-view{ background-color: #01ccb6; color: white; padding: 40rpx; } .top-view .user{ display: flex; flex-direction: row; justify-content: space-between; align-items: center; } .top-view .user .row{ display: flex; flex-direction: row; justify-content: flex-start; align-items: center; } .top-view .user .avatar{ width: 100rpx; height: 100rpx; border-radius: 50%; } .top-view .user .name{ display: flex; flex-direction: row; justify-content: space-around; width: 200rpx; } .top-view .site{ background-color: rgba(0, 0, 0, 0.16); padding: 20rpx; border-top-left-radius: 32rpx; border-bottom-left-radius: 32rpx; }
home.js
var app = getApp(); Page({ /** * 页面的初始数据 */ data: { userInfo: null }, /** * 生命周期函数--监听页面加载(第一次打开时会执行) */ onLoad: function (options) { }, /** * 生命周期函数--监听页面初次渲染完成(第一次打开时会执行) */ onReady: function () { }, /** * 生命周期函数--监听页面显示 */ onShow: function () { //本地storage中获取值 this.setData({ userInfo: app.globalData.userInfo }) }, /** * 用户注销 */ onClickLogout:function(){ app.delUserInfo(); this.setData({ userInfo: null }) }, })
auth页面
auth.wxml
<view class="form"> <view class="row-group"> <text>手机</text> <input placeholder="请填写手机号码" placeholder-class='txt' maxlength='11' value="{{phone}}" bindinput="bindPhoneInput" /> </view> <view class="row-group"> <text>验证码</text> <input placeholder="请填写验证码" placeholder-class='txt' maxlength='4' value="{{code}}" bindinput="bindCodeInput" /> <view class="code" bindtap="onClickCheckCode">获取验证码</view> </view> <view> <button class="submit" open-type="getUserInfo" bindgetuserinfo="onClickSubmit">登录</button> </view> </view>
auth.js
// pages/auth/auth.js // 获取公共的那个app var app = getApp(); Page({ /** * 页面的初始数据 */ data: { phone:"", code:"", }, bindPhoneInput: function (e) { this.setData({ phone: e.detail.value }); }, bindCodeInput: function (e) { this.setData({ code: e.detail.value }); }, /** * 点击获取短信验证码 */ onClickCheckCode: function (e) { // 判断手机号格式是否正确 if (this.data.phone.length == 0) { wx.showToast({ title: '请填写手机号码', icon: 'none' }) return } var reg = /^(1[3|4|5|6|7|8|9])\d{9}$/; if (!reg.test(this.data.phone)) { wx.showToast({ title: '手机格式错误', icon: 'none' }) return } // 发送短信验证码,登录成功之后获取jwt和微信用户信息,保存到globalData和本地存储中。 wx.request({ url: "http://127.0.0.1:8000/api/message/", data: { phone: this.data.phone }, method: 'GET', dataType: 'json', success: function (res) { if(res.data.status){ // 倒计时计数器 wx.showToast({ title: res.data.message, icon: 'none' }); }else{ // 短信发送失败 wx.showToast({title: res.data.message,icon: 'none'}); } } }) }, onClickSubmit:function(e){ e.detail.userInfo wx.request({ url: "http://127.0.0.1:8000/api/login/", data: { phone: this.data.phone, code: this.data.code }, method: 'POST', dataType: 'json', success: function (res) { if (res.data.status) { // 初始化用户信息 app.initUserInfo(res.data.data, e.detail.userInfo); // var pages = getCurrentPages(); //获取所有page // prevPage = pages[pages.length-2]; // 跳转会上一级页面 wx.navigateBack({}); } else { wx.showToast({ title: "登录失败", icon: 'none' }); } } }) }, })
auth.wxss
.logo{ display: flex; flex-direction: column; align-items: center; } .logo image { margin-top: 140rpx; width: 216rpx; height: 100rpx; } .logo text { margin-top: 26rpx; margin-bottom: 50rpx; font-size: 24rpx; line-height: 24rpx; font-weight: 400; color: #8c8c8c; text-align: center; } .form{ padding: 40rpx; } .form .row-group{ padding: 20rpx 0; border-bottom: 1rpx solid #ddd; position: relative; } .form .row-group text{ font-size: 28rpx; padding:10rpx 0; } .form .row-group input{ padding: 10rpx 0; } .form .row-group .txt{ color: #ccc; } .form .row-group .code{ position: absolute; right: 0; bottom: 26rpx; z-index: 2; width: 206rpx; height: 60rpx; border: 2rpx solid #00c8b6; border-radius: 12rpx; font-size: 26rpx; font-weight: 400; color: #00c8b6; display: flex; align-items: center; justify-content: center; } .form .submit{ margin-top: 80rpx; color: #fff; border: 2rpx solid #00c8b6; background-color: #00c8b6; font-size: 32rpx; font-weight: bold; }
小程序后台-Django dwr
setting信息
""" Django settings for auction project. """ import os # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = '^#&=_59i4qviix7_way6!vvm)p+3i_mtw50$sbgh$1sx+q6@@f' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'api.apps.ApiConfig', #注入app 'rest_framework', #注入rest_framework ] …… WSGI_APPLICATION = 'auction.wsgi.application' # Database # https://docs.djangoproject.com/en/2.1/ref/settings/#databases DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), } } …… STATIC_URL = '/static/' TEMPLATE_DIRS = (os.path.join(BASE_DIR, 'templates'),) #redis缓存 CACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": "redis://127.0.0.1:6379", "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", "CONNECTION_POOL_KWARGS": {"max_connections": 100} # "PASSWORD": "密码", } } } TEMPLATE_ID = '5973x' TENCENT_SIGN = '个人测试' TENCENT_APP_ID = '1400364xx' TENCENT_CITY = 'ap-guangzhou' BASE_LOG_DIR = os.path.join(BASE_DIR, "log") # log服务 LOGGING = { 'version': 1, 'disable_existing_loggers': False, # 禁用已经存在的logger实例 # 日志文件的格式 'formatters': { # 详细的日志格式 'standard': { 'format': '[%(asctime)s][%(threadName)s:%(thread)d][task_id:%(name)s][%(filename)s:%(lineno)d]' '[%(levelname)s][%(message)s]' }, # 简单的日志格式 'simple': { 'format': '[%(levelname)s][%(asctime)s][%(filename)s:%(lineno)d]%(message)s' }, }, 'handlers': { # 在终端打印 'console':{ 'level':'DEBUG', 'class':'logging.StreamHandler', 'formatter': 'simple' }, # 默认的 'default': { 'level': 'INFO', 'class': 'logging.handlers.RotatingFileHandler', # 保存到文件,自动切 'filename': os.path.join(BASE_LOG_DIR, "auction_info.log"), # 日志文件 'maxBytes': 1024 * 1024 * 50, # 日志大小 50M 'backupCount': 3, # 最多备份几个 'formatter': 'standard', 'encoding': 'utf-8', }, # 专门用来记错误日志 'error': { 'level': 'ERROR', 'class': 'logging.handlers.RotatingFileHandler', # 保存到文件,自动切 'filename': os.path.join(BASE_LOG_DIR, "auction_err.log"), # 日志文件 'maxBytes': 1024 * 1024 * 50, # 日志大小 50M 'backupCount': 5, 'formatter': 'standard', 'encoding': 'utf-8', }, }, 'loggers': { # 默认的logger应用如下配置 '': { 'handlers': ['default', 'console', 'error'], # 上线之后可以把'console'移除 'level': 'DEBUG', 'propagate': True, # 向不向更高级别的logger传递 }, } }
主urls.py
from django.contrib import admin from django.urls import path,include,re_path,register_converter urlpatterns = [ path('admin/', admin.site.urls), # 路由配置 # re_path(r'^articles/2003/$', views.special_case_2003), # re_path(r'^articles/([0-9]{4})/([0-9]{2})/$', views.month_archive), # 传递位置参数 # # 有名分组(就是给分组起个名字,这样定义的好处就是按照关键字参数去传参了,指名道姓的方式) # re_path(r'^articles/(?P<year>[0-9]{4})/$', views.year_archive), re_path(r"api/", include(('api.urls', 'api'))), # 分发 ]
应用urls.py
from django.contrib import admin from django.urls import path,include,re_path from api import views urlpatterns = [ path('admin/', admin.site.urls), # 路由配置 # re_path(r'^articles/2003/$', views.special_case_2003), # re_path(r'^articles/([0-9]{4})/([0-9]{2})/$', views.month_archive), # 传递位置参数 # # 有名分组(就是给分组起个名字,这样定义的好处就是按照关键字参数去传参了,指名道姓的方式) # re_path(r'^articles/(?P<year>[0-9]{4})/$', views.year_archive), re_path(r"login/", views.LoginView.as_view()), # 分发 re_path(r"message/", views.MessageView.as_view()), # 分发 ]
views.py
import uuid import random from rest_framework.views import APIView from rest_framework.response import Response from django_redis import get_redis_connection from api import models from api.utils.tencent.msg import send_message from api.serializer.account import MessageSerializer, LoginSerializer from common.Auc_Log import auc_logger # Create your views here. class LoginView(APIView): def post(self, request, *args, **kwargs): """ 1. 校验手机号是否合法 2. 校验验证码,redis - 无验证码 - 有验证码,输入错误 - 有验证码,成功 4. 将一些信息返回给小程序 """ ser = LoginSerializer(data=request.data) if not ser.is_valid(): return Response({"status": False, 'message': '验证码错误'}) # 3. 去数据库中获取用户信息(获取/创建) phone = ser.validated_data.get('phone') # user = models.UserInfo.objects.filter(phone=phone).first() # auc_logger.info(user.token) user_object, flag = models.UserInfo.objects.get_or_create(phone=phone) print(user_object,flag) user_object.token = str(uuid.uuid4()) user_object.save() return Response({"status": True, "data": {"token": user_object.token, 'phone': phone}}) # Create your views here. class MessageView(APIView): def get(self, request, *args, **kwargs): """ 发送手机短信验证码 :param request: :param args: :param kwargs: :return: """ # 1.获取手机号 # 2.手机格式校验 ser = MessageSerializer(data=request.query_params) if not ser.is_valid(): return Response({'status': False, 'message': '手机格式错误'}) phone = ser.validated_data.get('phone') # 3.生成随机验证码 random_code = random.randint(1000, 9999) # 5.把验证码+手机号保留(60s过期) auc_logger.info('random_code={}'.format(random_code)) # result = send_message(phone, random_code) # if not result: # return Response({"status": False, 'message': '短信发送失败'}) """ # 5.1 搭建redis服务器(云redis) # 5.2 django中方便使用redis的模块 django-redis 配置: CACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": "redis://127.0.0.1:6379", "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", "CONNECTION_POOL_KWARGS": {"max_connections": 100} # "PASSWORD": "密码", } } } 使用: """ conn = get_redis_connection() conn.set(phone, random_code, ex=60) return Response({"status": True, 'message': '发送成功'})
用户校验:account.py
from rest_framework import serializers from rest_framework.exceptions import ValidationError from django_redis import get_redis_connection from .validators import phone_validator class MessageSerializer(serializers.Serializer): phone = serializers.CharField(label='手机号',validators=[phone_validator,]) class LoginSerializer(serializers.Serializer): phone = serializers.CharField(label='手机号', validators=[phone_validator, ]) code = serializers.CharField(label='短信验证码') # 钩子函数 def validate_code(self, value): if len(value) !=4: raise ValidationError('短信格式错误') if not value.isdecimal(): raise ValidationError('短信格式错误') phone = self.initial_data.get('phone') conn = get_redis_connection() code = conn.get(phone) if not code: raise ValidationError('验证码过期') if value != code.decode('utf-8'): raise ValidationError('验证码错误') return value
发短信: msg.py
from tencentcloud.common import credential from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException from tencentcloud.sms.v20190711 import sms_client, models from django.conf import settings import os from common.Auc_Log import auc_logger def send_message(phone,random_code): """ 发送短信验证码 验证码发送到手机上,购买服务器进行发送短信:腾讯云 1.注册腾讯云,开通腾讯云短信。 2.创建应用 SDK AppID = 1400302209 3.申请签名(个人:公众号) ID 名称 260514 Python之路 4.申请模板 ID 名称 516680 miniprogram 5.申请腾讯云API https://console.cloud.tencent.com/cam/capi SecretId: SecretKey: 6.调用相关接口去发送短信 https://cloud.tencent.com/document/product/382/38778 SDK,写好的工具。 """ try: phone = "{}{}".format("+86", phone) cred = credential.Credential(os.environ.get("secretId"), os.environ.get("secretKey")) client = sms_client.SmsClient(cred, settings.TENCENT_CITY) req = models.SendSmsRequest() req.SmsSdkAppid = settings.TENCENT_APP_ID # 短信签名内容: 使用 UTF-8 编码,必须填写已审核通过的签名,签名信息可登录 [短信控制台] 查看 req.Sign = settings.TENCENT_SIGN # 示例如:+8613711112222, 其中前面有一个+号 ,86为国家码,13711112222为手机号,最多不要超过200个手机号 req.PhoneNumberSet = [phone, ] # 模板 ID: 必须填写已审核通过的模板 ID。模板ID可登录 [短信控制台] 查看 req.TemplateID = settings.TEMPLATE_ID # 模板参数: 若无模板参数,则设置为空 req.TemplateParamSet = [random_code, ] resp = client.SendSms(req) auc_logger.info(resp) # 输出json格式的字符串回包 if resp.SendStatusSet[0].Code == "Ok": return True except TencentCloudSDKException as err: pass
model.py
from django.db import models # Create your models here. class UserInfo(models.Model): phone = models.CharField(verbose_name='手机号',max_length=11,unique=True) token = models.CharField(verbose_name='用户token',max_length=64, null=True,blank=True)
手机验证:validators.py
import re from rest_framework.exceptions import ValidationError def phone_validator(value): if not re.match(r"^(1[3|4|5|6|7|8|9])\d{9}$",value): raise ValidationError('手机格式错误')
log服务Auc_Log.py
import logging # 生成一个以当前文件名为名字的logger实例 logger = logging.getLogger(__name__) # 生成一个名为auction的logger实例 auc_logger = logging.getLogger("auction")