案例:用户使用手机号,输入验证码登录,验证码保存60s,用户登录成功后,获取用户信息。
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.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.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;
}
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")