Django框架项目之登录注册——1-登录注册页面、2 多方式登录、3-手机是否存在验证接口、4-腾讯云短信开发、5 短信验证码接口、6-短信登录接口、7-短信注册接口、8-前台登录注册修订
1-登录注册页面
前提:基于element-ui环境
模态登录组件
<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>
模态注册组件
<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="nav">
<span @click="put_login">登录</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>
</template>
<script>
import Login from "./Login";
import Register from "./Register";
export default {
name: "Nav",
data() {
return {
is_login: false,
is_register: false,
}
},
methods: {
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;
}
},
components: {
Login,
Register
}
}
</script>
<style scoped>
</style>
登录业务分析
多方式登录

"""
1)前台提供账号密码,账号可能是 用户名、手机号、邮箱等
接口:
后台只需要提供一个多方式登录接口即可 - 多方式登录接口
"""
验证码登录

"""
1)前台提供手机号和验证码完成登录
接口:
前台填完手机号,往后台发送校验手机号的请求,如果存在继续,不存在提示注册 - 手机号存在与否接口
前台点击发送验证码,将手机再次发送给后台,后台将手机号通知给第三方,发送短信 - 手机验证码接口
前台点击登录提交手机号与验证码,完成验证码登录 - 验证码登录接口
"""
注册业务分析
验证码注册

"""
1)前台提供手机号、验证码、密码完成注册
接口:
前台填完手机号,往后台发送校验手机号的请求,如果不存在继续,存在提示登录 - 手机号存在与否接口
前台点击发送验证码,将手机再次发送给后台,后台将手机号通知给第三方,发送短信 - 手机验证码接口
前台点击注册提交手机号、验证码及密码,完成验证码注册 - 验证码注册接口
"""
汇总
"""
多方式登录接口
手机号存在与否接口
手机验证码接口
验证码登录接口
验证码注册接口
"""
前台填完手机号,往后台发送校验手机号的请求,如果存在继续,不存在提示注册 - 手机号存在与否接口
前台点击发送验证码,将手机再次发送给后台,后台将手机号通知给第三方,发送短信 - 手机验证码接口
前台点击登录提交手机号与验证码,完成验证码登录 - 验证码登录接口
2 多方式登录
后台
插件
>: pip install djangorestframework-jwt
urls.py
path('login/', views.LoginViewSet.as_view({'post': 'login'})),
dev.py
import datetime
JWT_AUTH = {
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7),
}
views.py
from rest_framework.viewsets import ViewSet
from . import serializers, models
from utils.response import APIResponse
class LoginViewSet(ViewSet):
# 局部禁用认证、权限组件
authentication_classes = ()
permission_classes = ()
def login(self, request, *args, **kwargs):
serializer = serializers.LoginSerializer(data=request.data, context={'request': request})
if serializer.is_valid():
token = serializer.context.get('token')
# 原来要一个个拿信息
# username = serializer.context.get('username')
# icon = serializer.context.get('icon')
# 拿到登录用户,直接走序列化过程,将要返回给前台的数据直接序列化好给前台
user = serializer.context.get('user')
result = serializers.LoginSerializer(user, context={'request': request}).data
result['token'] = token # id,username,icon,token
return APIResponse(result=result)
return APIResponse(1, serializer.errors)
serializers.py
from rest_framework import serializers
from rest_framework import exceptions
from django.conf import settings
from . import models
class LoginSerializer(serializers.ModelSerializer):
# 覆盖,避免login校验username有数据库唯一字段约束的限制
username = serializers.CharField()
class Meta:
model = models.User
# username、password可以通过局部钩子指定详细的校验规则
fields = ('id', 'username', 'password', 'icon')
extra_kwargs = {
'id': {
'read_only': True,
},
'icon': {
'read_only': True,
},
'password': {
'write_only': True,
}
}
def validate(self, attrs):
# 多方式得到user
user = self._get_user(attrs)
# user签发token
token = self._get_token(user)
# token用context属性携带给视图类
self.context['token'] = token
''' 自己将user的信息逐个处理,传给视图
# 前台可能不仅仅只需要登录成功的token,可能还需要用户名、用户头像等
self.context['username'] = user.username
# 通过请求头格式化icon
request = self.context['request']
icon = 'http://%s%s%s' % (request.META['HTTP_HOST'], settings.MEDIA_URL, user.icon)
self.context['icon'] = icon
'''
# 将登录用户对象直接传给视图
self.context['user'] = user
return attrs
def _get_user(self, attrs):
import re
username = attrs.get('username')
if re.match(r'^1[3-9][0-9]{9}$', username):
user = models.User.objects.filter(mobile=username).first()
else:
user = models.User.objects.filter(username=username).first()
if not user:
raise exceptions.ValidationError({'username': 'username error'})
password = attrs.get('password')
if not user.check_password(password):
raise exceptions.ValidationError({'password': 'password error'})
return user
def _get_token(self, user):
from rest_framework_jwt.serializers import jwt_payload_handler, jwt_encode_handler
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
return token
前台
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">登录</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.$message({
message: '请填好账号密码',
type: 'warning',
duration: 1500
});
return false // 直接结束逻辑
}
this.$axios({
url: this.$settings.base_url + '/user/login/',
method: 'post',
data: {
username: this.username,
password: this.password,
}
}).then(response => {
let username = response.data.result.username;
let token = response.data.result.token;
let user_id = response.data.result.id;
this.$cookies.set('username', username, '7d');
this.$cookies.set('token', token, '7d');
this.$cookies.set('user_id', user_id, '7d');
this.$emit('success', response.data.result);
}).catch(error => {
}).catch(error => {
console.log(error.response.data)
})
}
}
}
</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>
Header.vue
<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="!token">
<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>
</div>
</div>
<Login v-if="is_login" @close="close_login" @go="put_register" @success="success_login" />
<Register v-if="is_register" @close="close_register" @go="put_login" />
</div>
</template>
<script>
import Login from './Login'
import Register from "./Register"
export default {
name: "Header",
data() {
return {
url_path: sessionStorage.url_path || '/',
token: '',
username: '',
user_id: '',
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;
},
success_login(data) {
this.is_login = false;
this.username = data.username;
this.token = data.token;
this.user_id = data.user_id;
},
logout() {
this.token = '';
this.username = '';
this.user_id = '';
this.$cookies.remove('username');
this.$cookies.remove('token');
this.$cookies.remove('user_id');
}
},
created() {
sessionStorage.url_path = this.$route.path;
this.url_path = this.$route.path;
// 检测cookies,查看登录状态
this.username = this.$cookies.get('username');
this.token = this.$cookies.get('token');
this.user_id = this.$cookies.get('user_id');
},
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>
3-手机是否存在验证接口
后台
urls.py
path('mobile/', views.MobileViewSet.as_view({'post': 'check'})),
views.py
# 手机是否存在接口
import re
class MobileViewSet(ViewSet):
def check(self, request, *args, **kwargs):
mobile = request.data.get('mobile', None)
if not mobile:
return APIResponse(1, 'mobile field required')
if not re.match(r'^1[3-9][0-9]{9}$', mobile):
return APIResponse(1, 'mobile field error')
try:
models.User.objects.get(mobile=mobile)
return APIResponse(result=True) # 手机号存在
except:
return APIResponse(result=False) # 手机号不存在
4-腾讯云短信开发
腾讯云短信开发
短信服务应用申请
""" 准备工作
1)创建短信应用 - 应用管理
2)申请短信签名 - 国内短信 > 签名管理
3)申请短信模块 - 国内短信 > 正文模板管理
"""
python中开发腾讯云短信服务
"""
1)API文档,接口的使用说吧
2)SDK,基于开发语言封装的可以直接调用的功能(工具)集合
官网sdk使用文档中找到安装命令:pip install qcloudsms_py
按照sdk使用说明进行开发:https://cloud.tencent.com/document/product/382/11672
"""
t_sms.py
# 所有配置换成申请的数据
# 申请的短信应用 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 包
init.py
from .sms import get_code, send_code
settings.py
# 申请的短信应用 SDK AppID
APP_ID = 1400
# 申请的短信应用 SDK AppKey
APP_KEY = "ba81"
# 申请的短信模板ID,需要在短信控制台中申请
TEMPLATE_ID = 5447
# 申请的签名,参数使用的是`签名内容`,而不是`签名ID`
SIGN = "Owen的技术栈"
sms.py
import random
def get_code():
code = ''
for i in range(4):
code += str(random.randint(0, 9))
return code
from qcloudsms_py import SmsSingleSender
from . import settings
from utils.logging import logger
sender = SmsSingleSender(settings.APP_ID, settings.APP_KEY)
def send_code(mobile, code, exp):
try:
result = sender.send_with_param(
86,
mobile,
settings.TEMPLATE_ID,
(code, exp),
sign=settings.SIGN,
extend="", ext=""
)
if result and result.get('result') == 0:
return True
logger.error('短信发送失败:%s' % result.get('errmsg'))
except Exception as e:
logger.critical('短信发送异常:%s' % e)
return False
5 短信验证码接口
后台
urls.py
path('sms/', views.SMSViewSet.as_view({'get': 'send'})),
throttles.py
from rest_framework.throttling import SimpleRateThrottle
from django.core.cache import cache
from django.conf import settings
# 结合手机验证码接口来书写
class SMSRateThrottle(SimpleRateThrottle):
scope = 'sms'
def get_cache_key(self, request, view):
# 手机号是通过get请求提交的
mobile = request.query_params.get('mobile', None)
if not mobile:
return None # 不限制
# 手机验证码发送失败,不限制,只有发送成功才限制,如果需求是发送失败也做频率限制,就注释下方三行
code = cache.get(settings.SMS_CACHE_KEY % {'mobile': mobile})
if not code:
return None
return self.cache_format % {
'scope': self.scope,
'ident': mobile,
}
const.py
# 短信验证码缓存key
SMS_CACHE_KEY = 'sms_cache_%(mobile)s'
# 短信验证码缓存时间s
SMS_CACHE_TIME = 300
dev.py
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'utils.exception.exception_handler',
'DEFAULT_THROTTLE_RATES': {
'sms': '1/min'
}
}
views.py
from libs import tx_sms
from django.core.cache import cache
from django.conf import settings
from .throttles import SMSRateThrottle
class SMSViewSet(ViewSet):
# 设置频率限制,一个手机号一分钟只能访问一次
throttle_classes = [SMSRateThrottle]
def send(self, request, *args, **kwargs):
# return APIResponse(result=False)
# 1)接收前台手机号验证手机格式
mobile = request.query_params.get('mobile', None)
if not mobile:
return APIResponse(1, 'mobile field required')
if not re.match(r'^1[3-9][0-9]{9}$', mobile):
return APIResponse(1, 'mobile field error')
# 2)后台产生短信验证码
code = tx_sms.get_code()
# 3)把验证码交给第三方,发送短信
result = tx_sms.send_code(mobile, code, settings.SMS_CACHE_TIME // 60)
# 4)如果短信发送成功,服务器缓存验证码(内存数据库),方便下一次校验
if result:
cache.set(settings.SMS_CACHE_KEY % {'mobile': mobile}, code, settings.SMS_CACHE_TIME)
# 5)响应前台短信是否发生成功
return APIResponse(result=result)
6-短信登录接口
后台
urls.py
path('mobile/login/', views.MobileLoginViewSet.as_view({'post': 'login'})),
serializers.py
import re
from django.core.cache import cache
class MobileLoginSerializer(serializers.ModelSerializer):
# 覆盖
mobile = serializers.CharField(required=True, write_only=True)
# 自定义
code = serializers.CharField(min_length=4, max_length=4, required=True, write_only=True)
class Meta:
model = models.User
fields = ('id', 'username', 'icon', 'mobile', 'code')
extra_kwargs = {
'id': {
'read_only': True,
},
'username': {
'read_only': True,
},
'icon': {
'read_only': True,
},
}
# 手机号格式校验(手机号是否存在校验规则自己考量)
def validate_mobile(self, value):
if not re.match(r'^1[3-9][0-9]{9}$', value):
raise exceptions.ValidationError('mobile field error')
return value
def validate(self, attrs):
# 验证码校验 - 需要验证码与手机号两者参与
mobile = self._check_code(attrs)
# 多方式得到user
user = self._get_user(mobile)
# user签发token
token = self._get_token(user)
# token用context属性携带给视图类
self.context['token'] = token
# 将登录用户对象直接传给视图
self.context['user'] = user
return attrs
def _check_code(self, attrs):
mobile = attrs.get('mobile')
code = attrs.pop('code')
old_code = cache.get(settings.SMS_CACHE_KEY % {'mobile': mobile})
if code != old_code:
raise exceptions.ValidationError({'code': 'double code error'})
else:
# 验证码的时效性:一旦验证码验证通过,代表改验证码已使用,需要立即失效
# cache.set(settings.SMS_CACHE_KEY % {'mobile': mobile}, '', -1)
pass
return mobile
def _get_user(self, mobile):
try:
return models.User.objects.get(mobile=mobile)
except:
raise exceptions.ValidationError({'mobile': 'user not exist'})
def _get_token(self, user):
from rest_framework_jwt.serializers import jwt_payload_handler, jwt_encode_handler
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
return token
views.py
class MobileLoginViewSet(ViewSet):
# 局部禁用认证、权限组件
authentication_classes = ()
permission_classes = ()
def login(self, request, *args, **kwargs):
serializer = serializers.MobileLoginSerializer(data=request.data, context={'request': request})
if serializer.is_valid():
token = serializer.context.get('token')
# 拿到登录用户,直接走序列化过程,将要返回给前台的数据直接序列化好给前台
user = serializer.context.get('user')
# 返回给前台的数据结果:id,username,icon,token
result = serializers.MobileLoginSerializer(user, context={'request': request}).data
result['token'] = token
return APIResponse(result=result)
return APIResponse(1, serializer.errors)
7-短信注册接口
后台
urls.py
router.register('register', views.RegisterViewSet, 'register')
serializers.py
class RegisterSerializer(serializers.ModelSerializer):
code = serializers.CharField(min_length=4, max_length=4, required=True, write_only=True)
class Meta:
model = models.User
fields = ('mobile', 'password', 'code')
extra_kwargs = {
'password': {
'min_length': 8,
'max_length': 16,
'write_only': True,
}
}
def validate_mobile(self, value):
if not re.match(r'^1[3-9][0-9]{9}$', value):
raise exceptions.ValidationError('mobile field error')
return value
def validate(self, attrs):
# 验证码校验 - 需要验证码与手机号两者参与
mobile = attrs.get('mobile')
code = attrs.pop('code')
old_code = cache.get(settings.SMS_CACHE_KEY % {'mobile': mobile})
if code != old_code:
raise exceptions.ValidationError({'code': 'double code error'})
else:
# 验证码的时效性:一旦验证码验证通过,代表改验证码已使用,需要立即失效
# cache.set(settings.SMS_CACHE_KEY % {'mobile': mobile}, '', -1)
pass
# 数据入库必须需要唯一账号:1)前台注册必须提供账号 2)默认用手机号作为账号名,后期可以修改
attrs['username'] = mobile
return attrs
def create(self, validated_data): # 入库的数据:mobile,password,username
return models.User.objects.create_user(**validated_data)
views.py
# 手机验证码注册
from rest_framework.viewsets import GenericViewSet
from rest_framework import mixins
class RegisterViewSet(GenericViewSet, mixins.CreateModelMixin):
queryset = models.User.objects.all()
serializer_class = serializers.RegisterSerializer
def create(self, request, *args, **kwargs):
response = super().create(request, *args, **kwargs)
return APIResponse(result=response.data, http_status=response.status_code)
8-前台登录注册修订
前台
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 @click="mobile_login" 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;
// js正则:/正则语法/
// '字符串'.match(/正则语法/)
if (!this.mobile.match(/^1[3-9][0-9]{9}$/)) {
this.$message({
message: '手机号有误',
type: 'warning',
duration: 1000,
onClose: () => {
this.mobile = '';
}
});
return false;
}
// 后台校验手机号是否已存在
this.$axios({
url: this.$settings.base_url + '/user/mobile/',
method: 'post',
data: {
mobile: this.mobile
}
}).then(response => {
let result = response.data.result;
if (result) {
this.$message({
message: '账号正常',
type: 'success',
duration: 1000,
});
// 发生验证码按钮才可以被点击
this.is_send = true;
} else {
this.$message({
message: '账号不存在',
type: 'warning',
duration: 1000,
onClose: () => {
this.mobile = '';
}
})
}
}).catch(() => {
});
},
send_sms() {
// this.is_send必须允许发生验证码,才可以往下执行逻辑
if (!this.is_send) return;
// 按钮点一次立即禁用
this.is_send = false;
let sms_interval_time = 60;
this.sms_interval = "发送中...";
// 定时器: setInterval(fn, time, args)
// 往后台发送验证码
this.$axios({
url: this.$settings.base_url + '/user/sms/',
method: 'get',
params: {
mobile: this.mobile
}
}).then(response => {
let result = response.data.result;
if (result) { // 发送成功
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);
} else { // 发送失败
this.sms_interval = "重新获取";
this.is_send = true;
this.$message({
message: '短信发送失败',
type: 'warning',
duration: 3000
});
}
}).catch(() => {
this.sms_interval = "频率过快";
this.is_send = true;
})
},
login() {
if (!(this.username && this.password)) {
this.$message({
message: '请填好账号密码',
type: 'warning',
duration: 1500
});
return false // 直接结束逻辑
}
this.$axios({
url: this.$settings.base_url + '/user/login/',
method: 'post',
data: {
username: this.username,
password: this.password,
}
}).then(response => {
let username = response.data.result.username;
let token = response.data.result.token;
let user_id = response.data.result.id;
this.$cookies.set('username', username, '7d');
this.$cookies.set('token', token, '7d');
this.$cookies.set('user_id', user_id, '7d');
this.$emit('success', response.data.result);
}).catch(error => {
console.log(error.response.data)
})
},
mobile_login () {
if (!(this.mobile && this.sms)) {
this.$message({
message: '请填好手机与验证码',
type: 'warning',
duration: 1500
});
return false // 直接结束逻辑
}
this.$axios({
url: this.$settings.base_url + '/user/mobile/login/',
method: 'post',
data: {
mobile: this.mobile,
code: this.sms,
}
}).then(response => {
let username = response.data.result.username;
let token = response.data.result.token;
let user_id = response.data.result.id;
this.$cookies.set('username', username, '7d');
this.$cookies.set('token', token, '7d');
this.$cookies.set('user_id', user_id, '7d');
this.$emit('success', response.data.result);
}).catch(error => {
console.log(error.response.data)
})
}
}
}
</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 @click="register" 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;
// js正则:/正则语法/
// '字符串'.match(/正则语法/)
if (!this.mobile.match(/^1[3-9][0-9]{9}$/)) {
this.$message({
message: '手机号有误',
type: 'warning',
duration: 1000,
onClose: () => {
this.mobile = '';
}
});
return false;
}
// 后台校验手机号是否已存在
this.$axios({
url: this.$settings.base_url + '/user/mobile/',
method: 'post',
data: {
mobile: this.mobile
}
}).then(response => {
let result = response.data.result;
if (!result) {
this.$message({
message: '欢迎注册我们的平台',
type: 'success',
duration: 1500,
});
// 发生验证码按钮才可以被点击
this.is_send = true;
} else {
this.$message({
message: '账号已存在,请直接登录',
type: 'warning',
duration: 1500,
})
}
}).catch(() => {});
},
send_sms() {
// this.is_send必须允许发生验证码,才可以往下执行逻辑
if (!this.is_send) return;
// 按钮点一次立即禁用
this.is_send = false;
let sms_interval_time = 60;
this.sms_interval = "发送中...";
// 定时器: setInterval(fn, time, args)
// 往后台发送验证码
this.$axios({
url: this.$settings.base_url + '/user/sms/',
method: 'get',
params: {
mobile: this.mobile
}
}).then(response => {
let result = response.data.result;
if (result) { // 发送成功
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);
} else { // 发送失败
this.sms_interval = "重新获取";
this.is_send = true;
this.$message({
message: '短信发送失败',
type: 'warning',
duration: 3000
});
}
}).catch(() => {
this.sms_interval = "频率过快";
this.is_send = true;
})
},
register () {
if (!(this.mobile && this.sms && this.password)) {
this.$message({
message: '请填好手机、密码与验证码',
type: 'warning',
duration: 1500
});
return false // 直接结束逻辑
}
this.$axios({
url: this.$settings.base_url + '/user/register/',
method: 'post',
data: {
mobile: this.mobile,
code: this.sms,
password: this.password
}
}).then(response => {
this.$message({
message: '注册成功,3秒跳转登录页面',
type: 'success',
duration: 3000,
showClose: true,
onClose: () => {
// 去向成功页面
this.$emit('success')
}
});
}).catch(error => {
this.$message({
message: '注册失败,请重新注册',
type: 'warning',
duration: 1500,
showClose: true,
onClose: () => {
// 清空所有输入框
this.mobile = '';
this.password = '';
this.sms = '';
}
});
})
}
}
}
</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>
Header.vue
<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="!token">
<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>
</div>
</div>
<Login v-if="is_login" @close="close_login" @go="put_register" @success="success_login" />
<Register v-if="is_register" @close="close_register" @go="put_login" @success="success_register" />
</div>
</template>
<script>
import Login from './Login'
import Register from "./Register"
export default {
name: "Header",
data() {
return {
url_path: sessionStorage.url_path || '/',
token: '',
username: '',
user_id: '',
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;
},
success_login(data) {
this.is_login = false;
this.username = data.username;
this.token = data.token;
this.user_id = data.user_id;
},
logout() {
this.token = '';
this.username = '';
this.user_id = '';
this.$cookies.remove('username');
this.$cookies.remove('token');
this.$cookies.remove('user_id');
},
success_register () {
this.is_register = false;
this.is_login = true;
}
},
created() {
sessionStorage.url_path = this.$route.path;
this.url_path = this.$route.path;
// 检测cookies,查看登录状态
this.username = this.$cookies.get('username');
this.token = this.$cookies.get('token');
this.user_id = this.$cookies.get('user_id');
},
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>
本文来自博客园,作者:喝茶看猴戏,转载请注明原文链接:https://www.cnblogs.com/zdwzdwzdw/p/17487882.html

浙公网安备 33010602011771号