主要内容
# 登录注册功能
# 接口分析
-1 账号密码登录
-多方式登录接口
-校验手机号是否存在接口 (短信登录和注册,都需要校验手机号是否存在)
-2 短信登录
-发送短信接口
-阿里短信
-腾讯短信平台(以它为例)
-短信登录接口
-3 短信注册
-短信注册接口
1 登录注册页面(前端)
# 要点:
1.以模态框的形式弹出 登录与注册页面 (页面绝对定位 + 覆盖页面的 css样式)
2.点击关闭按钮,存在子组件向父组件通信 (通过自定义事件,子组件触发父组件的事件执行)
<template>
<div class="login">
<span @click="close_login">X</span>
</div>
</template>
<script>
export default {
name: "Login",
methods:{
close_login(){
//子传父组件 this.$emit,给父组件传递一个事件
this.$emit("close")
},
},
}
</script>
<style scoped>
.login{
width: 100vw;
height: 100vw;
position: fixed;
left: 0;
top: 0;
z-index: 666;
background-color: rgba(0,0,0,0.3);
}
span{
font-size: 30px;
cursor: pointer;
}
</style>
1.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">登录</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">登录</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);
}
}
}
</script>
<style scoped>
.login {
width: 100vw;
height: 100vh;
position: fixed;
top: 0;
left: 0;
z-index: 10;
background-color: rgba(0, 0, 0, 0.3);
}
.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>
1.2 Register.vue
<template>
<div class="register">
<div class="box">
<i class="el-icon-close" @click="close_register"></i>
<div class="content">
<div class="nav">
<span class="active">新用户注册</span>
</div>
<el-form>
<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-key"
v-model="password"
clearable
show-password>
</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">注册</el-button>
</el-form>
<div class="foot">
<span @click="go_login">立即登录</span>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Register",
data() {
return {
mobile: '',
password: '',
sms: '',
sms_interval: '获取验证码',
is_send: false,
}
},
methods: {
close_register() {
this.$emit('close', false)
},
go_login() {
this.$emit('go')
},
// 用到在看
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);
}
}
}
</script>
<style scoped>
.register {
width: 100vw;
height: 100vh;
position: fixed;
top: 0;
left: 0;
z-index: 10;
background-color: rgba(0, 0, 0, 0.3);
}
.box {
width: 400px;
height: 480px;
background-color: white;
border-radius: 10px;
position: relative;
top: calc(50vh - 240px);
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-left: 90px;
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>
<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>
<span @click="put_login">登录</span>
<span class="line">|</span>
<span @click="put_register">注册</span>
</div>
</div>
<!-- v-if 再次刷新为true时,无法渲染出来-->
<Login v-if="is_login" @close="close_login" @go="put_register"></Login>
<Register v-if="is_register" @close="close_register" @go="put_login"></Register>
</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,
}
},
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;
},
close_register() {
this.is_register = false;
}
},
created() {
// 前端存数据的地方:cookie中:过期时间,借助第三方, sessionStorage:临时存储,页面关闭就没了 localStorage:永久存储,除非客户清理
// sessionStorage.name = 'lqz';
// localStorage.age='19'
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>
2 多方式登录接口
# 使用用户名,邮箱,手机号+密码登录
{username:lqz/132834355/33@qq.com,password:12345}
class LoginView(ViewSet):
def login(self, request):
# 逻辑写在序列化类中
# 1.用if判断的形式,返回数据
# if ser.is_valid():
# token = ser.context.get('token')
# username = ser.context.get('user').username
# return APIResponse(msg='登录成功', token=token, username=username)
# else:
# # return APIResponse(code=101, msg=ser.errors)
# return APIResponse(code=101, msg='用户名或密码错误')
# 2.以序列化器校验 抛异常的形式 返回 (更简洁和常用)
ser.is_valid(raise_exception=True)
token = ser.context.get('token')
username = ser.context.get('user').username
return APIResponse(msg='登录成功', token=token, username=username)
# 序列化类
# 用来: 反序列化,验证前端传入的字段是否合法
class LoginSerializer(serializers.ModelSerializer):
# 坑
'''
username:由于映射了User表的Username,自己有规则,unique=True
username字段自己的校验,会走unique,会去数据库查是否有这个用户,如果有,直接抛异常
故重写这个字段,不设置规则
'''
username = serializers.CharField(max_length=16, min_length=3)
class Meta:
model = models.User
fields = ['username', 'password']
def validate(self, attrs):
'''
1 根据用户名(手机号,邮箱)获取用户
2 如果用户存在,签发token
3 把token放到序列化类的对象中
'''
user = self._get_user(attrs)
token = self._get_token(user)
# 上下文,是个字典
self.context['token']=token
self.context['username']=user.username
return attrs
def _get_user(self, attrs): # '_'表示不是私有,但尽量只给内部用
username = attrs.get('username')
password = attrs.get('password')
# 多种登录方式,正则匹配,如果是手机号根据手机号查。。。
# ^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$
if re.match(r'^1[3-9][0-9]{9}$', username):
user=models.User.objects.filter(mobile=username).first()
# elif re.match(r'[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$',username):
elif re.match(r'^.+@.+$',username):
user = models.User.objects.filter(email=username).first()
else:
user = models.User.objects.filter(username=username).first()
if user and user.check_password(password):
return user
else:
raise ValidationError('用户名或密码错误')
def _get_token(self, user):
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
return token
3 手机号是否存在接口
class MobileView(ViewSet):
# api/v1/user/mobile/?mobile=12334454
# url_path的作用,自动生成的url路径,默认为空,则以函数名为路径
# @action(methods=['GET'], detail=False,url_path='mobile')
# def check_mobile(self, request):
@action(methods=['GET'], detail=False)
def mobile(self, request):
mobile = request.query_params.get('mobile')
try:
models.User.objects.get(mobile=mobile)
except Exception:
raise APIException('手机号不存在')
return APIResponse(is_exisit=True)
4 腾讯云短信开发
# 申请公众号
-一个邮箱只能注册一个(公众号和小程序)
-https://mp.weixin.qq.com/ 小程序和公众号放到一起了
-按照流程一步步注册
-登录,进入到账号详情
# 集成腾讯短信服务---》按照文档操作
# https://cloud.tencent.com/product/sms
# 注册--》登录---》短信服务
# https://console.cloud.tencent.com/smsv2/csms-template
# 使用步骤
1.创建短信签名
2.创建短信正文模板
等待审核
3.创建应用
4.发送短信
# 刚申请,送100条
# API和SDK的区别
-API:指一堆接口,http/https接口---》写起来比较麻烦
-sdk:使用不同语言基于这些接口封装的代码,java,python,nodejs版本sdk
-python的sdk:V2版本,V3版本
-咱们用V2:qcloudsms_py,只用来发短信
-https://cloud.tencent.com/document/product/382/11672
-新版本V3:tencentcloud-sdk-python,不仅包含了短信功能,还包含了其他腾讯云功能:对象存储...
-https://cloud.tencent.com/document/product/382/56059
-第三方服务肯定会有api,sdk看公司情况
-支付宝支付-->有api,但是没有sdk(第三方


4.1 腾讯云v2.0 单发短信
from qcloudsms_py import SmsSingleSender
from qcloudsms_py.httpclient import HTTPError
# 短信应用 SDK AppID
appid = 1400000000 # SDK AppID 以1400开头
# 短信应用 SDK AppKey
appkey = "fd972f6d5a15add46de47b50b8dbe930"
# 需要发送短信的手机号码
phone_numbers = ["18953670000", ]
# 短信模板ID,需要在短信控制台中申请
template_id = 1049981 # NOTE: 这里的模板 ID`7839` 只是示例,真实的模板 ID 需要在短信控制台中申请
# 签名
sms_sign = "小猿取经" # NOTE: 签名参数使用的是`签名内容`,而不是`签名ID`。这里的签名"腾讯云"只是示例,真实的签名需要在短信控制台中申请
ssender = SmsSingleSender(appid, appkey)
params = ["5678", '3'] # 当模板没有参数时,`params = []`
try:
result = ssender.send_with_param(86, phone_numbers[0],
template_id, params, sign=sms_sign, extend="", ext="")
except HTTPError as e:
print(e)
except Exception as e:
print(e)
print(result)
4.2 腾讯云v3.0 单发短信
from tencentcloud.common import credential
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
from tencentcloud.sms.v20210111 import sms_client, models
from .settings import *
try:
# 用户API密钥 secretId,secretKey
cred = credential.Credential(SECRET_ID, SECRET_KEY)
client = sms_client.SmsClient(cred, "ap-guangzhou")
req = models.SendSmsRequest()
req.SmsSdkAppId = SDK_APP_ID # 短信应用 SDK AppID
req.SignName = SMS_SIGN # 签名 公众号名字
req.TemplateId = TEMPLATE_ID # 短信模板ID
req.TemplateParamSet = [random_code] # 参数:验证码
req.PhoneNumberSet = ["+86"+phone] # 需要发送短信的手机号码
resp = client.SendSms(req)
# 输出json格式的字符串回包
print(resp.to_json_string(indent=2))
except TencentCloudSDKException as err:
print(err)