luffy值短信登录接口、注册接口
复习
# 1 pycharm 操作git
# fetch和pull的区别 :fetch+merge=pull
# 2 给开源项目贡献代码
# fork代码---》clone下来改就行了---》提交pr---》同意--》贡献者
# 3 登录注册 前端页面
# 4 5个接口
-多方式登录 #手机号/邮箱/用户名
-手机号是否存在 #验证手机号是否在数据库存在
-发送短信 #点发送短信
-短信登录 #短信登录
-短信注册 #短信注册
# 5 手机号是否存在
try:
pass
except:
raise
return Response()
# 6 多方式登录
-前端数据:{username:用户名/手机号/邮箱,password:密码}
-序列化类中---》视图类中调用 ser.is_valid--->执行字段自己的规则,局部钩子,全局钩子---》登录(通过正则匹配),查出用户,校验密码是否正确--->通过用户签发token---》context---》头像处理(imageField)
今日内容
1 腾讯云发送短信二次封装
# 腾讯云短信---》https://console.cloud.tencent.com/smsv2/csms-template
# 步骤:
-创建签名
-创建模板
-发送短信 # 用代码写个方法发送短信 调用他的接口 他给某个手机发短信
# API和SDK
-api接口:http接口
其实就是别人已经写好的可以实现特定功能的函数,而你只需要根据他提供好的接口,也就是调用他的方法,传入他规定的参数,然后这个函数就会帮你实现这些功能。
-sdk:不同语言对api的封装,更方便的使用提供的服务 #软件开发工具包 底层也是封装了api
# 腾讯云短信的sdk
-老版本:2.0 :只针对于发短信(qcloudsms_py):https://cloud.tencent.com/document/product/382/11672
-新版本:3.0:不仅仅针对于发短信(tencentcloud-sdk-python):https://cloud.tencent.com/document/product/382/43196
# 注意:python3.8以上,2.0用起来会有问题, 所以直接用3.0
# 原因用了内置的json模块
# 如何解决
# json:3.6以前是不能序列化bytes格式的
libs/sms/settings.py
APP_ID = ''
APP_KEY = ''
TEMPLATE_ID = ''
SMS_SIGN = ''
SECRET_ID = ''
SECRET_KEY = ''
libs/sms/init.py
from .v2 import send_sms as send_sms_v2
from .v3 import send_sms as send_sms_v3
from .code import gen_code
libs/sms/code.py
import random
# 生成4,或5位的验证码
def gen_code(length=4):
code_str = ''
for i in range(length):
code_str += str(random.randrange(0, 9)) # python 是动态强类型,所以需要转换
return code_str
libs/sms/v2.py
from qcloudsms_py import SmsSingleSender
from qcloudsms_py.httpclient import HTTPError
from . import settings
from utils.logging import logger
def send_sms(phone, code, exp=2):
ssender = SmsSingleSender(settings.APP_ID, settings.APP_KEY)
params = [code, exp]
try:
result = ssender.send_with_param(86, phone, settings.TEMPLATE_ID, params, sign=settings.SMS_SIGN, extend="",
ext="")
if result['result'] == 0:
return True
else:
logger.error('发送短信失败,失败原因为:%s' % result['errmsg'])
return False
except Exception as e:
logger.error('发送短信失败,失败原因为:%s' % str(e))
return False
libs/sms/v3.py
from . import settings
def send_sms(phone, code, exp='2'):
# -*- coding: utf-8 -*-
from tencentcloud.common import credential
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
# 导入对应产品模块的client models。
from tencentcloud.sms.v20210111 import sms_client, models
# 导入可选配置类
from tencentcloud.common.profile.client_profile import ClientProfile
from tencentcloud.common.profile.http_profile import HttpProfile
try:
cred = credential.Credential(settings.SECRET_ID, settings.SECRET_KEY)
httpProfile = HttpProfile()
httpProfile.reqMethod = "POST" # post请求(默认为post请求)
httpProfile.reqTimeout = 30 # 请求超时时间,单位为秒(默认60秒)
httpProfile.endpoint = "sms.tencentcloudapi.com" # 指定接入地域域名(默认就近接入)
# 非必要步骤:
# 实例化一个客户端配置对象,可以指定超时时间等配置
clientProfile = ClientProfile()
clientProfile.signMethod = "TC3-HMAC-SHA256" # 指定签名算法
clientProfile.language = "en-US"
clientProfile.httpProfile = httpProfile
# 实例化要请求产品(以sms为例)的client对象
# 第二个参数是地域信息,可以直接填写字符串ap-guangzhou,支持的地域列表参考 https://cloud.tencent.com/document/api/382/52071#.E5.9C.B0.E5.9F.9F.E5.88.97.E8.A1.A8
client = sms_client.SmsClient(cred, "ap-guangzhou", clientProfile)
# 实例化一个请求对象,根据调用的接口和实际情况,可以进一步设置请求参数
# 你可以直接查询SDK源码确定SendSmsRequest有哪些属性可以设置
# 属性可能是基本类型,也可能引用了另一个数据结构
# 推荐使用IDE进行开发,可以方便的跳转查阅各个接口和数据结构的文档说明
req = models.SendSmsRequest()
# 基本类型的设置:
# SDK采用的是指针风格指定参数,即使对于基本类型你也需要用指针来对参数赋值。
# SDK提供对基本类型的指针引用封装函数
# 帮助链接:
# 短信控制台: https://console.cloud.tencent.com/smsv2
# sms helper: https://cloud.tencent.com/document/product/382/3773
# 短信应用ID: 短信SdkAppId在 [短信控制台] 添加应用后生成的实际SdkAppId,示例如1400006666
req.SmsSdkAppId = settings.APP_ID
# 短信签名内容: 使用 UTF-8 编码,必须填写已审核通过的签名,签名信息可登录 [短信控制台] 查看
req.SignName = settings.SMS_SIGN
# 短信码号扩展号: 默认未开通,如需开通请联系 [sms helper]
req.ExtendCode = ""
# 用户的 session 内容: 可以携带用户侧 ID 等上下文信息,server 会原样返回
req.SessionContext = "xxx"
# 国际/港澳台短信 senderid: 国内短信填空,默认未开通,如需开通请联系 [sms helper]
req.SenderId = ""
# 下发手机号码,采用 E.164 标准,+[国家或地区码][手机号]
# 示例如:+8613711112222, 其中前面有一个+号 ,86为国家码,13711112222为手机号,最多不要超过200个手机号
req.PhoneNumberSet = ["+86" + phone, ]
# 模板 ID: 必须填写已审核通过的模板 ID。模板ID可登录 [短信控制台] 查看
req.TemplateId = settings.TEMPLATE_ID
# 模板参数: 若无模板参数,则设置为空
req.TemplateParamSet = [code, exp]
# 通过client对象调用DescribeInstances方法发起请求。注意请求方法名与请求对象是对应的。
# 返回的resp是一个DescribeInstancesResponse类的实例,与请求对象对应。
resp = client.SendSms(req)
# 输出json格式的字符串回包
import json
res_dict = json.loads(resp.to_json_string(indent=2))
print(res_dict)
if res_dict['SendStatusSet'][0]['Code'] == 'Ok':
return True
else:
return False
except TencentCloudSDKException as err:
print(err)
return False
2 发送短信(验证码)接口
class UserView(ViewSet):
@action(methods=['GET'], detail=False)
def send_sms(self, request):
from libs.sms import gen_code, send_sms_v3
# 从请求url获取phone
phone = request.query_params.get('phone')
#调用gen_code获得随机字符串
sms_code = gen_code()
#验证码不只是需要发送还需要保存起来 放到缓存,默认放到内存中
#设置缓存
cache.set(settings.SMS_CODE_CACHE%phone,sms_code)
# 调用方法获得发送短信
res = send_sms_v3(phone, sms_code)
print(sms_code)
if res:
return APIResponse()
else:
return APIResponse(code=101, msg='发送失败,请稍后再试')
3 短信登录接口
# 前端传入(post):{mobile:1892322345,code:1234}
界面如下
3.1 serializer.py
class MobileSerializer(serializers.ModelSerializer):
# code字段不是表字段,需要重写 他是验证码字段
code = serializers.CharField(max_length=5, min_length=4)
mobile= serializers.CharField()
class Meta:
model = User
# 往里走--->反序列化校验:username和password---》只是校验长短
# 往外走--->序列化---》username,icon
fields = ['mobile', 'code']
# 不需要序列化 所以不用这个
# extra_kwargs = {
# 'password': {'write_only': True},
# 'icon': {'read_only': True}
# }
def validate(self, attrs):
# 1.检验code是否正确
self._check_code(attrs)
# 2.根据手机号查用户
user=self._get_user_by_mobile(attrs)
# 3.签发token
token=_get_token(user)
# 4.把token 放入到当前对象中给view用
self.context['token'] = token
self.context['username'] = user.username
# self.context['icon']=user.icon # icon的有点问题
# 这个地址是服务端地址,服务端地址从request对象中可以取出request.META['HTTP_HOST']
request = self.context.get('request')
self.context['icon'] = 'http://%s/media/' % request.META['HTTP_HOST'] + str(user.icon)
return attrs
#检查code验证码是否正确
def _check_code(self, attrs):
# 发送验证码接口还要存一份验证码 和注册的手机号 方便后期验证
code = attrs.get('code')
# 取出该手机号对应的code
mobile = attrs.get('mobile')
old_code = cache.get(settings.SMS_CODE_CACHE % mobile)
#比较code
if not old_code==code:
raise ValidationError('验证码错误')
#判断用户是否存在
def _get_user_by_mobile(self,attrs):
mobile=attrs.get('mobile')
user=User.objects.filter(mobile=mobile).first()
if user:
return user
else:
raise ValidationError('用户不存在')
#由于token获取经常用到拿出来写一个方法需要就调用一下减少了代码冗余
def _get_token(user):
# 根据user获取payload
payload = jwt_payload_handler(user)
# 根据payload得到token
token = jwt_encode_handler(payload);
return token
3.2 views.py
class LoginView(GenericViewSet):
queryset = User.objects.all()
serializer_class = LoginSerializer
#重写get_serializer_class方法 get serializer从这里取值
def get_serializer_class(self):
print(self.action)
if self.action == 'mul_login':
return self.LoginSerializer
else:
return MobileSerializer
@action(methods=['POST'], detail=False, ) # user/login/mobile_login
def mobile_login(self, request):
return self._login(request)
# 代码冗余抽出来写成一个方法
def _login(self, request):
try:
# 校验规则和签发token都写到序列化类中,获取序列化类获取的不对 所以需要
ser = self.get_serializer(data=request.data, context={'request': request})
# 方式一
# ser=MobileSerializer(data=request.data, context={'request': request})
# 方式一 重写get_serializer
ser.is_valid(raise_exception=True) # 走序列化类的字段自己的规则,局部钩子和全局钩子
token = ser.context.get('token')
username = ser.context.get('username')
icon = ser.context.get('icon')
except Exception as e:
raise APIException(str(e))
return APIResponse(token=token, username=username, icon=icon)
3.3 cache的使用
from django.core.cache import cache
cache.set(key,value) #存
cache.get(key) #取
4 短信注册接口
# 前端传入的 {mobile:1234,code:1234,password:1234} 手机号 验证码 密码(这个有的网站没有)
# 自动给他设置一个密码 或者没有密码可以登陆之后修改密码
# 新增--》User表中插入数据,username唯一,干脆手机号就是用户名
界面如下
4.1 serializer.py
class RegisterSerializer(serializers.ModelSerializer,Code_Token):
# code 字段不存在 需要重写 不然
code = serializers.CharField(write_only=True)
class Meta:
model = User
fields = ['mobile', 'code', 'password', 'username']
extra_kwargs = {
'username': {'read_only': True},
'password': {'write_only': True}
}
def validate(self, attrs):
# 1.校验手机号是否合法 没有必要去校验 因为如果手机号不合法 他在缓存取出code一对比就失败了code就已经不合法了,写上也没问题
# 2.验证code
self._check_code(attrs)
# 3.创建出username 不然怎么存数据库
# attrs{'mobile':xxx,'password':123,'username':xxx,'code':1234}
attrs['username'] = attrs.get('mobile')
# 4.踢出code 因为code不是表字段
attrs.pop('code')
return attrs
# 因为在父类里 save方法触发 如果是新增触发create 修改触发update 所以自己需要重写create
# validated_data 校验成功的数据
def create(self, validated_data):
# 重写目的是因为密码是秘文 传过来的是明文 需要使用create_user
user = User.objects.create_user(**validated_data)
print(user)
return user
4.2 views.py
# 短信注册接口 继承GenericViewSet为了自动生成路由 CreateModelMixin为了create方法 写入数据库
class RegisterView(GenericViewSet,CreateModelMixin):
# 数据库
queryset = User.objects.all()
# 序列化类
serializer_class = RegisterSerializer
# 重写create
def create(self, request, *args, **kwargs):
# 父类返回的是return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
# 父类create里 self.perform_create(serializer) 会调用save方法
# save方法 会触发 新增触发create 修改触发update方法 所以序列化类重写create
res = super().create(request, *args, **kwargs)
print(res)
# data=父类的serializer.data
return APIResponse(msg='注册成功')
# data是一个字典 有mobile password等
5 登录注册前端完成(账号密码登录和短信验证码方式登录)
前端存的三个位置
1 cookie中
2 localStorage 3 sessionStorage
cookie 可以设置过期时间,
需要借助于第三方模块,vue-cookies
-cnpm install vue-cookies
-main.js中 设置
import cookies from 'vue-cookies'
Vue.prototype.$cookies = cookies;
localStorage:永久存储
sessionStorage:关闭浏览器就失效 清空
5.1 Login.vue
<template>
<div class="login">
<div class="box">
<i class="el-icon-close" @click="close_login"></i>
<div class="content">
<div class="nav">
<span :class="{active: login_method === 'is_pwd'}"
@click="change_login_method('is_pwd')">密码登录</span>
<span :class="{active: login_method === 'is_sms'}"
@click="change_login_method('is_sms')">短信登录</span>
</div>
<el-form v-if="login_method === 'is_pwd'">
<el-input
placeholder="用户名/手机号/邮箱"
prefix-icon="el-icon-user"
v-model="username"
clearable>
</el-input>
<el-input
placeholder="密码"
prefix-icon="el-icon-key"
v-model="password"
clearable
show-password>
</el-input>
<el-button type="primary" @click="mul_login">登录</el-button>
</el-form>
<el-form v-if="login_method === 'is_sms'">
<el-input
placeholder="手机号"
prefix-icon="el-icon-phone-outline"
v-model="mobile"
clearable
@blur="check_mobile">
</el-input>
<el-input
placeholder="验证码"
prefix-icon="el-icon-chat-line-round"
v-model="sms"
clearable>
<template slot="append">
<span class="sms" @click="send_sms">{{ sms_interval }}</span>
</template>
</el-input>
<el-button type="primary" @click="sms_login">登录</el-button>
</el-form>
<div class="foot">
<span @click="go_register">立即注册</span>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Login",
data() {
return {
username: '',
password: '',
mobile: '',
sms: '',
login_method: 'is_pwd',
sms_interval: '获取验证码',
is_send: false,
}
},
methods: {
close_login() {
this.$emit('close')
},
go_register() {
this.$emit('go')
},
change_login_method(method) {
this.login_method = method;
},
check_mobile() {
if (!this.mobile) return;
if (!this.mobile.match(/^1[3-9][0-9]{9}$/)) {
this.$message({
message: '手机号有误',
type: 'warning',
duration: 1000,
onClose: () => {
this.mobile = '';
}
});
return false;
}
this.is_send = true;
},
send_sms() {
if (!this.is_send) return;
this.is_send = false;
let sms_interval_time = 60;
this.sms_interval = "发送中...";
let timer = setInterval(() => {
if (sms_interval_time <= 1) {
clearInterval(timer);
this.sms_interval = "获取验证码";
this.is_send = true; // 重新回复点击发送功能的条件
} else {
sms_interval_time -= 1;
this.sms_interval = `${sms_interval_time}秒后再发`;
}
}, 1000);
this.$axios.get(this.$settings.base_url + 'user/userinfo/send_sms/?phone=' + this.mobile).then(res => {
if (res.data.code == 100) {
this.$message({
message: '发送成功',
type: 'success'
});
} else {
this.$message({
message: '发送失败,请稍后再试',
type: 'error'
});
}
})
},
mul_login() {
if (this.username && this.password) {
// 发送axios请求
this.$axios.post(this.$settings.base_url + 'user/login/mul_login/',
{'username': this.username, 'password': this.password}).then(res => {
console.log(res.data)
if (res.data.code == 100) {
// token,username,存起来(存到哪?)
// 1 cookie中 2 localStorage 3 sessionStorage
// localStorage.setItem('name', 'lqz')
// sessionStorage.setItem('name', 'pyy')
this.$cookies.set('token', res.data.token, '7d')
this.$cookies.set('username', res.data.username, '7d')
// 关闭模态框
this.$emit("close")
} else {
this.$message({
message: '用户名或密码错误',
type: 'error'
});
this.username = ''
}
})
} else {
this.$message({
message: '用户名或密码不能为空',
type: 'warning'
});
}
},
sms_login() {
if (this.mobile && this.sms) {
// 发送axios请求
this.$axios.post(this.$settings.base_url + 'user/login/mobile_login/',
{'mobile': this.mobile, 'code': this.sms}).then(res => {
console.log(res.data)
if (res.data.code == 100) {
this.$cookies.set('token', res.data.token, '7d')
this.$cookies.set('username', res.data.username, '7d')
// 关闭模态框
this.$emit("close")
} else {
this.$message({
message: '用户名或密码错误',
type: 'error'
});
this.username = ''
}
})
} else {
this.$message({
message: '手机号或验证码不能为空',
type: 'warning'
});
}
}
}
}
</script>
<style scoped>
.login {
width: 100vw;
height: 100vh;
position: fixed;
top: 0;
left: 0;
z-index: 10;
background-color: rgba(0, 0, 0, 0.5);
}
.box {
width: 400px;
height: 420px;
background-color: white;
border-radius: 10px;
position: relative;
top: calc(50vh - 210px);
left: calc(50vw - 200px);
}
.el-icon-close {
position: absolute;
font-weight: bold;
font-size: 20px;
top: 10px;
right: 10px;
cursor: pointer;
}
.el-icon-close:hover {
color: darkred;
}
.content {
position: absolute;
top: 40px;
width: 280px;
left: 60px;
}
.nav {
font-size: 20px;
height: 38px;
border-bottom: 2px solid darkgrey;
}
.nav > span {
margin: 0 20px 0 35px;
color: darkgrey;
user-select: none;
cursor: pointer;
padding-bottom: 10px;
border-bottom: 2px solid darkgrey;
}
.nav > span.active {
color: black;
border-bottom: 3px solid black;
padding-bottom: 9px;
}
.el-input, .el-button {
margin-top: 40px;
}
.el-button {
width: 100%;
font-size: 18px;
}
.foot > span {
float: right;
margin-top: 20px;
color: orange;
cursor: pointer;
}
.sms {
color: orange;
cursor: pointer;
display: inline-block;
width: 70px;
text-align: center;
user-select: none;
}
</style>
5.2 Header.vu
<template>
<div class="header">
<div class="slogan">
<p>老男孩IT教育 | 帮助有志向的年轻人通过努力学习获得体面的工作和生活</p>
</div>
<div class="nav">
<ul class="left-part">
<li class="logo">
<router-link to="/">
<img src="../assets/img/head-logo.svg" alt="">
</router-link>
</li>
<li class="ele">
<span @click="goPage('/free-course')" :class="{active: url_path === '/free-course'}">免费课</span>
</li>
<li class="ele">
<span @click="goPage('/actual-course')" :class="{active: url_path === '/actual-course'}">实战课</span>
</li>
<li class="ele">
<span @click="goPage('/light-course')" :class="{active: url_path === '/light-course'}">轻课</span>
</li>
</ul>
<div class="right-part">
<div v-if="username">
<span>{{username}}</span>
<span class="line">|</span>
<span @click="logout">退出</span>
</div>
<div v-else>
<span @click="put_login">登录</span>
<span class="line">|</span>
<span @click="put_register">注册</span>
<Login v-if="is_login" @close="close_login" @go="put_register"/>
<Register v-if="is_register" @close="close_register" @go="put_login"/>
</div>
</div>
</div>
</div>
</template>
<script>
import Login from "./Login";
import Register from "./Register";
export default {
name: "Header",
data() {
return {
url_path: sessionStorage.url_path || '/',
is_login: false,
is_register: false,
username: this.$cookies.get('username')
}
},
methods: {
goPage(url_path) {
// 已经是当前路由就没有必要重新跳转
if (this.url_path !== url_path) {
this.$router.push(url_path);
}
sessionStorage.url_path = url_path;
},
put_login() {
this.is_login = true;
this.is_register = false;
},
put_register() {
this.is_login = false;
this.is_register = true;
},
close_login() {
this.is_login = false;
this.username = this.$cookies.get('username')
},
close_register() {
this.is_register = false;
},
logout() {
this.$cookies.set('token', '')
this.$cookies.set('username', '')
this.username = ''
}
},
created() {
sessionStorage.url_path = this.$route.path;
this.url_path = this.$route.path;
},
components: {
Login, Register
}
}
</script>
<style scoped>
.header {
background-color: white;
box-shadow: 0 0 5px 0 #aaa;
}
.header:after {
content: "";
display: block;
clear: both;
}
.slogan {
background-color: #eee;
height: 40px;
}
.slogan p {
width: 1200px;
margin: 0 auto;
color: #aaa;
font-size: 13px;
line-height: 40px;
}
.nav {
background-color: white;
user-select: none;
width: 1200px;
margin: 0 auto;
}
.nav ul {
padding: 15px 0;
float: left;
}
.nav ul:after {
clear: both;
content: '';
display: block;
}
.nav ul li {
float: left;
}
.logo {
margin-right: 20px;
}
.ele {
margin: 0 20px;
}
.ele span {
display: block;
font: 15px/36px '微软雅黑';
border-bottom: 2px solid transparent;
cursor: pointer;
}
.ele span:hover {
border-bottom-color: orange;
}
.ele span.active {
color: orange;
border-bottom-color: orange;
}
.right-part {
float: right;
}
.right-part .line {
margin: 0 10px;
}
.right-part span {
line-height: 68px;
cursor: pointer;
}
</style>