路飞项目搭建4 Django缓存, 腾讯云短信验证码接口, 短信登录接口, 短信注册接口, 前台登录接口
# 1)导入缓存功能 from django.core.cache import cache # Django缓存默认采用内嵌的memcache数据库 - 每次重启项目,缓存重置
# redis数据库, 可以替代memcache数据库作为Django的缓存数据库,防止重启缓存消失
# 2)设置,如果将exp过期时间设置0或负值,就是删除缓存 cache.set(key, value, exp) # 3)获取 cache.get(key)
path('sms/', views.SMSViewSet.as_view({'get': 'send'})),
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, }
# 短信验证码缓存key SMS_CACHE_KEY = 'sms_cache_%(mobile)s' # 特定%s占位符,只能替换mobile变量 # 短信验证码缓存时间s SMS_CACHE_TIME = 300
REST_FRAMEWORK = { 'EXCEPTION_HANDLER': 'utils.exception.exception_handler', 'DEFAULT_THROTTLE_RATES': { 'sms': '1/min' } }
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)
短信登录接口
path('mobile/login/', views.MobileLoginViewSet.as_view({'post': 'login'})),
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
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)
短信注册接口
router.register('register', views.RegisterViewSet, 'register')
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) # 注意此处重写是为了保证数据入库,密码为密文保存
# 手机验证码注册 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)
前台登录接口
<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>
<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>
<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>
""" 1)redis服务启动: 终端在哪个目录下启动是有目的的:日志文件、数据持久化文件(快照) 不管前台还是后台启动,要通过一个可控的配置文件 2)redis配置:ip、port、dbcount、log、rdb、requirepass 3)reids数据类型:字符串、列表、哈希、集合、有序集合 4)python中使用redis:pip install redis 5)django缓存配置redis:pip install django-redis """