- 短信接口分析
- 使用Redis缓存验证码
- 手机号验证接口
- 短信接口
- 短信过期时间
- 注册后台接口
- 注册前台逻辑
- 多方式登录
- 前台登录注销
- 短信登录
- 短信接口频率限制
- 接口缓存
-曾老湿, 江湖人称曾老大。
-多年互联网运维工作经验,曾负责过大规模集群架构自动化运维管理工作。 -擅长Web集群架构与自动化运维,曾负责国内某大型金融公司运维工作。 -devops项目经理兼DBA。 -开发过一套自动化运维平台(功能如下): 1)整合了各个公有云API,自主创建云主机。 2)ELK自动化收集日志功能。 3)Saltstack自动化运维统一配置管理工具。 4)Git、Jenkins自动化代码上线及自动化测试平台。 5)堡垒机,连接Linux、Windows平台及日志审计。 6)SQL执行及审批流程。 7)慢查询日志分析web界面。
短信接口分析
短信发送验证码步骤 |
---|
# 1.拿到前台的手机号
# 2.调用tzsms生成手机验证码
# 3.调用txsms发送手机验证码
# 4.失败反馈信息给前台
# 5.成功服务器缓存手机验证码 - redis
# 6.反馈成功信息给前台
使用Redis缓存验证码
配置Django使用redis |
---|
安装
代码语言:javascript复制(luffy) bash-3.2$ pip install django-redis
settings/dev.py
代码语言:javascript复制## 配置Django缓存 - 采用redis
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://10.0.0.51:6379/10",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
"CONNECTION_POOL_KWARGS": {"max_connections": 100}
}
}
}
测试 |
---|
scripts/test_dg_redis.py
代码语言:javascript复制from django.core.cache import cache
import os, django
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "luffyapi.settings.dev")
django.setup()
from user.models import User
user = User.objects.first()
from rest_framework.serializers import ModelSerializer
class UserModelSerializer(ModelSerializer):
class Meta:
model = User
fields = ('username','password')
user_data = UserModelSerializer(user).data
print(user_data)
# 原生redis,django不支持直接存储
# import redis
#
# r = redis.Redis(host='10.0.0.51')
# r.set(user.username,user_data)
cache.set(user.username,user_data,60)
print(cache.get(user.username))
手机号验证接口
创建路由 |
---|
apps/user/urls.py
代码语言:javascript复制from django.urls import path,re_path
from . import views
urlpatterns = [
path('mobile/',views.MobileAPIView.as_view())
]
视图层 |
---|
from rest_framework.views import APIView
from .models import User
from utils.repsonse import APIResponse
import re
# 注册逻辑:1.校验手机号是否存在 2.发送验证码 3.完成注册
class MobileAPIView(APIView):
def post(self, request, *args, **kwargs):
mobile = request.data.get('mobile')
if not mobile or not re.match(r'^1[3-9]d{9}$', mobile):
return APIResponse(1, '数据有误')
try:
User.objects.get(mobile=mobile)
return APIResponse(2, '已注册')
except:
return APIResponse(0, '未注册')
短信接口
路由 |
---|
apps/user/urls.py
代码语言:javascript复制from django.urls import path, re_path
from . import views
urlpatterns = [
path('mobile/', views.MobileAPIView.as_view()),
path('sms/', views.SMSAPIView.as_view()),
]
视图层 |
---|
apps/user/views.py
代码语言:javascript复制from rest_framework.views import APIView
from .models import User
from utils.repsonse import APIResponse
import re
from libs import txsms
from django.core.cache import cache
# 注册逻辑:1.校验手机号是否存在 2.发送验证码 3.完成注册
class MobileAPIView(APIView):
def post(self, request, *args, **kwargs):
mobile = request.data.get('mobile')
if not mobile or not re.match(r'^1[3-9]d{9}$', mobile):
return APIResponse(1, '数据有误')
try:
User.objects.get(mobile=mobile)
return APIResponse(2, '已注册')
except:
return APIResponse(0, '未注册')
# 发送验证码接口
# 1.拿到前台的手机号
# 2.调用tzsms生成手机验证码
# 3.调用txsms发送手机验证码
# 4.失败反馈信息给前台
# 5.成功服务器缓存手机验证码 - redis
# 6.反馈成功信息给前台
class SMSAPIView(APIView):
def post(self, request, *args, **kwargs):
# 1.拿到前台的手机号
mobile = request.data.get('mobile')
if not mobile or not re.match(r'^1[3-9]d{9}$', mobile):
return APIResponse(1, '数据有误')
# 2.调用tzsms生成手机验证码
code = txsms.get_code()
# 3.调用txsms发送手机验证码
result = txsms.send_sms(mobile, code, 5)
# 4.失败反馈信息给前台
if not result:
return APIResponse(1, '短信发送失败')
# 5.成功服务器缓存手机验证码 - redis
cache.set('sms_%s' % mobile, code, 5 * 60)
# 6.反馈成功信息给前台
return APIResponse(0, '短信发送成功')
前端注册页 |
---|
Register.vue
代码语言:javascript复制<template>
<div class="box">
<img src="@/assets/img/Loginbg.jpg" alt="">
<div class="register">
<div class="register_box">
<div class="register-title">注册路飞学城</div>
<div class="inp">
<input v-model="mobile" @blur="checkMobile" type="text" placeholder="手机号码" class="user">
<input v-model="password" type="password" placeholder="用户密码" class="user">
<div class="sms">
<input v-model="sms" type="text" placeholder="输入验证码" class="user">
<span class="sms_btn" @click="send_sms">{{sms_interval_tips}}</span>
</div>
<div id="geetest"></div>
<button class="register_btn" @click="registerMobile">注册</button>
<p class="go_login">已有账号
<router-link to="/login">直接登录</router-link>
</p>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Register',
data() {
return {
sms: "",
mobile: "",
password: "",
is_send: false, // 是否在60s内发送了短信
sms_interval_tips: "获取验证码",
}
},
created() {
},
methods: {
checkMobile() {
// 手机框必须填内容
if (this.mobile.length < 1) {
return false;
}
// 手机号码格式是否正确
// js正则语法糖 /正则语法/
if (!/^1[3-9]d{9}$/.test(this.mobile)) {
this.$message({
message: "对不起!手机号码格式有误!"
});
return false;
}
// 验证手机号码是否已经注册了
// this.$axios.get(this.$settings.base_url "/user/mobile/?mobile=" this.mobile "/");
this.$axios({
url: this.$settings.base_url '/user/mobile/',
method: 'post',
data: {
mobile: this.mobile
}
}).then(response => {
let data = response.data;
// window.console.log(data);
if (data.status !== 0) {
this.$message({
message: "对不起!手机号码已经被注册!",
type: 'warning'
});
return false;
} else {
this.$message({
message: "期待您加入我们!"
});
}
}).catch(error => {
let data = error.response.data;
this.$message({
message: data.message
})
})
},
send_sms() {
// 发送短信
if (!/^1[3-9]d{9}$/.test(this.mobile)) {
this.$message({
message: "对不起!手机号码格式有误!"
});
return false;
}
// 判断是否在60s内发送过短信
if (this.is_send) {
this.$message({
message: "对不起,不能频繁发送短信验证!"
});
return false;
}
// 请求发送短信
this.$axios({
url: this.$settings.base_url '/user/sms/',
method: 'post',
data: {
mobile: this.mobile
}
}).then(response => {
this.$message({
message: response.data.msg,
});
}).catch(error => {
this.$message({
message: error.response.data.result,
})
});
// 修改短信的发送状态
this.is_send = true;
// 设置间隔时间60s
let sms_interval_time = 60;
// 设置短信发送间隔倒计时,.60s后把is_send改成false
let timer = setInterval(() => {
if (sms_interval_time <= 1) {
clearInterval(timer);
this.sms_interval_tips = "获取验证码";
this.is_send = false; // 重新回复点击发送功能的条件
} else {
sms_interval_time -= 1;
this.sms_interval_tips = `${sms_interval_time}秒后再次获取`;
}
}, 1000);
},
registerMobile() {
// 注册信息提交
if (!/^1[3-9]d{9}$/.test(this.mobile)) {
this.$message({
message: "对不起!手机号码格式有误!"
});
return false;
}
if (this.sms.length < 1) {
this.$message({
message: "短信验证码不能为空!"
});
return false;
}
if (this.password.length < 6 || this.password.length > 16) {
this.$message({
message: "对不起,密码长度必须在6-16个字符之间!"
});
return false;
}
this.$axios({
url: this.$settings.Host 'user/register/mobile/',
method: 'post',
data: {
mobile: this.mobile,
password: this.password,
sms: this.sms
}
}).then(response => {
let _this = this;
let status = response.data.status;
let msg = response.data.msg;
_this.$message({
message: msg,
onClose() {
if (status === 0) {
// 保存登录状态
sessionStorage.user_name = response.data.user.username;
// sessionStorage.user_mobile = response.data.user.mobile;
// 跳转到用户中心
// _this.$router.push('/user');
}
}
});
}).catch(error => {
this.$message({
message: error.response.data.result
});
})
}
},
};
</script>
<style scoped>
.box {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
}
.box img {
width: 100%;
min-height: 100%;
}
.box .register {
position: absolute;
width: 500px;
height: 400px;
left: 0;
margin: auto;
right: 0;
bottom: 0;
top: -238px;
}
.register .register-title {
width: 100%;
font-size: 24px;
text-align: center;
padding-top: 30px;
padding-bottom: 30px;
color: #4a4a4a;
letter-spacing: .39px;
}
.register-title img {
width: 190px;
height: auto;
}
.register-title p {
font-size: 18px;
color: #fff;
letter-spacing: .29px;
padding-top: 10px;
padding-bottom: 50px;
}
.register_box {
width: 400px;
height: auto;
background: #fff;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .5);
border-radius: 4px;
margin: 0 auto;
padding-bottom: 40px;
}
.register_box .title {
font-size: 20px;
color: #9b9b9b;
letter-spacing: .32px;
border-bottom: 1px solid #e6e6e6;
display: flex;
justify-content: space-around;
padding: 50px 60px 0 60px;
margin-bottom: 20px;
cursor: pointer;
}
.register_box .title span:nth-of-type(1) {
color: #4a4a4a;
border-bottom: 2px solid #84cc39;
}
.inp {
width: 350px;
margin: 0 auto;
}
.inp input {
outline: 0;
width: 100%;
height: 45px;
border-radius: 4px;
border: 1px solid #d9d9d9;
text-indent: 20px;
font-size: 14px;
background: #fff !important;
}
.inp input.user {
margin-bottom: 16px;
}
.inp .rember {
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
margin-top: 10px;
}
.inp .rember p:first-of-type {
font-size: 12px;
color: #4a4a4a;
letter-spacing: .19px;
margin-left: 22px;
display: -ms-flexbox;
display: flex;
-ms-flex-align: center;
align-items: center;
/*position: relative;*/
}
.inp .rember p:nth-of-type(2) {
font-size: 14px;
color: #9b9b9b;
letter-spacing: .19px;
cursor: pointer;
}
.inp .rember input {
outline: 0;
width: 30px;
height: 45px;
border-radius: 4px;
border: 1px solid #d9d9d9;
text-indent: 20px;
font-size: 14px;
background: #fff !important;
}
.inp .rember p span {
display: inline-block;
font-size: 12px;
width: 100px;
/*position: absolute;*/
/*left: 20px;*/
}
#geetest {
margin-top: 20px;
}
.register_btn {
width: 100%;
height: 45px;
background: #84cc39;
border-radius: 5px;
font-size: 16px;
color: #fff;
letter-spacing: .26px;
margin-top: 30px;
}
.inp .go_login {
text-align: center;
font-size: 14px;
color: #9b9b9b;
letter-spacing: .26px;
padding-top: 20px;
}
.inp .go_login a {
color: #84cc39;
cursor: pointer;
}
.sms {
position: relative;
}
.sms .sms_btn {
position: absolute;
top: -12px;
right: 0;
bottom: 0;
margin: auto;
width: 130px;
text-align: center;
height: 24px;
color: #ff7000;
cursor: pointer;
border-left: 1px solid #999;
}
</style>
短信过期时间
设置常量 |
---|
在settings目录下创建一个const.py文件
settings/const.py
代码语言:javascript复制# 短信过期时间(单位:s)
SMS_EXP = 300
视图 |
---|
from rest_framework.views import APIView
from .models import User
from utils.repsonse import APIResponse
import re
from libs import txsms
from django.core.cache import cache
from settings.const import SMS_EXP
# 注册逻辑:1.校验手机号是否存在 2.发送验证码 3.完成注册
class MobileAPIView(APIView):
def post(self, request, *args, **kwargs):
mobile = request.data.get('mobile')
if not mobile or not re.match(r'^1[3-9]d{9}$', mobile):
return APIResponse(1, '数据有误')
try:
User.objects.get(mobile=mobile)
return APIResponse(2, '已注册')
except:
return APIResponse(0, '未注册')
# 发送验证码接口
# 1.拿到前台的手机号
# 2.调用tzsms生成手机验证码
# 3.调用txsms发送手机验证码
# 4.失败反馈信息给前台
# 5.成功服务器缓存手机验证码 - redis
# 6.反馈成功信息给前台
class SMSAPIView(APIView):
def post(self, request, *args, **kwargs):
# 1.拿到前台的手机号
mobile = request.data.get('mobile')
if not mobile or not re.match(r'^1[3-9]d{9}$', mobile):
return APIResponse(1, '数据有误')
# 2.调用txsms生成手机验证码
code = txsms.get_code()
# 3.调用txsms发送手机验证码
result = txsms.send_sms(mobile, code, SMS_EXP // 60)
print(result)
# 4.失败反馈信息给前台
if not result:
return APIResponse(1, '短信发送失败')
# 5.成功服务器缓存手机验证码 - redis
cache.set('sms_%s' % mobile, code, SMS_EXP)
# 6.反馈成功信息给前台
return APIResponse(0, '短信发送成功')
注册后台接口
路由 |
---|
from django.urls import path, re_path
from . import views
urlpatterns = [
path('mobile/', views.MobileAPIView.as_view()),
path('sms/', views.SMSAPIView.as_view()),
path('register/', views.RegisterCreateAPIView.as_view()),
]
视图 |
---|
from rest_framework.generics import CreateAPIView
from . import serializers
class RegisterCreateAPIView(CreateAPIView):
# queryset = User.objects.filter(is_active=True)
serializer_class = serializers.RegisterModelSerializer
# 自定义响应结果
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True) # 校验失败就主动抛异常 => 自定义异常结果,配置异常模块
user_obj = serializer.save() # 要自定义入库逻辑,重写create方法
headers = self.get_success_headers(serializer.data)
# 响应结果需要格式化,使用序列化类要提供序列化与反序列化两套规则
return APIResponse(0, 'ok',
results=serializers.RegisterModelSerializer(user_obj).data,
http_status=201,
headers=headers
)
序列化 |
---|
user/serializers.py
代码语言:javascript复制from rest_framework import serializers
from . import models
import re
from django.core.cache import cache
from settings.const import SMS_CACHE_KEY
class RegisterModelSerializer(serializers.ModelSerializer):
# 自定义反序列化字段的规则必须在字段声明时规定
code = serializers.CharField(write_only=True, min_length=4, max_length=4)
class Meta:
model = models.User
fields = ('mobile', 'password', 'code', 'username', 'email')
extra_kwargs = {
'password': {
'min_length': 6,
'max_length': 18,
'write_only': True
},
'username': {
'read_only': True
},
'email': {
'read_only': True
}
}
def validate_mobile(self, value):
if not re.match(r'^1[3-9]d{9}$', value):
raise serializers.ValidationError('手机号有误')
return value
def validate(self, attrs):
mobile = attrs.get('mobile')
code = attrs.pop('code') # code不入库
old_code = cache.get(SMS_CACHE_KEY % {'mobile': mobile})
if not old_code:
raise serializers.ValidationError({'code': '验证码已失效'})
if code != old_code:
raise serializers.ValidationError({'code': '验证码错误'})
# 验证码一旦验证成功,就失效(一次性)
# cache.set(SMS_CACHE_KEY % {'mobile': mobile}, '0000', 1)
return attrs
# create方法重写:通过手机号注册的用户,用户名默认就是手机号
def create(self, validated_data):
mobile = validated_data.get('mobile')
username = mobile
password = validated_data.get('password')
return models.User.objects.create_user(username=username, mobile=mobile, password=password)
自定义exception |
---|
settings/dev.py
代码语言:javascript复制# drf配置
from rest_framework import settings
REST_FRAMEWORK = {
# 异常配置
'EXCEPTION_HANDLER': 'utils.exception.exception_handler',
# 频率限制配置
'DEFAULT_THROTTLE_RATES': {
'user': None,
'anon': None,
'sms': '1/m',
},
}
utils/exception.py
代码语言:javascript复制from rest_framework.views import exception_handler as drf_exception_handler
from rest_framework import status
from utils.logging import logger
from utils.response import APIResponse
def exception_handler(exc, context):
response = drf_exception_handler(exc, context)
if response is None:
logger.error('%s - %s - %s' % (context['view'], context['request'].method, exc))
return APIResponse(3, '异常',
results={'detail': '服务器错误'},
http_status=status.HTTP_500_INTERNAL_SERVER_ERROR,
exception=True
)
return APIResponse(3, '异常', results=response.data, http_status=status.HTTP_401_UNAUTHORIZED)
注册前台逻辑
注册页面 |
---|
Register.vue
代码语言:javascript复制<template>
<div class="box">
<img src="@/assets/img/Loginbg.jpg" alt="">
<div class="register">
<div class="register_box">
<div class="register-title">注册路飞学城</div>
<div class="inp">
<input v-model="mobile" @blur="checkMobile" type="text" placeholder="手机号码" class="user">
<input v-model="password" type="password" placeholder="用户密码" class="user">
<div class="sms">
<input v-model="sms" type="text" placeholder="输入验证码" class="user">
<span class="sms_btn" @click="send_sms">{{sms_interval_tips}}</span>
</div>
<div id="geetest"></div>
<button class="register_btn" @click="registerMobile">注册</button>
<p class="go_login">已有账号
<router-link to="/login">直接登录</router-link>
</p>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Register',
data() {
return {
sms: "",
mobile: "",
password: "",
is_send: false, // 是否在60s内发送了短信
sms_interval_tips: "获取验证码",
}
},
created() {
},
methods: {
checkMobile() {
// 手机框必须填内容
if (this.mobile.length < 1) {
return false;
}
// 手机号码格式是否正确
// js正则语法糖 /正则语法/
if (!/^1[3-9]d{9}$/.test(this.mobile)) {
this.$message({
message: "对不起!手机号码格式有误!"
});
return false;
}
// 验证手机号码是否已经注册了
// this.$axios.get(this.$settings.base_url "/user/mobile/?mobile=" this.mobile "/");
this.$axios({
url: this.$settings.base_url '/user/mobile/',
method: 'post',
data: {
mobile: this.mobile
}
}).then(response => {
let data = response.data;
// window.console.log(data);
if (data.status !== 0) {
this.$message({
message: "对不起!手机号码已经被注册!",
type: 'warning'
});
return false;
} else {
this.$message({
message: "期待您加入我们!"
});
}
}).catch(error => {
let data = error.response.data;
this.$message({
message: data.message
})
})
},
send_sms() {
// 发送短信
if (!/^1[3-9]d{9}$/.test(this.mobile)) {
this.$message({
message: "对不起!手机号码格式有误!"
});
return false;
}
// 判断是否在60s内发送过短信
if (this.is_send) {
this.$message({
message: "对不起,不能频繁发送短信验证!"
});
return false;
}
// 请求发送短信
this.$axios({
url: this.$settings.base_url '/user/sms/',
method: 'post',
data: {
mobile: this.mobile
}
}).then(response => {
this.$message({
message: response.data.msg,
});
}).catch(error => {
this.$message({
message: error.response.data.result,
})
});
// 修改短信的发送状态
this.is_send = true;
// 设置间隔时间60s
let sms_interval_time = 60;
// 设置短信发送间隔倒计时,.60s后把is_send改成false
let timer = setInterval(() => {
if (sms_interval_time <= 1) {
clearInterval(timer);
this.sms_interval_tips = "获取验证码";
this.is_send = false; // 重新回复点击发送功能的条件
} else {
sms_interval_time -= 1;
this.sms_interval_tips = `${sms_interval_time}秒后再次获取`;
}
}, 1000);
},
registerMobile() {
// 注册信息提交
if (!/^1[3-9]d{9}$/.test(this.mobile)) {
this.$message({
message: "对不起!手机号码格式有误!"
});
return false;
}
if (this.sms.length < 1) {
this.$message({
message: "短信验证码不能为空!"
});
return false;
}
if (this.password.length < 6 || this.password.length > 18) {
this.$message({
message: "对不起,密码长度必须在6-16个字符之间!"
});
return false;
}
this.$axios({
url: this.$settings.base_url '/user/register/',
method: 'post',
data: {
mobile: this.mobile,
password: this.password,
code: this.sms
}
}).then(response => {
window.console.log(response);
// let _this = this;
let status = response.data.status;
this.$message({
message: '注册成功',
duration: 1500,
onClose: () => {
if (status === 0) {
// 跳转到主页
this.$router.push('/')
}
}
});
}).catch(error => {
window.console.log(error)
// this.$message({
// message: error.response.data.result
// });
})
}
},
};
</script>
<style scoped>
.box {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
}
.box img {
width: 100%;
min-height: 100%;
}
.box .register {
position: absolute;
width: 500px;
height: 400px;
left: 0;
margin: auto;
right: 0;
bottom: 0;
top: -238px;
}
.register .register-title {
width: 100%;
font-size: 24px;
text-align: center;
padding-top: 30px;
padding-bottom: 30px;
color: #4a4a4a;
letter-spacing: .39px;
}
.register-title img {
width: 190px;
height: auto;
}
.register-title p {
font-size: 18px;
color: #fff;
letter-spacing: .29px;
padding-top: 10px;
padding-bottom: 50px;
}
.register_box {
width: 400px;
height: auto;
background: #fff;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .5);
border-radius: 4px;
margin: 0 auto;
padding-bottom: 40px;
}
.register_box .title {
font-size: 20px;
color: #9b9b9b;
letter-spacing: .32px;
border-bottom: 1px solid #e6e6e6;
display: flex;
justify-content: space-around;
padding: 50px 60px 0 60px;
margin-bottom: 20px;
cursor: pointer;
}
.register_box .title span:nth-of-type(1) {
color: #4a4a4a;
border-bottom: 2px solid #84cc39;
}
.inp {
width: 350px;
margin: 0 auto;
}
.inp input {
outline: 0;
width: 100%;
height: 45px;
border-radius: 4px;
border: 1px solid #d9d9d9;
text-indent: 20px;
font-size: 14px;
background: #fff !important;
}
.inp input.user {
margin-bottom: 16px;
}
.inp .rember {
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
margin-top: 10px;
}
.inp .rember p:first-of-type {
font-size: 12px;
color: #4a4a4a;
letter-spacing: .19px;
margin-left: 22px;
display: -ms-flexbox;
display: flex;
-ms-flex-align: center;
align-items: center;
/*position: relative;*/
}
.inp .rember p:nth-of-type(2) {
font-size: 14px;
color: #9b9b9b;
letter-spacing: .19px;
cursor: pointer;
}
.inp .rember input {
outline: 0;
width: 30px;
height: 45px;
border-radius: 4px;
border: 1px solid #d9d9d9;
text-indent: 20px;
font-size: 14px;
background: #fff !important;
}
.inp .rember p span {
display: inline-block;
font-size: 12px;
width: 100px;
/*position: absolute;*/
/*left: 20px;*/
}
#geetest {
margin-top: 20px;
}
.register_btn {
width: 100%;
height: 45px;
background: #84cc39;
border-radius: 5px;
font-size: 16px;
color: #fff;
letter-spacing: .26px;
margin-top: 30px;
}
.inp .go_login {
text-align: center;
font-size: 14px;
color: #9b9b9b;
letter-spacing: .26px;
padding-top: 20px;
}
.inp .go_login a {
color: #84cc39;
cursor: pointer;
}
.sms {
position: relative;
}
.sms .sms_btn {
position: absolute;
top: -12px;
right: 0;
bottom: 0;
margin: auto;
width: 130px;
text-align: center;
height: 24px;
color: #ff7000;
cursor: pointer;
border-left: 1px solid #999;
}
</style>
多方式登录
安装jwt |
---|
(luffy) bash-3.2$ pip install djangorestframework-jwt
序列化类 |
---|
from rest_framework_jwt.serializers import jwt_payload_handler
from rest_framework_jwt.serializers import jwt_encode_handler
class LoginModelSerializer(serializers.ModelSerializer):
usr = serializers.CharField(write_only=True)
pwd = serializers.CharField(write_only=True)
class Meta:
model = models.User
fields = ['usr', 'pwd', 'username', 'mobile', 'email']
extra_kwargs = {
'username': {
'read_only': True
},
'mobile': {
'read_only': True
},
'email': {
'read_only': True
},
}
def validate(self, attrs):
usr = attrs.get('usr')
pwd = attrs.get('pwd')
# 多方式登录:各分支处理得到该方式下对应的用户
if re.match(r'. @. ', usr):
user_query = models.User.objects.filter(email=usr)
elif re.match(r'1[3-9][0-9]{9}', usr):
user_query = models.User.objects.filter(mobile=usr)
else:
user_query = models.User.objects.filter(username=usr)
user_obj = user_query.first()
# 签发:得到登录用户,签发token并存储在实例化对象中
if user_obj and user_obj.check_password(pwd):
# 签发token,将token存放到 实例化类对象的token 名字中
payload = jwt_payload_handler(user_obj)
token = jwt_encode_handler(payload)
# 将当前用户与签发的token都保存在序列化对象中
self.user = user_obj
self.token = token
return attrs
raise serializers.ValidationError({'data': '数据有误'})
视图 |
---|
from rest_framework.views import APIView
from .models import User
from utils.repsonse import APIResponse
import re
# 注册逻辑:1.校验手机号是否存在 2.发送验证码 3.完成注册
class MobileAPIView(APIView):
def post(self, request, *args, **kwargs):
mobile = request.data.get('mobile')
if not mobile or not re.match(r'^1[3-9]d{9}$', mobile):
return APIResponse(1, '数据有误')
try:
User.objects.get(mobile=mobile)
return APIResponse(2, '已注册')
except:
return APIResponse(0, '未注册')
# 发送验证码接口分析
from libs import txsms
from django.core.cache import cache
from settings.const import SMS_EXP, SMS_CACHE_KEY
# from .thorttles import SMSRateThrottle
class SMSAPIView(APIView):
# 频率限制
# throttle_classes = [SMSRateThrottle]
def post(self, request, *args, **kwargs):
# 1)拿到前台的手机号
mobile = request.data.get('mobile')
if not mobile or not re.match(r'^1[3-9]d{9}$', mobile):
return APIResponse(2, '数据有误')
# 2)调用txsms生成手机验证码
code = txsms.get_code()
# 3)调用txsms发送手机验证码
result = txsms.send_sms(mobile, code, SMS_EXP // 60)
# 4)失败反馈信息给前台
if not result:
return APIResponse(1, '短信发送失败')
# 5)成功服务器缓存手机验证码 - 用缓存存储(方便管理) - redis
cache.set(SMS_CACHE_KEY % {'mobile': mobile}, code, SMS_EXP)
# 6)反馈成功信息给前台
return APIResponse(0, '短信发送成功')
from rest_framework.generics import CreateAPIView
from . import serializers
class RegisterCreateAPIView(CreateAPIView):
# queryset = User.objects.filter(is_active=True)
serializer_class = serializers.RegisterModelSerializer
# 自定义响应结果
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True) # 校验失败就主动抛异常 => 自定义异常结果,配置异常模块
user_obj = serializer.save() # 要自定义入库逻辑,重写create方法
headers = self.get_success_headers(serializer.data)
# 响应结果需要格式化,使用序列化类要提供序列化与反序列化两套规则
return APIResponse(0, 'ok',
results=serializers.RegisterModelSerializer(user_obj).data,
http_status=201,
headers=headers
)
# 多方式登录
class LoginAPIView(APIView):
# 1) 禁用认证与权限组件
authentication_classes = []
permission_classes = []
def post(self, request, *args, **kwargs):
# 2) 拿到前台登录信息,交给序列化类,规则:账号用usr传,密码用pwd传
user_ser = serializers.LoginModelSerializer(data=request.data)
# 3) 序列化类校验得到登录用户与token存放在序列化对象中
user_ser.is_valid(raise_exception=True)
# 4) 取出登录用户与token返回给前台
return APIResponse(token=user_ser.token, results=serializers.LoginModelSerializer(user_ser.user).data)
路由 |
---|
from django.urls import path, re_path
from . import views
urlpatterns = [
path('mobile/', views.MobileAPIView.as_view()),
path('sms/', views.SMSAPIView.as_view()),
path('register/', views.RegisterCreateAPIView.as_view()),
path('login/', views.LoginAPIView.as_view()),
]
前台登录页 |
---|
<template>
<div class="login box">
<img src="@/assets/img/Loginbg.jpg" alt="">
<div class="login">
<div class="login-title">
<img src="@/assets/img/Logotitle.png" alt="">
<p>帮助有志向的年轻人通过努力学习获得体面的工作和生活!</p>
</div>
<div class="login_box">
<div class="title">
<span :class="{active: a0}" @click="changeLogin(0)">密码登录</span>
<span :class="{active: a1}" @click="changeLogin(1)">短信登录</span>
</div>
<div class="inp" v-if="login_type===0">
<input v-model="username" type="text" placeholder="用户名 / 手机号码" class="user">
<input v-model="password" type="password" name="" class="pwd" placeholder="密码">
<div id="geetest1"></div>
<div class="rember">
<p>
<input id="checkbox" type="checkbox" class="no" v-model="remember"/>
<span>记住密码</span>
</p>
<p>忘记密码</p>
</div>
<button class="login_btn" @click="loginAction">登录</button>
<p class="go_login">没有账号 <router-link to="/register">立即注册</router-link></p>
</div>
<div class="inp" v-show="login_type===1">
<input v-model="mobile" type="text" placeholder="手机号码" class="user">
<div class="sms">
<input v-model="sms" type="text" placeholder="输入验证码" class="user">
<span class="sms_btn" @click="send_sms">{{sms_interval_tips}}</span>
</div>
<button class="login_btn" @click="loginMobile">登录</button>
<p class="go_login">没有账号 <router-link to="/register">立即注册</router-link></p>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Login',
data() {
return {
a0: 1,
a1: 0,
login_type: 0,
username: "",
password: "",
remember: false,
mobile: "",
sms: "",
is_send: false, // 是否在60s内发送了短信
sms_interval_tips: "获取验证码",
}
},
methods: {
changeLogin(i) {
this.login_type = i;
if (i) {
this.a0 = 0;
this.a1 = 1;
} else {
this.a0 = 1;
this.a1 = 0;
}
},
loginAction() {
if (!this.username || !this.password) {
return
}
this.$axios({
url: this.$settings.base_url '/user/login/',
method: 'post',
data: {
'usr': this.username,
'pwd': this.password
}
}).then((response) => {
// 判断用户是否要记住密码
// window.console.log(">>>>", response.data);
if (this.remember) { // 记住密码
sessionStorage.clear();
localStorage.token = response.data.token;
localStorage.user_name = response.data.results.username;
localStorage.user_mobile = response.data.results.mobile;
} else { /// 没记住密码
localStorage.clear();
sessionStorage.token = response.data.token;
sessionStorage.user_name = response.data.results.username;
sessionStorage.user_mobile = response.data.results.mobile;
}
// 页面跳转
this.$alert("欢迎回来!", "登录成功!", {
confirmButtonText: '确定',
callback: () => {
// 跳转页面
// this.$router.go(-1); // 返回上一页
// 进行制定的网站内部地址跳转
// this.$router.push("站内地址");
this.$router.push("/"); // 前往主页
}
})
}).catch(() => {
this.$alert("检查账号密码!", "登录失败!", {
confirmButtonText: '确定',
callback: () => {
this.username = '';
this.password = '';
}
});
})
},
send_sms() {
// 发送短信
if (!/^1[3-9]d{9}$/.test(this.mobile)) {
this.$message({
message: "对不起!手机号码格式有误!"
});
return false;
}
// 判断是否在60s内发送过短信
if (this.is_send) {
this.$message({
message: "对不起,不能频繁发送短信验证!"
});
return false;
}
// 请求发送短信
this.$axios({
url: this.$settings.base_url '/user/sms/',
method: 'post',
data: {
mobile: this.mobile
}
}).then(response => {
this.$message({
message: response.data.msg,
});
}).catch(error => {
this.$message({
message: error.response.data.result,
})
});
// 修改短信的发送状态
this.is_send = true;
// 设置间隔时间60s
let sms_interval_time = 60;
// 设置短信发送间隔倒计时,.60s后把is_send改成false
let timer = setInterval(() => {
if (sms_interval_time <= 1) {
clearInterval(timer);
this.sms_interval_tips = "获取验证码";
this.is_send = false; // 重新回复点击发送功能的条件
} else {
sms_interval_time -= 1;
this.sms_interval_tips = `${sms_interval_time}秒后再次获取`;
}
}, 1000);
},
loginMobile() {
// 注册信息提交
if (!/^1[3-9]d{9}$/.test(this.mobile)) {
this.$message({
message: "对不起!手机号码格式有误!"
});
return false;
}
if (this.sms.length < 1) {
this.$message({
message: "短信验证码不能为空!"
});
return false;
}
this.$axios({
url: this.$settings.base_url '/user/login/mobile/',
method: 'post',
data: {
mobile: this.mobile,
code: this.sms
}
}).then(response => {
let _this = this;
let status = response.data.status;
let msg = response.data.msg;
_this.$message({
message: msg,
duration: 1500,
onClose() {
if (status === 0) {
// 保存登录状态
sessionStorage.token = response.data.token;
sessionStorage.user_name = response.data.results.username;
sessionStorage.user_mobile = response.data.results.mobile;
// 跳转到主页
_this.$router.push('/');
} else {
// 清空数据库
sessionStorage.clear();
// 清空输入框
_this.mobile = '';
_this.sms = '';
}
}
});
}).catch(error => {
this.$message({
message: error.response.data.result
});
})
},
},
};
</script>
<style scoped>
.box {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
}
.box img {
width: 100%;
min-height: 100%;
}
.box .login {
position: absolute;
width: 500px;
height: 400px;
left: 0;
margin: auto;
right: 0;
bottom: 0;
top: -338px;
}
.login .login-title {
width: 100%;
text-align: center;
padding-top: 20px;
}
.login-title img {
width: 190px;
height: auto;
}
.login-title p {
font-family: PingFangSC-Regular;
font-size: 18px;
color: #fff;
letter-spacing: .29px;
padding-top: 10px;
padding-bottom: 50px;
}
.login_box {
width: 400px;
height: auto;
background: #fff;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .5);
border-radius: 4px;
margin: 0 auto;
padding-bottom: 40px;
}
.login_box .title {
font-size: 20px;
color: #9b9b9b;
letter-spacing: .32px;
border-bottom: 1px solid #e6e6e6;
display: flex;
justify-content: space-around;
padding: 50px 60px 0 60px;
margin-bottom: 20px;
cursor: pointer;
}
.login_box .title span.active {
color: #4a4a4a;
border-bottom: 2px solid #84cc39;
}
.inp {
width: 350px;
margin: 0 auto;
}
.inp input {
outline: 0;
width: 100%;
height: 45px;
border-radius: 4px;
border: 1px solid #d9d9d9;
text-indent: 20px;
font-size: 14px;
background: #fff !important;
}
.inp input.user {
margin-bottom: 16px;
}
.inp .rember {
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
margin-top: 10px;
}
.inp .rember p:first-of-type {
font-size: 12px;
color: #4a4a4a;
letter-spacing: .19px;
margin-left: 22px;
display: -ms-flexbox;
display: flex;
-ms-flex-align: center;
align-items: center;
/*position: relative;*/
}
.inp .rember p:nth-of-type(2) {
font-size: 14px;
color: #9b9b9b;
letter-spacing: .19px;
cursor: pointer;
}
.inp .rember input {
outline: 0;
width: 30px;
height: 45px;
border-radius: 4px;
border: 1px solid #d9d9d9;
text-indent: 20px;
font-size: 14px;
background: #fff !important;
}
.inp .rember p span {
display: inline-block;
font-size: 12px;
width: 100px;
/*position: absolute;*/
/*left: 20px;*/
}
#geetest {
margin-top: 20px;
}
.login_btn {
width: 100%;
height: 45px;
background: #84cc39;
border-radius: 5px;
font-size: 16px;
color: #fff;
letter-spacing: .26px;
margin-top: 30px;
}
.inp .go_login {
text-align: center;
font-size: 14px;
color: #9b9b9b;
letter-spacing: .26px;
padding-top: 20px;
}
.inp .go_login a {
color: #84cc39;
cursor: pointer;
}
#get_code {
border: 0;
width: 120px;
height: 30px;
background-color: antiquewhite;
outline: none;
}
#get_code:active {
color: white;
}
#checkbox {
width: 20px;
height: 20px;
}
.sms {
position: relative;
}
.sms .sms_btn {
position: absolute;
top: -12px;
right: 0;
bottom: 0;
margin: auto;
width: 130px;
text-align: center;
height: 24px;
color: #ff7000;
cursor: pointer;
border-left: 1px solid #999;
}
</style>
前台登录注销
登录注销 |
---|
components/Header.vue
代码语言:javascript复制<template>
<div class="header-box">
<div class="header">
<div class="content">
<div class="logo full-left">
<router-link to="/"><img @click="jump('/')" src="@/assets/img/logo.svg" alt=""></router-link>
</div>
<ul class="nav full-left">
<li><span @click="jump('/course')" :class="this_nav=='/course'?'this':''">免费课</span></li>
<li><span @click="jump('/light-course')" :class="this_nav=='/light-course'?'this':''">轻课</span></li>
<li><span>学位课</span></li>
<li><span>题库</span></li>
<li><span>老男孩教育</span></li>
</ul>
<div class="login-bar full-right">
<div class="shop-cart full-left">
<img src="@/assets/img/cart.svg" alt="">
<span><router-link to="/cart">购物车</router-link></span>
</div>
<div class="login-box full-left">
<span v-if="!token">
<router-link to="/login">登录</router-link>
|
<router-link to="/register">注册</router-link>
</span>
<span v-else>
<router-link to="/user">{{ username }}</router-link>
|
<i @click="logoutAction">注销</i>
</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Header",
data() {
return {
this_nav: "",
token: '',
}
},
created() {
this.this_nav = localStorage.this_nav;
this.token = sessionStorage.token || localStorage.token || '';
this.username = sessionStorage.user_name || localStorage.user_name || '';
},
methods: {
jump(location) {
localStorage.this_nav = location;
// vue-router除了提供router-link标签跳转页面以外,还提供了js跳转的方式
this.$router.push(location);
},
logoutAction() {
// 清除登录信息
sessionStorage.clear();
localStorage.clear();
this.token = '';
this.username = '';
}
}
}
</script>
<style scoped>
.header-box {
height: 80px;
}
.header {
width: 100%;
height: 80px;
box-shadow: 0 0.5px 0.5px 0 #c9c9c9;
position: fixed;
top: 0;
left: 0;
right: 0;
margin: auto;
z-index: 99;
background: #fff;
}
.header .content {
max-width: 1200px;
width: 100%;
margin: 0 auto;
}
.header .content .logo {
height: 80px;
line-height: 80px;
margin-right: 50px;
cursor: pointer;
}
.header .content .logo img {
vertical-align: middle;
}
.header .nav li {
float: left;
height: 80px;
line-height: 80px;
margin-right: 30px;
font-size: 16px;
color: #4a4a4a;
cursor: pointer;
}
.header .nav li span {
padding-bottom: 16px;
padding-left: 5px;
padding-right: 5px;
}
.header .nav li span a {
display: inline-block;
}
.header .nav li .this {
color: #4a4a4a;
border-bottom: 4px solid #ffc210;
}
.header .nav li:hover span {
color: #000;
}
.header .login-bar {
height: 80px;
}
.header .login-bar .shop-cart {
margin-right: 20px;
border-radius: 17px;
background: #f7f7f7;
cursor: pointer;
font-size: 14px;
height: 28px;
width: 88px;
margin-top: 30px;
line-height: 32px;
text-align: center;
}
.header .login-bar .shop-cart:hover {
background: #f0f0f0;
}
.header .login-bar .shop-cart img {
width: 15px;
margin-right: 4px;
margin-left: 6px;
}
.header .login-bar .shop-cart span {
margin-right: 6px;
}
.header .login-bar .login-box {
margin-top: 33px;
}
.header .login-bar .login-box span {
color: #4a4a4a;
cursor: pointer;
}
.header .login-bar .login-box span:hover {
color: #000000;
}
.full-left {
float: left !important;
}
.full-right {
float: right !important;
}
.el-carousel__arrow {
width: 120px;
height: 120px;
}
.el-checkbox__input.is-checked .el-checkbox__inner,
.el-checkbox__input.is-indeterminate .el-checkbox__inner {
background: #ffc210;
border-color: #ffc210;
border: none;
}
.el-checkbox__inner:hover {
border-color: #9b9b9b;
}
.el-checkbox__inner {
width: 16px;
height: 16px;
border: 1px solid #9b9b9b;
border-radius: 0;
}
.el-checkbox__inner::after {
height: 9px;
width: 5px;
}
a {
color: #333;
}
</style>
修改登录页面 |
---|
Login.vue
代码语言:javascript复制<template>
<div class="login box">
<img src="@/assets/img/Loginbg.jpg" alt="">
<div class="login">
<div class="login-title">
<img src="@/assets/img/Logotitle.png" alt="">
<p>帮助有志向的年轻人通过努力学习获得体面的工作和生活!</p>
</div>
<div class="login_box">
<div class="title">
<span :class="{active: a0}" @click="changeLogin(0)">密码登录</span>
<span :class="{active: a1}" @click="changeLogin(1)">短信登录</span>
</div>
<div class="inp" v-if="login_type===0">
<input v-model="username" type="text" placeholder="用户名 / 手机号码" class="user">
<input v-model="password" type="password" name="" class="pwd" placeholder="密码">
<div id="geetest1"></div>
<div class="rember">
<p>
<input id="checkbox" type="checkbox" class="no" v-model="remember"/>
<span>记住密码</span>
</p>
<p>忘记密码</p>
</div>
<button class="login_btn" @click="loginAction">登录</button>
<p class="go_login">没有账号 <router-link to="/register">立即注册</router-link></p>
</div>
<div class="inp" v-show="login_type===1">
<input v-model="mobile" type="text" placeholder="手机号码" class="user">
<div class="sms">
<input v-model="sms" type="text" placeholder="输入验证码" class="user">
<span class="sms_btn" @click="send_sms">{{sms_interval_tips}}</span>
</div>
<button class="login_btn" @click="loginMobile">登录</button>
<p class="go_login">没有账号 <router-link to="/register">立即注册</router-link></p>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Login',
data() {
return {
a0: 1,
a1: 0,
login_type: 0,
username: "",
password: "",
remember: false,
mobile: "",
sms: "",
is_send: false, // 是否在60s内发送了短信
sms_interval_tips: "获取验证码",
}
},
methods: {
changeLogin(i) {
this.login_type = i;
if (i) {
this.a0 = 0;
this.a1 = 1;
} else {
this.a0 = 1;
this.a1 = 0;
}
},
loginAction() {
if (!this.username || !this.password) {
return
}
this.$axios({
url: this.$settings.base_url '/user/login/',
method: 'post',
data: {
'usr': this.username,
'pwd': this.password
}
}).then((response) => {
// 判断用户是否要记住密码
// window.console.log(">>>>", response.data);
if (this.remember) { // 记住密码
sessionStorage.clear();
localStorage.token = response.data.token;
localStorage.user_name = response.data.results.username;
localStorage.user_mobile = response.data.results.mobile;
} else { /// 没记住密码
localStorage.clear();
sessionStorage.token = response.data.token;
sessionStorage.user_name = response.data.results.username;
sessionStorage.user_mobile = response.data.results.mobile;
}
// 页面跳转
this.$alert("欢迎回来!", "登录成功!", {
confirmButtonText: '确定',
callback: () => {
// 跳转页面
// this.$router.go(-1); // 返回上一页
// 进行制定的网站内部地址跳转
// this.$router.push("站内地址");
this.$router.push("/"); // 前往主页
}
})
}).catch(() => {
this.$alert("检查账号密码!", "登录失败!", {
confirmButtonText: '确定',
callback: () => {
this.username = '';
this.password = '';
}
});
})
},
send_sms() {
// 发送短信
if (!/^1[3-9]d{9}$/.test(this.mobile)) {
this.$message({
message: "对不起!手机号码格式有误!"
});
return false;
}
// 判断是否在60s内发送过短信
if (this.is_send) {
this.$message({
message: "对不起,不能频繁发送短信验证!"
});
return false;
}
// 请求发送短信
this.$axios({
url: this.$settings.base_url '/user/sms/',
method: 'post',
data: {
mobile: this.mobile
}
}).then(response => {
this.$message({
message: response.data.msg,
});
}).catch(error => {
this.$message({
message: error.response.data.result,
})
});
// 修改短信的发送状态
this.is_send = true;
// 设置间隔时间60s
let sms_interval_time = 60;
// 设置短信发送间隔倒计时,.60s后把is_send改成false
let timer = setInterval(() => {
if (sms_interval_time <= 1) {
clearInterval(timer);
this.sms_interval_tips = "获取验证码";
this.is_send = false; // 重新回复点击发送功能的条件
} else {
sms_interval_time -= 1;
this.sms_interval_tips = `${sms_interval_time}秒后再次获取`;
}
}, 1000);
},
loginMobile() {
// 注册信息提交
if (!/^1[3-9]d{9}$/.test(this.mobile)) {
this.$message({
message: "对不起!手机号码格式有误!"
});
return false;
}
if (this.sms.length < 1) {
this.$message({
message: "短信验证码不能为空!"
});
return false;
}
this.$axios({
url: this.$settings.base_url '/user/login/mobile/',
method: 'post',
data: {
mobile: this.mobile,
code: this.sms
}
}).then(response => {
let _this = this;
let status = response.data.status;
let msg = response.data.msg;
_this.$message({
message: msg,
duration: 1500,
onClose() {
if (status === 0) {
// 保存登录状态
sessionStorage.token = response.data.token;
sessionStorage.user_name = response.data.results.username;
sessionStorage.user_mobile = response.data.results.mobile;
// 跳转到主页
_this.$router.push('/');
} else {
// 清空数据库
sessionStorage.clear();
// 清空输入框
_this.mobile = '';
_this.sms = '';
}
}
});
}).catch(error => {
this.$message({
message: error.response.data.result
});
})
},
},
};
</script>
<style scoped>
.box {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
}
.box img {
width: 100%;
min-height: 100%;
}
.box .login {
position: absolute;
width: 500px;
height: 400px;
left: 0;
margin: auto;
right: 0;
bottom: 0;
top: -338px;
}
.login .login-title {
width: 100%;
text-align: center;
padding-top: 20px;
}
.login-title img {
width: 190px;
height: auto;
}
.login-title p {
font-family: PingFangSC-Regular;
font-size: 18px;
color: #fff;
letter-spacing: .29px;
padding-top: 10px;
padding-bottom: 50px;
}
.login_box {
width: 400px;
height: auto;
background: #fff;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .5);
border-radius: 4px;
margin: 0 auto;
padding-bottom: 40px;
}
.login_box .title {
font-size: 20px;
color: #9b9b9b;
letter-spacing: .32px;
border-bottom: 1px solid #e6e6e6;
display: flex;
justify-content: space-around;
padding: 50px 60px 0 60px;
margin-bottom: 20px;
cursor: pointer;
}
.login_box .title span.active {
color: #4a4a4a;
border-bottom: 2px solid #84cc39;
}
.inp {
width: 350px;
margin: 0 auto;
}
.inp input {
outline: 0;
width: 100%;
height: 45px;
border-radius: 4px;
border: 1px solid #d9d9d9;
text-indent: 20px;
font-size: 14px;
background: #fff !important;
}
.inp input.user {
margin-bottom: 16px;
}
.inp .rember {
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
margin-top: 10px;
}
.inp .rember p:first-of-type {
font-size: 12px;
color: #4a4a4a;
letter-spacing: .19px;
margin-left: 22px;
display: -ms-flexbox;
display: flex;
-ms-flex-align: center;
align-items: center;
/*position: relative;*/
}
.inp .rember p:nth-of-type(2) {
font-size: 14px;
color: #9b9b9b;
letter-spacing: .19px;
cursor: pointer;
}
.inp .rember input {
outline: 0;
width: 30px;
height: 45px;
border-radius: 4px;
border: 1px solid #d9d9d9;
text-indent: 20px;
font-size: 14px;
background: #fff !important;
}
.inp .rember p span {
display: inline-block;
font-size: 12px;
width: 100px;
/*position: absolute;*/
/*left: 20px;*/
}
#geetest {
margin-top: 20px;
}
.login_btn {
width: 100%;
height: 45px;
background: #84cc39;
border-radius: 5px;
font-size: 16px;
color: #fff;
letter-spacing: .26px;
margin-top: 30px;
}
.inp .go_login {
text-align: center;
font-size: 14px;
color: #9b9b9b;
letter-spacing: .26px;
padding-top: 20px;
}
.inp .go_login a {
color: #84cc39;
cursor: pointer;
}
#get_code {
border: 0;
width: 120px;
height: 30px;
background-color: antiquewhite;
outline: none;
}
#get_code:active {
color: white;
}
#checkbox {
width: 20px;
height: 20px;
}
.sms {
position: relative;
}
.sms .sms_btn {
position: absolute;
top: -12px;
right: 0;
bottom: 0;
margin: auto;
width: 130px;
text-align: center;
height: 24px;
color: #ff7000;
cursor: pointer;
border-left: 1px solid #999;
}
</style>
短信登录
后台路由 |
---|
from django.urls import path, re_path
from . import views
urlpatterns = [
path('mobile/', views.MobileAPIView.as_view()),
path('sms/', views.SMSAPIView.as_view()),
path('register/', views.RegisterCreateAPIView.as_view()),
path('login/', views.LoginAPIView.as_view()),
path('login/mobile/', views.LoginMobileAPIView.as_view()),
]
后台视图 |
---|
# 手机验证码登录
from rest_framework_jwt.serializers import jwt_payload_handler
from rest_framework_jwt.serializers import jwt_encode_handler
class LoginMobileAPIView(APIView):
authentication_classes = []
permission_classes = []
def post(self, request, *args, **kwargs):
mobile = request.data.get('mobile')
code = request.data.get('code')
if not mobile or not code:
return APIResponse(1, '数据有误')
old_code = cache.get(SMS_CACHE_KEY % {'mobile': mobile})
if code != old_code:
return APIResponse(1, '验证码错误')
try:
user = User.objects.get(mobile=mobile)
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
return APIResponse(token=token, results=serializers.LoginModelSerializer(user).data)
except:
return APIResponse(1, '用户不存在')
前台登录页 |
---|
<template>
<div class="login box">
<img src="@/assets/img/Loginbg.jpg" alt="">
<div class="login">
<div class="login-title">
<img src="@/assets/img/Logotitle.png" alt="">
<p>帮助有志向的年轻人通过努力学习获得体面的工作和生活!</p>
</div>
<div class="login_box">
<div class="title">
<span :class="{active: a0}" @click="changeLogin(0)">密码登录</span>
<span :class="{active: a1}" @click="changeLogin(1)">短信登录</span>
</div>
<div class="inp" v-if="login_type===0">
<input v-model="username" type="text" placeholder="用户名 / 手机号码" class="user">
<input v-model="password" type="password" name="" class="pwd" placeholder="密码">
<div id="geetest1"></div>
<div class="rember">
<p>
<input id="checkbox" type="checkbox" class="no" v-model="remember"/>
<span>记住密码</span>
</p>
<p>忘记密码</p>
</div>
<button class="login_btn" @click="loginAction">登录</button>
<p class="go_login">没有账号 <router-link to="/register">立即注册</router-link></p>
</div>
<div class="inp" v-show="login_type===1">
<input v-model="mobile" type="text" placeholder="手机号码" class="user">
<div class="sms">
<input v-model="sms" type="text" placeholder="输入验证码" class="user">
<span class="sms_btn" @click="send_sms">{{sms_interval_tips}}</span>
</div>
<button class="login_btn" @click="loginMobile">登录</button>
<p class="go_login">没有账号 <router-link to="/register">立即注册</router-link></p>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Login',
data() {
return {
a0: 1,
a1: 0,
login_type: 0,
username: "",
password: "",
remember: false,
mobile: "",
sms: "",
is_send: false, // 是否在60s内发送了短信
sms_interval_tips: "获取验证码",
}
},
methods: {
changeLogin(i) {
this.login_type = i;
if (i) {
this.a0 = 0;
this.a1 = 1;
} else {
this.a0 = 1;
this.a1 = 0;
}
},
loginAction() {
if (!this.username || !this.password) {
return
}
this.$axios({
url: this.$settings.base_url '/user/login/',
method: 'post',
data: {
'usr': this.username,
'pwd': this.password
}
}).then((response) => {
// 判断用户是否要记住密码
// window.console.log(">>>>", response.data);
if (this.remember) { // 记住密码
sessionStorage.clear();
localStorage.token = response.data.token;
localStorage.user_name = response.data.results.username;
localStorage.user_mobile = response.data.results.mobile;
} else { /// 没记住密码
localStorage.clear();
sessionStorage.token = response.data.token;
sessionStorage.user_name = response.data.results.username;
sessionStorage.user_mobile = response.data.results.mobile;
}
// 页面跳转
this.$alert("欢迎回来!", "登录成功!", {
confirmButtonText: '确定',
callback: () => {
// 跳转页面
// this.$router.go(-1); // 返回上一页
// 进行制定的网站内部地址跳转
// this.$router.push("站内地址");
this.$router.push("/"); // 前往主页
}
})
}).catch(() => {
this.$alert("检查账号密码!", "登录失败!", {
confirmButtonText: '确定',
callback: () => {
this.username = '';
this.password = '';
}
});
})
},
send_sms() {
// 发送短信
if (!/^1[3-9]d{9}$/.test(this.mobile)) {
this.$message({
message: "对不起!手机号码格式有误!"
});
return false;
}
// 判断是否在60s内发送过短信
if (this.is_send) {
this.$message({
message: "对不起,不能频繁发送短信验证!"
});
return false;
}
// 请求发送短信
this.$axios({
url: this.$settings.base_url '/user/sms/',
method: 'post',
data: {
mobile: this.mobile
}
}).then(response => {
this.$message({
message: response.data.msg,
});
}).catch(error => {
this.$message({
message: error.response.data.result,
})
});
// 修改短信的发送状态
this.is_send = true;
// 设置间隔时间60s
let sms_interval_time = 60;
// 设置短信发送间隔倒计时,.60s后把is_send改成false
let timer = setInterval(() => {
if (sms_interval_time <= 1) {
clearInterval(timer);
this.sms_interval_tips = "获取验证码";
this.is_send = false; // 重新回复点击发送功能的条件
} else {
sms_interval_time -= 1;
this.sms_interval_tips = `${sms_interval_time}秒后再次获取`;
}
}, 1000);
},
loginMobile() {
// 注册信息提交
if (!/^1[3-9]d{9}$/.test(this.mobile)) {
this.$message({
message: "对不起!手机号码格式有误!"
});
return false;
}
if (this.sms.length < 1) {
this.$message({
message: "短信验证码不能为空!"
});
return false;
}
this.$axios({
url: this.$settings.base_url '/user/login/mobile/',
method: 'post',
data: {
mobile: this.mobile,
code: this.sms
}
}).then(response => {
let _this = this;
let status = response.data.status;
let msg = response.data.msg;
_this.$message({
message: msg,
duration: 1500,
onClose() {
if (status === 0) {
// 保存登录状态
sessionStorage.token = response.data.token;
sessionStorage.user_name = response.data.results.username;
sessionStorage.user_mobile = response.data.results.mobile;
// 跳转到主页
_this.$router.push('/');
} else {
// 清空数据库
sessionStorage.clear();
// 清空输入框
_this.mobile = '';
_this.sms = '';
}
}
});
}).catch(error => {
this.$message({
message: error.response.data.result
});
})
},
},
};
</script>
<style scoped>
.box {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
}
.box img {
width: 100%;
min-height: 100%;
}
.box .login {
position: absolute;
width: 500px;
height: 400px;
left: 0;
margin: auto;
right: 0;
bottom: 0;
top: -338px;
}
.login .login-title {
width: 100%;
text-align: center;
padding-top: 20px;
}
.login-title img {
width: 190px;
height: auto;
}
.login-title p {
font-family: PingFangSC-Regular;
font-size: 18px;
color: #fff;
letter-spacing: .29px;
padding-top: 10px;
padding-bottom: 50px;
}
.login_box {
width: 400px;
height: auto;
background: #fff;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .5);
border-radius: 4px;
margin: 0 auto;
padding-bottom: 40px;
}
.login_box .title {
font-size: 20px;
color: #9b9b9b;
letter-spacing: .32px;
border-bottom: 1px solid #e6e6e6;
display: flex;
justify-content: space-around;
padding: 50px 60px 0 60px;
margin-bottom: 20px;
cursor: pointer;
}
.login_box .title span.active {
color: #4a4a4a;
border-bottom: 2px solid #84cc39;
}
.inp {
width: 350px;
margin: 0 auto;
}
.inp input {
outline: 0;
width: 100%;
height: 45px;
border-radius: 4px;
border: 1px solid #d9d9d9;
text-indent: 20px;
font-size: 14px;
background: #fff !important;
}
.inp input.user {
margin-bottom: 16px;
}
.inp .rember {
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
margin-top: 10px;
}
.inp .rember p:first-of-type {
font-size: 12px;
color: #4a4a4a;
letter-spacing: .19px;
margin-left: 22px;
display: -ms-flexbox;
display: flex;
-ms-flex-align: center;
align-items: center;
/*position: relative;*/
}
.inp .rember p:nth-of-type(2) {
font-size: 14px;
color: #9b9b9b;
letter-spacing: .19px;
cursor: pointer;
}
.inp .rember input {
outline: 0;
width: 30px;
height: 45px;
border-radius: 4px;
border: 1px solid #d9d9d9;
text-indent: 20px;
font-size: 14px;
background: #fff !important;
}
.inp .rember p span {
display: inline-block;
font-size: 12px;
width: 100px;
/*position: absolute;*/
/*left: 20px;*/
}
#geetest {
margin-top: 20px;
}
.login_btn {
width: 100%;
height: 45px;
background: #84cc39;
border-radius: 5px;
font-size: 16px;
color: #fff;
letter-spacing: .26px;
margin-top: 30px;
}
.inp .go_login {
text-align: center;
font-size: 14px;
color: #9b9b9b;
letter-spacing: .26px;
padding-top: 20px;
}
.inp .go_login a {
color: #84cc39;
cursor: pointer;
}
#get_code {
border: 0;
width: 120px;
height: 30px;
background-color: antiquewhite;
outline: none;
}
#get_code:active {
color: white;
}
#checkbox {
width: 20px;
height: 20px;
}
.sms {
position: relative;
}
.sms .sms_btn {
position: absolute;
top: -12px;
right: 0;
bottom: 0;
margin: auto;
width: 130px;
text-align: center;
height: 24px;
color: #ff7000;
cursor: pointer;
border-left: 1px solid #999;
}
</style>
短信接口频率限制
重写频率限制 |
---|
user/thorttles.py
代码语言:javascript复制from rest_framework.throttling import SimpleRateThrottle
class SMSRateThrottle(SimpleRateThrottle):
scope = 'sms'
def get_cache_key(self, request, view):
mobile = request.data.get('mobile') or request.query_params.get('mobile')
if not mobile:
return None
return self.cache_format % {'scope': self.scope, 'ident': mobile}
视图层 |
---|
from rest_framework.views import APIView
from .models import User
from utils.repsonse import APIResponse
import re
# 注册逻辑:1.校验手机号是否存在 2.发送验证码 3.完成注册
class MobileAPIView(APIView):
def post(self, request, *args, **kwargs):
mobile = request.data.get('mobile')
if not mobile or not re.match(r'^1[3-9]d{9}$', mobile):
return APIResponse(1, '数据有误')
try:
User.objects.get(mobile=mobile)
return APIResponse(2, '已注册')
except:
return APIResponse(0, '未注册')
# 发送验证码接口分析
from libs import txsms
from django.core.cache import cache
from settings.const import SMS_EXP, SMS_CACHE_KEY
from .thorttles import SMSRateThrottle
class SMSAPIView(APIView):
# 频率限制
throttle_classes = [SMSRateThrottle]
def post(self, request, *args, **kwargs):
# 1)拿到前台的手机号
mobile = request.data.get('mobile')
if not mobile or not re.match(r'^1[3-9]d{9}$', mobile):
return APIResponse(2, '数据有误')
# 2)调用txsms生成手机验证码
code = txsms.get_code()
# 3)调用txsms发送手机验证码
result = txsms.send_sms(mobile, code, SMS_EXP // 60)
# 4)失败反馈信息给前台
if not result:
return APIResponse(1, '短信发送失败')
# 5)成功服务器缓存手机验证码 - 用缓存存储(方便管理) - redis
cache.set(SMS_CACHE_KEY % {'mobile': mobile}, code, SMS_EXP)
# 6)反馈成功信息给前台
return APIResponse(0, '短信发送成功')
from rest_framework.generics import CreateAPIView
from . import serializers
class RegisterCreateAPIView(CreateAPIView):
# queryset = User.objects.filter(is_active=True)
serializer_class = serializers.RegisterModelSerializer
# 自定义响应结果
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True) # 校验失败就主动抛异常 => 自定义异常结果,配置异常模块
user_obj = serializer.save() # 要自定义入库逻辑,重写create方法
headers = self.get_success_headers(serializer.data)
# 响应结果需要格式化,使用序列化类要提供序列化与反序列化两套规则
return APIResponse(0, 'ok',
results=serializers.RegisterModelSerializer(user_obj).data,
http_status=201,
headers=headers
)
# 多方式登录
class LoginAPIView(APIView):
# 1) 禁用认证与权限组件
authentication_classes = []
permission_classes = []
def post(self, request, *args, **kwargs):
# 2) 拿到前台登录信息,交给序列化类,规则:账号用usr传,密码用pwd传
user_ser = serializers.LoginModelSerializer(data=request.data)
# 3) 序列化类校验得到登录用户与token存放在序列化对象中
user_ser.is_valid(raise_exception=True)
# 4) 取出登录用户与token返回给前台
return APIResponse(token=user_ser.token, results=serializers.LoginModelSerializer(user_ser.user).data)
# 手机验证码登录
from rest_framework_jwt.serializers import jwt_payload_handler
from rest_framework_jwt.serializers import jwt_encode_handler
class LoginMobileAPIView(APIView):
authentication_classes = []
permission_classes = []
def post(self, request, *args, **kwargs):
mobile = request.data.get('mobile')
code = request.data.get('code')
if not mobile or not code:
return APIResponse(1, '数据有误')
old_code = cache.get(SMS_CACHE_KEY % {'mobile': mobile})
if code != old_code:
return APIResponse(1, '验证码错误')
try:
user = User.objects.get(mobile=mobile)
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
return APIResponse(token=token, results=serializers.LoginModelSerializer(user).data)
except:
return APIResponse(1, '用户不存在')
django配置 |
---|
settings/dev.py
代码语言:javascript复制# drf配置
from rest_framework import settings
REST_FRAMEWORK = {
# 异常配置
'EXCEPTION_HANDLER': 'utils.exception.exception_handler',
# 频率限制配置
'DEFAULT_THROTTLE_RATES': {
'user': None,
'anon': None,
'sms': '1/m',
},
}
接口缓存
接口每次访问都会去查数据库 ,这样数据库的压力会很大,所以我们要对接口进行缓存。
home视图 |
---|
from rest_framework.generics import ListAPIView
from utils.repsonse import APIResponse
from . import models, serializers
from django.core.cache import cache
from rest_framework.response import Response
class BannerListAPIView(ListAPIView):
queryset = models.Banner.objects.filter(is_delete=False, is_show=True).order_by('-orders')
serializer_class = serializers.BannerModelSerializer
# 找缓存,缓存有走缓存,缓存没有走数据库
def list(self, request, *args, **kwargs):
banner_data = cache.get('banner_list')
if not banner_data:
print('走数据库')
response = super().list(request, *args, **kwargs)
banner_data = response.data
# 建立缓存
cache.set('banner_list', banner_data)
return Response(banner_data)