day79:luffy:注册之对手机号的验证&实现基本的注册功能逻辑&点击获取验证码&redis

时间:2022-06-24 17:23:04

目录

1.前端和后端对于手机号的验证

2.实现基本的注册功能-不包括验证码

3.点击获取验证码

4.解决登录不上Xadmin的bug

5.redis

register.vue页面

<template>
<div class="login box">
<img src="../../static/img/Loginbg.3377d0c.jpg" alt="">
<div class="login">
<div class="login-title">
<img src="../../static/img/Logotitle.1ba5466.png" alt="">
<p>帮助有志向的年轻人通过努力学习获得体面的工作和生活!</p>
</div>
<div class="login_box">
<div class="title">
<span @click="login_type=0">密码登录</span>
<span @click="login_type=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 type="checkbox" class="no" name="a" v-model="remember"/>
<span>记住密码</span>
</p>
<p>忘记密码</p>
</div>
<button class="login_btn" @click="loginHandle">登录</button>
<p class="go_login" >没有账号 <router-link to="/user/register">立即注册</router-link></p>
</div>
<div class="inp" v-show="login_type==1">
<input v-model = "username" type="text" placeholder="手机号码" class="user">
<input v-model = "password" type="text" class="pwd" placeholder="短信验证码">
<button id="get_code">获取验证码</button>
<button class="login_btn">登录</button>
<p class="go_login" >没有账号 <span>立即注册</span></p>
</div>
</div>
</div>
</div>
</template> <script>
export default {
name: 'Login',
data(){
return {
login_type: 0,
username:"",
password:"",
remember:false,
}
}, methods:{
loginHandle(){
var captcha1 = new TencentCaptcha('2095582704', (res) =>{
if (res.ret === 0){ this.$axios.post(`${this.$settings.Host}/users/login/`,{
username:this.username,
password:this.password,
ticket:res.ticket,
randstr:res.randstr, }).then((res)=>{
console.log(res);
// console.log(this.remember);
if (this.remember){
localStorage.token = res.data.token;
localStorage.username = res.data.username;
localStorage.id = res.data.id;
sessionStorage.removeItem('token');
sessionStorage.removeItem('username');
sessionStorage.removeItem('id'); }else {
sessionStorage.token = res.data.token;
sessionStorage.username = res.data.username;
sessionStorage.id = res.data.id;
localStorage.removeItem('token');
localStorage.removeItem('username');
localStorage.removeItem('id');
} this.$confirm('下一步想去哪消费!', '提示', {
confirmButtonText: '去首页',
cancelButtonText: '去个人中心',
type: 'success'
}).then(() => {
this.$router.push('/');
}).catch(() => {
this.$router.push('/person');
}); }).catch((error)=>{
this.$alert('用户名或者密码错误', '登录失败', {
confirmButtonText: '确定', });
})
}
});
captcha1.show(); // 显示验证码 } }, };
</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;
top: 0;
left: 0;
margin: auto;
right: 0;
bottom: 0;
top: -338px;
}
.login .login-title{
width: 100%;
text-align: center;
}
.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:nth-of-type(1){
color: #4a4a4a;
border-bottom: 2px solid #84cc39;
} .inp{
width: 350px;
margin: 0 auto;
}
.inp input{
border: 0;
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 span{
color: #84cc39;
cursor: pointer;
}
</style>

1.前端和后端对于手机号的验证

1.将注册的几个输入框和js中的数据设置好数据驱动视图 :v-model

// 修改input框中的相关的值
<input v-model = "mobile" type="text" placeholder="手机号码" class="user" @blur="checkPhone">
<input v-model = "password" type="password" placeholder="密码" class="user">
<input v-model = "r_password" type="password" placeholder="确认密码" class="user"> // 修改js中data的相关值
data(){
return {
sms:"",
mobile:"",
password:"",
r_password:"",
validateResult:false,
}

2.前端对输入的手机号长度做校验

给手机号的输入框绑定一个鼠标离开的事件,当用户输入手机号之后离开输入框,会对手机号格式进行校验

<!-- html -->
<input v-model = "mobile" type="text" placeholder="手机号码" class="user" @blur="checkPhone">
// js
methods:{
checkPhone(){ let phoneNumber = this.mobile;
// 前端
let reg = /^1[3-9][0-9]{9}$/;
if (!reg.test(phoneNumber)){ // 前端检测手机号长度是否符合要求
this.$message.error('手机号 格式 error');
return false;
}
// 如果前端验证手机号长度没问题,就向后端发起请求
this.$axios.get(`${this.$settings.Host}/users/check_phone/?phone=${phoneNumber}`) // request.GET.get(phone)
.then((res)=>{
console.log(res);
}).catch((error)=>{
this.$message.error(error.response.data.error_msg);
}) },

关于手机号的校验:现在我们想达到的效果是:在前端进行对手机号的格式校验,只要用户输入的手机号格式有问题,会立刻出现弹窗:手机号格式错误。(且前端只能对手机号的格式进行校验)

比如:你想验证手机号是不是已经存在 这种逻辑就没有办法放到前端去做,因为手机号都存储在数据库中,所以如果想验证手机号是否存在,肯定是后端要做相关逻辑。

也就是:

前端验证手机号格式

后端验证手机号格式加手机号是否已经存在

3.后端设置接口:验证手机号格式和手机号唯一性

users/urls.py

# urls.py
from rest_framework_jwt.views import obtain_jwt_token,verify_jwt_token
from django.urls import path
from . import views urlpatterns = [
......
path(r'/check_phone/',views.CheckPhoneNumber.as_view())
]

users/views.py

# views.py

import re
from rest_framework.views import APIView
from rest_framework.response import Response
from .utils import get_user_obj ...... class CheckPhoneNumber(APIView): def get(self,request):
phone_number = request.GET.get('phone') # 验证手机号格式
if not re.match('^1[3-9][0-9]{9}$', phone_number): # 格式不对
return Response({'error_msg':'手机号格式有误,请重新输入!'}, status=status.HTTP_400_BAD_REQUEST) # 验证手机号唯一性
ret = get_user_obj(phone_number)
if ret:
return Response({'error_msg': '手机号已被注册,请换手机号'}, status=status.HTTP_400_BAD_REQUEST) return Response({'msg': 'ok'})

现在前端和后端的校验都已经编写完毕,让我们看一下效果

day79:luffy:注册之对手机号的验证&实现基本的注册功能逻辑&点击获取验证码&redis

day79:luffy:注册之对手机号的验证&实现基本的注册功能逻辑&点击获取验证码&redis

2.实现基本的注册功能-不包括验证码

1.注册功能的流程

day79:luffy:注册之对手机号的验证&实现基本的注册功能逻辑&点击获取验证码&redis

2.注册-前端

点击注册按钮,将输入框中的数据发送到后端

<!-- html -->
<button class="register_btn" @click="registerHandler">注册</button>
// js
methods:{ ......
registerHandler(){ this.$axios.post(`${this.$settings.Host}/`,{
// 将输入框的四项一并提交到后台
sms:this.sms,
mobile:this.mobile,
password:this.password,
r_password:this.r_password,
}).then((res)=>{ }).catch((error)=>{ }) },

3.注册-后端接口

users/urls.py

# urls.py
from rest_framework_jwt.views import obtain_jwt_token,verify_jwt_token
from django.urls import path
from . import views urlpatterns = [
......
path(r'register/', views.RegisterView.as_view()),
]

users/views.py

# views.py
class RegisterView(CreateAPIView):
queryset = models.User.objects.all()
serializer_class = RegisterModelSerializer

user/serializers.py

反序列化需要的字段:phone password r_password sms

序列化需要的字段:id token phone

反序列化之后需要存到数据库的字段:phone password

# serializers.py
class RegisterModelSerializer(serializers.ModelSerializer):
'''
sms,r_password这两个字段只需要write_only=True
意思是:反序列化校验的时候,这两个字段必须要传递给我
但是序列化返回数据的时候,这两个字段是不序列化出来的 id,token这两个字段只需要read_only=True
意思是:反序列化校验的时候,这些字段是无需校验的
但是序列化返回数据的时候,必须要返回这两个数据
'''
id = serializers.IntegerField(read_only=True) # sms和r_password这两个字段数据库并没有,但是反序列化的时候还需要校验这两个字段,所以需要在这写这两个字段
# 将两个字段设置为write_only=True的原因是因为:验证码和确认密码这两个字段只做反序列化校验功能,不需要序列化返回到前端
sms = serializers.CharField(max_length=6, min_length=4, write_only=True) # '3333'
r_password = serializers.CharField(write_only=True) token = serializers.CharField(read_only=True) # 后端需要给前端传递token class Meta:
model = models.User
fields = ['id', 'phone', 'password', 'r_password', 'sms', 'token']
extra_kwargs = {
'password': {'write_only': True},
} # 校验密码和确认密码
def validate(self, attrs):
# 校验手机号
phone_number = attrs.get('phone') # 后端校验手机号格式是否正确
if not re.match('^1[3-9][0-9]{9}$', phone_number):
raise serializers.ValidationError('手机号格式不对') # 验证手机号是否已经存在
ret = get_user_obj(phone_number)
if ret:
raise serializers.ValidationError('手机号已经存在!!')
p1 = attrs.get('password')
p2 = attrs.get('r_password') # 验证密码和确认密码是否一致
if p1 != p2:
raise serializers.ValidationError('两次密码不一致,请核对') # todo 校验验证码[待做] return attrs '''
重写create方法:因为序列化器中有的字段是不需要反序列化存到数据库中 '''
def create(self, validated_data): # 确认密码和验证码不需要存到数据库中,所以要剔除它们
validated_data.pop('r_password')
validated_data.pop('sms') # 密码加密
hash_password = make_password(validated_data['password'])
validated_data['password'] = hash_password # 以上处理好的数据存到数据库中
user = models.User.objects.create(
**validated_data
)
user.token = '123' # 硬加上一个token return user # 返回user对象时,就会有token这个属性了

3.点击获取验证码

1.点击获取验证码的原理

day79:luffy:注册之对手机号的验证&实现基本的注册功能逻辑&点击获取验证码&redis

2.点击获取验证码-配置

1.安装django-redis

pip install django-redis

2.dev.py文件做相关配置

# dev.py

# 设置redis缓存
CACHES = {
# 默认缓存
"default": {
"BACKEND": "django_redis.cache.RedisCache",
# 项目上线时,需要调整这里的路径
"LOCATION": "redis://127.0.0.1:6379/0", "OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
}
},
# 提供给xadmin或者admin的session存储
"session": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/1",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
}
},
# 提供存储短信验证码
"sms_code":{
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/2",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
},
}
} # 设置xadmin用户登录时,登录信息session保存到redis
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "session"

3.点击获取验证码-前端

<!-- html -->
<button style="width: 34%;height: 41px;" @click="getSmsCode">点击获取验证码</button>
// js
getSmsCode(){
this.$axios.get(`${this.$settings.Host}/`)
}

4.点击获取验证码-后端接口

users/urls.py

# urls.py
from rest_framework_jwt.views import obtain_jwt_token, verify_jwt_token
from . import views
from django.urls import path urlpatterns = [
......
path(r'sms_code/(?P<phone>^1[3-9][0-9]{9}$)/', views.GetSMSCodeView.as_view()), ]

users/views.py

# views.py
from django_redis import get_redis_connection
class GetSMSCodeView(APIView): def get(self,request,phone): # 验证是否已经发送过短信了
conn = get_redis_connection('sms_code')
ret = conn.get('mobile_interval_%s'%phone)
if ret:
return Response({'msg':'60秒内已经发送过了,别瞎搞'}, status=status.HTTP_400_BAD_REQUEST) # 生成验证码
sms_code = "%06d" % random.randint(0,999999) # 保存验证码
'''
验证码是存到redis里的,因为验证码有时限,所以用setex将验证码存入
存入方式是以键值对存入的:
key:手机号 value:验证码
'''
conn.setex('mobile_%s'%phone, sms_code, contains.SMS_CODE_EXPIRE_TIME) # 设置有效期 #
conn.setex('mobile_interval_%s'%phone, sms_code, contains.SMS_CODE_INTERVAL_TIME) # 设置发送短信的时间间隔 # todo 发送验证码 return Response({'msg':'ok'})

两个常量放到constant.py中

settings/constant.py

# settings/constant.py
SMS_CODE_EXPIRE_TIME = 60*10 # 有效时间
SMS_CODE_INTERVAL_TIME = 60 # 间隔时间

4.解决登录不上Xadmin的bug

登录不上Xadmin的原因是:

之前我们设置了Xadmin使用咱们自己创建的用户表,而不是使用Xadmin原先自带的表:传送门:Xadmin使用自己创建的用户表

而在昨天我们又重写了jwt代码,让其除了验证用户名和密码还要验证ticket票据:传送门:重写jwt代码来实现对滑动成功的认证

这样的话就会导致我们在登录Xadmin的时候,也会验证ticket票据,所以会登录失败。

所以我们做一个if判断:

当ticket有值的时候才走滑动成功的认证:验证用户名 密码 ticket randstr

当ticket没有值的时候,就做常规认证,只验证用户名和密码即可

users/utils.py

# users/utils.py
class CustomeModelBackend(ModelBackend): def authenticate(self, request, username=None, password=None, **kwargs):
try:
user_obj = get_user_obj(username) '''
看是否有票据信息,如果没有,直接走下面的代码(Xadmin用户名密码登录)
如果有票据信息,走if里面的代码 对票据进行验证
这样的话xadmin就不会因为走票据信息这套逻辑导致登录失败了
'''
if kwargs.get('ticket'): ticket = kwargs.get('ticket') userip = request.META['REMOTE_ADDR']
randstr = kwargs.get('randstr')
print('userip:', userip) params = {
"aid": settings.FSQ.get('appid'),
"AppSecretKey": settings.FSQ.get('app_serect_key'),
"Ticket": ticket,
"Randstr": randstr,
"UserIP": userip
}
params = urlencode(params).encode() url = settings.FSQ.get('URL') f = urlopen(url, params) content = f.read()
res = json.loads(content)
print(res) # {'response': '1', 'evil_level': '0', 'err_msg': 'OK'}
if res.get('response') != '1':
return None if user_obj:
if user_obj.check_password(password):
return user_obj else:
return None
except Exception:
logger.error('验证过程代码有误,请联系管理员')
return None

5.redis

1.redis简要介绍

mysql的表类型[表引擎,存储引擎],memory 表结构和表数据分开存储的,表结构保存在硬盘中,表数据保存在内存中.

memcache 一款软件,可以使用键值对的格式,保存数据到内存中.

redis是意大利的工程师开发的开源\免费的高速内存缓存数据库.需要注意的是,作者本身是只开发了linux版本的redis数据库.

window系统的redis是微软团队根据官方的linux版本高仿的

官方原版: https://redis.io/

中文官网:http://www.redis.cn

参考命令:http://doc.redisfans.com/

2.redis的下载和安装

下载地址: https://github.com/MicrosoftArchive/redis/releases

使用以下命令启动redis服务端[配置文件路径根据自己的摆放位置改动]

redis-server C:/tool/redis/redis.windows.conf

redis作为windows服务启动方式

redis-server --service-install redis.windows.conf

启动服务:redis-server --service-start

停止服务:redis-server --service-stop

3.redis的使用

Redis 是一个高性能的key-value数据格式的内存缓存,NoSQL数据库。

NOSQL:not only sql,泛指非关系型数据库。

关系型数据库: (mysql, oracle, sql server, sqlite, db2)

1. 数据存放在表中,表之间有关系。
2. 通用的SQL操作语言。
3. 大部分支持事务。

非关系型数据库:[ redis,hadoop,mangoDB]:

1. 没有数据表的概念,不同的nosql数据库存放数据位置不同。
2. nosql数据库没有通用的操作语言。
3. 基本不支持事务。 redis支持简单事务

redis:内存型(数据存放在内存中)的非关系型(nosql)key-value(键值存储)数据库,支持数据的持久化(注: 数据持久化时将数据存放到文件中,每次启动redis之后会先将文件中数据加载到内存),经常用来做缓存(用来缓存一些经常用到的数据,提高读写速度)。

redis是一款基于CS架构的数据库,所以redis有客户端,也有服务端。

其中,客户端可以使用python等编程语言,也可以终端命令行工具

redis客户端连接服务器:

redis-cli -h `redis服务器ip` -p `redis服务器port`  #6379

4.redis数据类型

# 1. string类型:
字符串类型是 Redis 中最为基础的数据存储类型,它在 Redis 中是二进制安全的,也就是byte类型
最大容量是512M。
key: string

# 2. hash类型:
hash用于存储对象,对象的结构为属性、值,值的类型为string。
key:{
域:值[这里的值只能是字符串],
域:值,
域:值,
域:值,
...
}

# 3. list类型:
列表的元素类型为string。
key:[ 值1,值2,值3..... ]

# 4. set类型:
无序集合,元素为string类型,元素唯一不重复,没有修改操作。
key: {值1,值4,值3,值5,....}

# 5. zset类型[sortset]:
有序集合,元素为string类型,元素唯一不重复,有修改操作。
key:{
值: 权重值,
值: 权重值,
}

5.String

如果设置的键不存在则为添加,如果设置的键已经存在则修改

  • 设置键值

    set key value

  • 例1:设置键为name值为xiaoming的数据

    set name xiaoming

  • 设置键值及过期时间,以秒为单位

    setex key seconds value

  • 例2:设置键为aa值为aa过期时间为3秒的数据

    setex name 20 xiaoming

关于设置保存数据的有效期

# setex 添加保存数据到redis,同时设置有效期
格式:
setex key time value
# expire 给已有的数据重新设置有效期
格式:
expire key time
  • 设置多个键值

    mset key1 value1 key2 value2 ...

  • 例3:设置键为a1值为python、键为a2值为java、键为a3值为c

    mset a1 python a2 java a3 c

  • 追加值

    append key value

  • 例4:向键为a1中追加值haha,a1='xx'

    append a1 haha

    a1= 'xxhaha'

  • 获取:根据键获取值,如果不存在此键则返回nil

    get key

  • 例5:获取键name的值

    get name

  • 根据多个键获取多个值

    mget key1 key2 ...

  • 例6:获取键a1、a2、a3的值

    mget a1 a2 a3

6.键操作

  • 查找键,参数⽀持正则表达式

    keys pattern

  • 例1:查看所有键

    keys *

  • 例2:查看名称中包含a的键

    keys a*

  • 判断键是否存在,如果存在返回1,不存在返回0

    exists key1

  • 例3:判断键a1是否存在

    exists a1

  • 查看键对应的value的类型

    type key

  • 例4:查看键a1的值类型,为redis⽀持的五种类型中的⼀种

    type a1

  • 删除键及对应的值

    del key1 key2 ...

  • 例5:删除键a2、a3

    del a2 a3

  • 查看有效时间,以秒为单位

    ttl key

  • 例7:查看键bb的有效时间

    ttl bb

7.hash

# 结构:

键key:{
域field:值value
}
  • 设置单个属性

    hset key field value

  • 例1:设置键 user的属性namexiaohong

    hset user name xiaohong

  • 设置多个属性

    hmset key field1 value1 field2 value2 ...

  • 例2:设置键u2的属性namexiaohong、属性age11

    hmset u2 name xiaohong age 11

  • 获取指定键所有的属性

    hkeys key

  • 例3:获取键u2的所有属性

    hkeys u2

  • 获取⼀个属性的值

    hget key field

  • 例4:获取键u2属性name的值

    hget u2 name

  • 获取多个属性的值

    hmget key field1 field2 ...

  • 例5:获取键u2属性nameage的值

    hmget u2 name age

  • 获取所有属性的值

    hvals key

  • 例6:获取键u2所有属性的值

    hvals u2

  • 删除属性,属性对应的值会被⼀起删除

    hdel key field1 field2 ...

  • 例7:删除键u2的属性age

    hdel u2 age

8.set

可删除不可修改

  • 添加元素

    sadd key member1 member2 ...

  • 例1:向键a3的集合中添加元素zhangsanlisiwangwu

    sadd a3 zhangsan sili wangwu

  • 返回所有的元素

    smembers key

  • 例2:获取键a3的集合中所有元素

    smembers a3

  • 删除指定元素

    srem key value

  • 例3:删除键a3的集合中元素wangwu

    srem a3 wangwu

9.list

列表的元素类型为string

按照插⼊顺序排序

  • 在左侧插⼊数据

    lpush key value1 value2 ...

  • 例1:从键为a1的列表左侧加⼊数据a 、 b 、c

    lpush a1 a b c

  • 在右侧插⼊数据

    rpush key value1 value2 ...

  • 例2:从键为a1的列表右侧加⼊数据0、1

    rpush a1 0 1

  • 在指定元素的前或后插⼊新元素

    linsert key before或after 现有元素 新元素

  • 例3:在键为a1的列表中元素b前加⼊3

    linsert a1 before b 3

设置指定索引位置的元素值

  • 索引从左侧开始,第⼀个元素为0

  • 索引可以是负数,表示尾部开始计数,如-1表示最后⼀个元素

    lset key index value

  • 例5:修改键为a1的列表中下标为1的元素值为z

    lset a 1 z

  • 删除指定元素

    • 将列表中前count次出现的值为value的元素移除

    • count > 0: 从头往尾移除

    • count < 0: 从尾往头移除

    • count = 0: 移除所有

     lrem key count value

  • 例6.2:从a2列表右侧开始删除2个b

    lrem a2 -2 b

  • 例6.3:查看列表a2的所有元素

    lrange a2 0 -1