登录注册
03-01 登录注册页面
一. 模态登录组件
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>
二. 模态注册组件
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>
1. components中新建Register.vue文件, 写入如上代码 2. components中的Header.vue中. 导入组件, 注册组件, 使用组件 3. 登录注册绑定点击事件, 为Register组件使用条件渲染指令v-if, 指定一个值is_Register控制Register组件的展示状态. 在点击z注册按钮时is_register为True 在点击登录组件中×时, Login组件应该控制传递数据, 控制父组件的展示或者隐藏. 子组件对×绑定事件的传递this.$emit('close') 5. 父组件中的Register组件中获取到传递过来的事件@close. 绑定一个父组件中的触发该事的方法, 一当该事件触发, 表示用户×了登录界面. 此时is_register因该为false.
四. 登录业务分析
1. 多方式登录

""" 1)前台提供账号密码,账号可能是 用户名、手机号、邮箱等 接口: 后台只需要提供一个多方式登录接口即可 - 多方式登录接口 """
""" 1)前台提供手机号和验证码完成登录 接口: 前台填完手机号,往后台发送校验手机号的请求,如果存在继续,不存在提示注册 - 手机号存在与否接口 前台点击发送验证码,将手机再次发送给后台,后台将手机号通知给第三方,发送短信 - 手机验证码接口 前台点击登录提交手机号与验证码,完成验证码登录 - 验证码登录接口 """
五. 注册业务分析
1. 验证码注册

""" 1)前台提供手机号、验证码、密码完成注册 接口: 前台填完手机号,往后台发送校验手机号的请求,如果不存在继续,存在提示登录 - 手机号存在与否接口 前台点击发送验证码,将手机再次发送给后台,后台将手机号通知给第三方,发送短信 - 手机验证码接口 前台点击注册提交手机号、验证码及密码,完成验证码注册 - 验证码注册接口 """
""" 1. 多方式登录接口 2. 手机号存在与否接口 3. 手机验证码接口 4. 验证码登录接口 5. 验证码注册接口 """
03-02 多方式登录
一. 后台
1. 插件
pip install djangorestframework-jwt
from django.urls import path, include from rest_framework.routers import SimpleRouter from . import views router = SimpleRouter() # '',action装饰器,自动将login作为路径 router.register('', views.LoginAPIView, 'login') urlpatterns = [ path('', include(router.urls)), ]
import datetime JWT_AUTH = { # 过期时间七天 'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7), }
import re from rest_framework.viewsets import ViewSet, GenericViewSet from rest_framework.mixins import CreateModelMixin from rest_framework.decorators import action from . import serializers from . import models from luffyapi.utils.response import APIResponse # Create your views here. class LoginView(ViewSet): # 多方式登录,jwt @action(methods=['POST'], detail=False) def login(self, request, *args, **kwargs): ser = serializers.UserInfoModelSerializer(data=request.data) if ser.is_valid(): token = ser.context['token'] username = ser.context['user_obj'].username return APIResponse(token=token, username=username) else: return APIResponse(code=0, msg=ser.errors)
import re from rest_framework import serializers from rest_framework.exceptions import ValidationError from . import models class UserInfoModelSerializer(serializers.ModelSerializer): # 提示: 因为继承了AbstractUser的表源码中username字段指定的是unique字段. 在视图中进行序列化data=request.data时, 如果不重定义 # 在序列化中就会被认为存储数据, 因为data本质就是存储数据, 而我们这里因该重定义该字段, 去除这种影响 sername = serializers.CharField() class Meta: model = models.UserInfo fields = ['username', 'password', 'id'] extra_kwargs = { 'id': {'read_only': True}, 'password': {'write_only': True}, } def validate(self, attrs): # 多种登录方式 user_obj = self._get_user(attrs) # 签发token token = self._get_token(user_obj) # 放在context中,在视图类中可以取出来 self.context['token'] = token self.context['user_obj'] = user_obj # 钩子函数,记得返回值 return attrs # 内部规范, def _get_user(self, attrs): username = attrs.get('username') password = attrs.get('password') # 多种方式登录 if re.match(r'^1[3-9][0-9]{9}$', username): user_obj = models.UserInfo.objects.filter(phone=username).first() elif re.match(r'^.*?@.*?\.com$', username): user_obj = models.UserInfo.objects.filter(email=username).first() else: user_obj = models.UserInfo.objects.filter(username=username).first() if user_obj: # 用户存在,校验密码 # 因校验的密文,需要check_password res = user_obj.check_password(password) if res: return user_obj else: raise ValidationError('密码错误') else: raise ValidationError('用户不存在') def _get_token(self, user_obj): from rest_framework_jwt.serializers import jwt_payload_handler, jwt_encode_handler payload = jwt_payload_handler(user_obj) # 通过user对象获得payload token = jwt_encode_handler(payload) # 通过payload获得token return token
二. 前台(cookies修改页面登陆状态)
<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="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">登录</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);
},
login() {
if (this.username && this.password) {
this.$axios.post(this.$settings.base_url + '/user/login/', {
'username': this.username,
'password': this.password,
}).then(response => {
console.log(response.data);
// 把用户信息保存到cookie中
// this.$cookies.set('key','value','过期时间,按s计')
this.$cookies.set('token', response.data.token, '7d');
this.$cookies.set('username', response.data.username, '7d');
// 关闭登录窗口(子传父)
// 给父组件,Head传递一个事件,让它从cookie中取出token和username
this.$emit('close');
this.$emit('loginSuccess');
}).catch(error => {
console.log(error.response);
})
} 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.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>
<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 @click="put_login">登录</span>
<span class="line">|</span>
<span @click="put_register">注册</span>
</div>
<div v-else>
<span>{{username}}</span>
<span class="line">|</span>
<span @click="logout">注销</span>
</div>
<Login v-if="is_login" @close="close_login" @go="put_register" @loginSuccess="loginSuccess"/>
<Register v-if="is_register" @close="close_register" @go="put_login"/>
</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,
token: '',
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;
},
close_register() {
this.is_register = false;
},
loginSuccess() {
this.username = this.$cookies.get('username');
this.token = this.$cookies.get('token');
},
logout() {
// 把两个变量值为空
this.username = '';
this.token = '';
// 清除cookie
this.$cookies.remove('username');
this.$cookies.remove('token');
}
},
created() {
sessionStorage.url_path = this.$route.path;
this.url_path = this.$route.path;
// 当页面一创建,我就去cookie中取token和username
this.username = this.$cookies.get('username');
this.token = this.$cookies.get('token');
},
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>
03-03 手机是否存在验证接口
urls.py
router = SimpleRouter() router.register('', views.LoginAPIView, 'login') urlpatterns = [ path('', include(router.urls)), ]
class LoginView(ViewSet): # 多方式登录,jwt @action(methods=['POST'], detail=False) def login(self, request, *args, **kwargs): ser = serializers.UserInfoModelSerializer(data=request.data) if ser.is_valid(): token = ser.context['token'] username = ser.context['user_obj'].username return APIResponse(token=token, username=username) else: return APIResponse(code=0, msg=ser.errors) @action(methods=['GET'], detail=False) def verify_telephone(self, request, *args, **kwargs): # 校验手机号 telephone = request.query_params.get('telephone') if not re.match(r'^1[3-9][0-9]{9}$', telephone): return APIResponse(code=0, msg='手机号不合法 ') try: models.UserInfo.objects.get(phone=telephone) return APIResponse(result=True) except: return APIResponse(code=0, msg='手机号不存在')
03-04 腾讯云短信开发
""" 准备工作 1)创建短信应用 - 应用管理 2)申请短信签名 - 国内短信 > 签名管理 3)申请短信模块 - 国内短信 > 正文模板管理 """

""" 1)API文档,接口的使用说吧 2)SDK,基于开发语言封装的可以直接调用的功能(工具)集合 官网sdk使用文档中找到安装命令:pip install qcloudsms_py 按照sdk使用说明进行开发:https://cloud.tencent.com/document/product/382/11672 """
# 所有配置换成申请的数据 # 申请的短信应用 SDK AppID appid = 1400 # 申请的短信应用 SDK AppKey appkey = "ba81" # 申请的短信模板ID,需要在短信控制台中申请 template_id = 5447 # 申请的签名,参数使用的是`签名内容`,而不是`签名ID` sms_sign = "Owen的技术栈" from qcloudsms_py import SmsSingleSender sender = SmsSingleSender(appid, appkey) import random def get_code(): code = '' for i in range(4): code += str(random.randint(0, 9)) return code mobile = 13344556677 # 模板所需参数,和申请的模板中占位符要保持一致 code = get_code() print(code) params = [code, 5] try: result = sender.send_with_param(86, mobile, template_id, params, sign=sms_sign, extend="", ext="") if result and result.get('result') == 0: print('发送成功') except Exception as e: print('短信发送失败:%s' % e)
三. 短信服务二次封装
在libs下创造tx_sms包
初始化
from .send import send_sms from .send import make_code
# 短信应用 SDK AppID APPID = 1400123123 # SDK AppID 以1400开头 # 短信应用 SDK AppKey APPKEY = "2025dfawefwfjkiokjg984100" # 需要发送短信的手机号码 # phone_numbers = ["21212313123", "12345678902", "12345678903"] # 短信模板ID,需要在短信控制台中申请 TEMPLATE_ID = 7839 # NOTE: 这里的模板 ID`7839`只是示例,真实的模板 ID 需要在短信控制台中申请 # 签名 SMS_SIGN = "腾讯云" # NOTE: 签名参数使用的是`签名内容`,而不是`签名ID`。这里的签名"腾讯云"只是示例,真实的签名需要在短信控制台中申请
import random from qcloudsms_py import SmsSingleSender from libs.sms import settings import utils def make_code(): code_list = [] for i in range(6): code_list.append(str(random.randint(0, 9))) return ''.join(code_list) def send_sms(code, telephone): ssender = SmsSingleSender(settings.APPID, settings.APPKEY) params = [code] # 当模板没有参数时,`params = []` try: result = ssender.send_with_param(86, telephone, settings.TEMPLATE_ID, params, sign=settings.SMS_SIGN, extend="", ext="") """ 'result': 0表示成功 result: {'result': 0, 'errmsg': 'OK', 'ext': '', 'sid': '2025dfawefwfjkiokjg984100', 'fee': 1, 'isocode': 'CN'} """ # print('result:', result) if not result.get('result'): return True return False except Exception as e: utils.log.error(f'手机号:{telephone},短信发送失败,错误为:{e}') if __name__ == '__main__': test_phone = ['13333333333'] for phone in range(3): send_sms(make_code(), telephone=test_phone[0])
码缓存时间s
SMS_CACHE_TIME = 300
from rest_framework.throttling import SimpleRateThrottle class SmsThrotting(SimpleRateThrottle): scope = 'sms' def get_cache_key(self, request, view): telephone = request.query_params.get('telephone') # 唯一性 # cache_format = 'throttle_%(scope)s_%(ident)s'字符串替换 return self.cache_format % {'scope': self.scope, 'ident': telephone}
REST_FRAMEWORK = { 'DEFAULT_THROTTLE_RATES': { # 一分钟,发送一次验证码 'sms': '1/m' # key要跟类中的scope对应 }, }
from luffyapi.utils.throttlings import SmsThrotting class SendSmsView(ViewSet): # 对手机号发送验证码频率进行限制,一分钟1次 throttle_classes = [SmsThrotting, ] @action(methods=['GET'], detail=False) def send_code(self, request, *args, **kwargs): ''' 发送验证码接口 :return: ''' from luffyapi.libs.tx_sms import get_code, send_message from django.core.cache import cache from django.conf import settings telephone = request.query_params.get('telephone') # 后端校验手机号是否合法 if not re.match(r'^1[3-9][0-9]{9}$', telephone): return APIResponse(code=0, msg='手机号不合法 ') # code = get_code() code = '1234' # result = send_message(telephone, code) result = True # 验证码保存 # 'sms_cache_{}'加字符串,是为了唯一性,180,是缓存保存180s cache.set(settings.PHONE_CACHE_KEY.format(telephone), code, 180) if result: return APIResponse() else: return APIResponse(code=0, msg='验证码发送失败')
router = SimpleRouter() router.register('', views.SendSmsView, 'sms') urlpatterns = [ path('', include(router.urls)), ]
03-06 短信登录接口
user/urls.py
router = SimpleRouter() router.register('', views.LoginAPIView, 'login') urlpatterns = [ path('', include(router.urls)), ]
# 手机验证码登录 class CodeUserInfoSerializer(serializers.ModelSerializer): code = serializers.CharField() class Meta: model = models.UserInfo fields = ['phone', 'code'] def validate(self, attrs): user_obj = self._get_user(attrs) # 用户存在,签发token token = self._get_token(user_obj) self.context['token'] = token self.context['user_obj'] = user_obj return attrs def _get_user(self, attrs): """ 校验获取用户对象: 1. 判断用户手机号码是否符合格式 2. 判断用户手机号码是否存在数据中 3. 通过手机号码从缓存中获取用户的正确验证码 """ print(attrs) telephone = attrs.get('phone') print(telephone) code = attrs.get('code') # 取出原来的code cache_code = cache.get(settings.PHONE_CACHE_KEY.format(telephone)) print(cache_code) if code == cache_code: if re.match(r'^1[3-9][0-9]{9}$', telephone): user_obj = models.UserInfo.objects.filter(phone=telephone).first() if user_obj: # 把使用过的验证码删除 cache.set(settings.PHONE_CACHE_KEY.format(telephone), '') return user_obj else: raise ValidationError('用户不存在') else: raise ValidationError('手机号不合法') else: raise ValidationError('验证码错误') def _get_token(self, user_obj): from rest_framework_jwt.serializers import jwt_payload_handler, jwt_encode_handler payload = jwt_payload_handler(user_obj) # 通过user对象获得payload token = jwt_encode_handler(payload) # 通过payload获得token return token
class LoginView(ViewSet): @action(methods=['POST'], detail=False) def code_login(self, request, *args, **kwargs): ser = serializers.CodeUserInfoSerializer(data=request.data) if ser.is_valid(): token = ser.context['token'] username = ser.context['user_obj'].username return APIResponse(token=token, username=username) else: return APIResponse(code=0, msg=ser.errors)
03-07 后端短信注册接口
一. urls.py
router = SimpleRouter() router.register('register', views.RegisterAPIView, 'register') # -> actions={post: create} urlpatterns = [ path('', include(router.urls)), ]
# 注册 class UserRegisterSerilaizer(serializers.ModelSerializer): code = serializers.CharField(max_length=4, min_length=4, write_only=True) """ 提示: 如果code不指定 write_only. 在序列化的时候, 库中没有, 当然会抛出异常 Got AttributeError when attempting to get a value for field `code` on serializer `RegisterModelSerializer`. The serializer field might be named incorrectly and not match any attribute or key on the `User` instance. Original exception text was: 'User' object has no attribute 'code'. """ class Meta: model = models.UserInfo fields = ['phone', 'password', 'code','username'] extra_kwargs = { 'username': {'read_only': True}, } def validate(self, attrs): telephone = attrs.get('phone') code = attrs.get('code') cache_code = cache.get(settings.PHONE_CACHE_KEY.format(telephone)) if code == cache_code: if re.match(r'^1[3-9][0-9]{9}$', telephone): attrs['username'] = telephone attrs.pop('code') return attrs else: raise ValidationError('手机号不合法') else: raise ValidationError('验证码错误') def create(self, validated_data): user_obj = models.UserInfo.objects.create_user(**validated_data) return user_obj
# 注册 class RegisterView(GenericViewSet, CreateModelMixin): queryset = models.UserInfo.objects.all() serializer_class = serializers.UserRegisterSerilaizer def create(self, request, *args, **kwargs): response = super().create(request, *args, **kwargs) username = response.data.get('username') return APIResponse(code=1, msg='注册成功', username=username)
03-08 前台登录注册修订
一. 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="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="code_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.$axios.get(this.$settings.base_url + '/user/verify_telephone/', {
params: {
// 15870638044
'telephone': this.mobile,
}
}).then(response => {
// console.log('response.data:', response.data);
if (response.data.code) {
this.is_send = true;
} else {
this.$message({
message: '手机号码不存在!',
type: 'warning',
duration: 1000,
});
}
}).catch(error => {
this.$message({
message: '未知错误!',
type: 'warning',
});
});
},
send_sms() {
if (!this.is_send) return;
this.is_send = false;
let sms_interval_time = 60;
this.sms_interval = "发送中...";
this.$axios.get(this.$settings.base_url + '/user/send/', {
params: {
'telephone': this.mobile,
}
}).then(response => {
// console.log('request.data:', response.data);
if (response.data.code) {
this.$message({
message: '验证码发送成功!',
type: 'success',
duration: 1000,
});
} else {
this.$message({
message: '验证码发送失败!',
type: 'warning',
});
}
}).catch(error => {
});
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);
},
login() {
if (this.username && this.password) {
this.$axios.post(this.$settings.base_url + '/user/login/', {
'username': this.username,
'password': this.password,
}).then(response => {
console.log(response.data);
// 把用户信息保存到cookie中
// this.$cookies.set('key','value','过期时间,按s计')
this.$cookies.set('token', response.data.token, '7d');
this.$cookies.set('username', response.data.username, '7d');
// 关闭登录窗口(子传父)
// 给父组件,Head传递一个事件,让它从cookie中取出token和username
this.$emit('close');
this.$emit('loginSuccess');
}).catch(error => {
console.log(error.response);
})
} else {
this.$message({
message: '用户名或密码输入不能为空!',
type: 'warning',
});
}
},
code_login() {
if (this.mobile && this.sms) {
this.$axios.post(this.$settings.base_url + '/user/code_login/', {
'telephone': this.mobile,
'code': this.sms,
}).then(response => {
if (response.data.code) {
console.log(response.data);
// 获取token, 获取username
let username = response.data.username;
let token = response.data.token;
// 将获取的token和username存入cookies中
this.$cookies.set('token', token, '7d');
this.$cookies.set('username', username, '7d');
// 向父组件传递事件
this.$emit('close');
this.$emit('loginSuccess');
} else {
this.$message({
message: '未知错误!',
type: 'warning',
});
}
}).catch(error => {
this.$message({
message: '未知错误!',
type: 'warning',
});
})
} 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.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>
<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" @click="register">注册</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;
} else {
this.$axios.get(this.$settings.base_url + '/user/verify_telephone/', {
params: {
'telephone': this.mobile,
}
}).then(response => {
if (response.data.code) {
// 手机号码存在, 用户已经注册
this.$message({
message: '该手机号码已经注册!',
type: 'warning',
});
} else {
// 手机号码不存在, 让用户可以点击发送验证码
this.is_send = true;
}
}).catch(error => {
// 异常
this.$message({
message: '未知错误!',
type: 'warning',
});
})
}
},
send_sms() {
if (!this.is_send) return;
this.is_send = false;
let sms_interval_time = 60;
this.sms_interval = "发送中...";
this.$axios.get(this.$settings.base_url + '/user/send/', {
params: {
'telephone': this.mobile
}
}).then(response => {
if (response.data.code) {
// 手机号码发送成功
this.$message({
message: '验证码发送成功!',
type: 'success',
});
} else {
// 手机号码发送失败
this.$message({
message: '验证码发送失败!',
type: 'warning',
});
}
}).catch(error => {
// 异常.
this.$message({
message: '未知错误!',
type: 'warning',
});
});
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);
},
register() {
if (this.mobile && this.sms && this.password) {
this.$axios.post(this.$settings.base_url + '/user/register/', {
'telephone': this.mobile,
'code': this.sms,
'password': this.password,
}).then(response => {
if (response.data.code) {
// 注册成功, 跳转到登录页面
this.$message({
message: '注册成功!',
type: 'success',
duration: 1000,
onClose: () => {
this.go_login();
}
});
} else {
// 注册失败
this.$message({
message: '注册失败!',
type: 'warning',
});
}
}).catch(error => {
// 异常
this.$message({
message: '未知错误!',
type: 'warning',
});
})
} else {
// 以上内容有空的
this.$message({
message: '手机号码, 密码, 验证码不能含有空值!',
type: 'warning',
});
}
}
}
}
</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 v-if="!username">
<span @click="put_login">登录</span>
<span class="line">|</span>
<span @click="put_register">注册</span>
</div>
<div v-else>
<span>{{username}}</span>
<span class="line">|</span>
<span @click="logout">注销</span>
</div>
<Login v-if="is_login" @close="close_login" @go="put_register" @loginSuccess="loginSuccess"/>
<Register v-if="is_register" @close="close_register" @go="put_login"/>
</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,
token: '',
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;
},
close_register() {
this.is_register = false;
},
loginSuccess() {
this.username = this.$cookies.get('username');
this.token = this.$cookies.get('token');
},
logout() {
// 把两个变量值为空
this.username = '';
this.token = '';
// 清除cookie
this.$cookies.remove('username');
this.$cookies.remove('token');
}
},
created() {
sessionStorage.url_path = this.$route.path;
this.url_path = this.$route.path;
// 当页面一创建,我就去cookie中取token和username
this.username = this.$cookies.get('username');
this.token = this.$cookies.get('token');
},
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>


浙公网安备 33010602011771号