用户的注册认证
前端显示注册页面并调整首页头部和登陆页面的注册按钮的链接。
注册页面 Register.vue
,主要是通过登录页面进行改进而成:
<template>
<div class="sign">
<div class="logo"><a href="/"><img src="/static/image/nav-logo.png" alt="Logo"></a></div>
<div class="main">
<h4 class="title">
<div class="normal-title">
<router-link to="/user/login">登录</router-link>
<b>·</b>
<router-link id="js-sign-up-btn" class="active" to="/user/register">注册</router-link>
</div>
</h4>
<div class="js-sign-up-container">
<form class="new_user" id="new_user" action="" accept-charset="UTF-8" method="post">
<div class="input-prepend restyle">
<input placeholder="你的昵称" type="text" value="" v-model="nickname" id="user_nickname">
<i class="iconfont ic-user"></i>
</div>
<div class="input-prepend restyle no-radius js-normal">
<input placeholder="手机号" type="tel" v-model="mobile" id="user_mobile_number">
<i class="iconfont ic-phonenumber"></i>
</div>
<div class="input-prepend restyle no-radius security-up-code js-security-number" v-if="is_show_sms_code">
<input type="text" v-model="sms_code" id="sms_code" placeholder="手机验证码">
<i class="iconfont ic-verify"></i>
<a tabindex="-1" class="btn-up-resend js-send-code-button disable" href="javascript:void(0);" id="send_code">{{sms_code_text}}</a>
</div>
<input type="hidden" name="security_number" id="security_number">
<div class="input-prepend">
<input placeholder="设置密码" type="password" v-model="password" id="user_password">
<i class="iconfont ic-password"></i>
</div>
<input type="submit" name="commit" value="注册" class="sign-up-button" id="sign_up_btn" data-disable-with="注册">
<p class="sign-up-msg">点击 “注册” 即表示您同意并愿意遵守荏苒<br> <a target="_blank" href="">用户协议</a> 和 <a target="_blank" href="">隐私政策</a> 。</p>
</form>
<!-- 更多注册方式 -->
<div class="more-sign">
<h6>社交帐号直接注册</h6>
<ul>
<li><a id="weixin" class="weixin" target="_blank" href=""><i class="iconfont ic-wechat"></i></a></li>
<li><a id="qq" class="qq" target="_blank" href=""><i class="iconfont ic-qq_connect"></i></a></li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Register",
data(){
return {
nickname:"",
mobile:"",
sms_code:"",
password:"",
sms_code_text:"发送验证码",
is_show_sms_code:false,
}
},
watch:{
mobile(){
if(/^1[3-9]\d{9}$/.test(this.mobile)){
this.is_show_sms_code = true;
}else{
this.is_show_sms_code = false;
}
}
}
}
</script>
<style scoped>
input{
outline: none;
}
*, :after, :before {
box-sizing: border-box;
}
.sign {
height: 100%;
min-height: 750px;
text-align: center;
font-size: 14px;
background-color: #f1f1f1
}
.sign:before {
content: "";
display: inline-block;
height: 85%;
vertical-align: middle
}
.sign .disable,.sign .disable-gray {
opacity: .5;
pointer-events: none
}
.sign .disable-gray {
background-color: #969696
}
.sign .tooltip-error {
font-size: 14px;
line-height: 25px;
white-space: nowrap;
background: none
}
.sign .tooltip-error .tooltip-inner {
max-width: 280px;
color: #333;
border: 1px solid #ea6f5a;
background-color: #fff
}
.sign .tooltip-error .tooltip-inner i {
position: static;
margin-right: 5px;
font-size: 20px;
color: #ea6f5a;
vertical-align: middle
}
.sign .tooltip-error .tooltip-inner span {
vertical-align: middle;
display: inline-block;
white-space: normal;
max-width: 230px
}
.sign .tooltip-error.right .tooltip-arrow-border {
border-right-color: #ea6f5a
}
.sign .tooltip-error.right .tooltip-arrow-bg {
left: 2px;
border-right-color: #fff
}
.sign .slide-error {
position: relative;
padding: 10px 0;
border: 1px solid #c8c8c8;
border-radius: 4px
}
.sign .slide-error i {
position: static!important;
margin-right: 10px;
color: #ea6f5a!important;
vertical-align: middle
}
.sign .slide-error span {
font-size: 15px;
vertical-align: middle
}
.sign .slide-error div {
margin-top: 10px;
font-size: 13px
}
.sign .slide-error a {
color: #3194d0
}
.sign .js-sign-up-forbidden {
color: #999;
padding: 80px 0 100px
}
.sign .js-sign-up-container .slide-error {
border-bottom: none;
border-radius: 0
}
.sign .logo {
position: absolute;
top: 56px;
margin-left: 50px
}
.sign .logo img {
width: 100px
}
.sign .main {
width: 400px;
margin: 60px auto 0;
padding: 50px 50px 30px;
background-color: #fff;
border-radius: 4px;
box-shadow: 0 0 8px rgba(0,0,0,.1);
vertical-align: middle;
display: inline-block
}
.sign .reset-title,.sign .title {
margin: 0 auto 50px;
padding: 10px;
font-weight: 400;
color: #969696
}
.sign .reset-title a,.sign .title a {
padding: 10px;
color: #969696
}
.sign .reset-title a:hover,.sign .title a:hover {
border-bottom: 2px solid #ea6f5a
}
.sign .reset-title .active,.sign .title .active {
font-weight: 700;
color: #ea6f5a;
border-bottom: 2px solid #ea6f5a
}
.sign .reset-title b,.sign .title b {
padding: 10px
}
.sign .reset-title {
color: #333;
font-weight: 700
}
.sign form {
margin-bottom: 30px
}
.sign form .input-prepend {
position: relative;
width: 100%
}
.sign form .input-prepend input {
width: 100%;
height: 50px;
margin-bottom: 0;
padding: 4px 12px 4px 35px;
border: 1px solid #c8c8c8;
border-radius: 0 0 4px 4px;
background-color: hsla(0,0%,71%,.1);
vertical-align: middle
}
.sign form .input-prepend i {
position: absolute;
top: 14px;
left: 10px;
font-size: 18px;
color: #969696
}
.sign form .input-prepend span {
color: #333
}
.sign form .input-prepend .ic-show {
top: 18px;
left: auto;
right: 8px;
font-size: 12px
}
.sign form .geetest-placeholder {
height: 44px;
border-radius: 4px;
background-color: hsla(0,0%,71%,.1);
text-align: center;
line-height: 44px;
font-size: 14px;
color: #999
}
.sign form .restyle {
margin-bottom: 0
}
.sign form .restyle input {
border-bottom: none;
border-radius: 4px 4px 0 0
}
.sign form .no-radius input {
border-radius: 0
}
.sign form .slide-security-placeholder {
height: 32px;
background-color: hsla(0,0%,71%,.1);
border-radius: 4px
}
.sign form .slide-security-placeholder p {
padding-top: 7px;
color: #999;
margin-right: -7px
}
.sign .overseas-btn {
font-size: 14px;
color: #999
}
.sign .overseas-btn:hover {
color: #2f2f2f
}
.sign .remember-btn {
float: left;
margin: 15px 0
}
.sign .remember-btn span {
margin-left: 5px;
font-size: 15px;
color: #969696;
vertical-align: middle
}
.sign .forget-btn {
float: right;
position: relative;
margin: 15px 0;
font-size: 14px
}
.sign .forget-btn a {
color: #999
}
.sign .forget-btn a:hover {
color: #333
}
.sign .forget-btn .dropdown-menu {
top: 20px;
left: auto;
right: 0;
border-radius: 4px
}
.sign .forget-btn .dropdown-menu a {
padding: 10px 20px;
color: #333
}
.sign #sign-in-loading {
position: relative;
width: 20px;
height: 20px;
vertical-align: middle;
margin-top: -4px;
margin-right: 2px;
display: none
}
.sign #sign-in-loading:after {
content: "";
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: transparent
}
.sign #sign-in-loading:before {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 20px;
height: 20px;
margin: -10px 0 0 -10px;
border-radius: 10px;
border: 2px solid #fff;
border-bottom-color: transparent;
vertical-align: middle;
-webkit-animation: rolling .8s infinite linear;
animation: rolling .8s infinite linear;
z-index: 1
}
.sign .sign-in-button,.sign .sign-up-button {
margin-top: 20px;
width: 100%;
padding: 9px 18px;
font-size: 18px;
border: none;
border-radius: 25px;
color: #fff;
background: #42c02e;
cursor: pointer;
outline: none;
display: block;
clear: both
}
.sign .sign-in-button:hover,.sign .sign-up-button:hover {
background: #3db922
}
.sign .sign-in-button {
background: #3194d0
}
.sign .sign-in-button:hover {
background: #187cb7
}
.sign .btn-in-resend,.sign .btn-up-resend {
position: absolute;
top: 7px;
right: 7px;
width: 100px;
height: 36px;
font-size: 13px;
color: #fff;
background-color: #42c02e;
border-radius: 20px;
line-height: 36px
}
.sign .btn-in-resend {
background-color: #3194d0
}
.sign .sign-up-msg {
margin: 10px 0;
padding: 0;
text-align: center;
font-size: 12px;
line-height: 20px;
color: #969696
}
.sign .sign-up-msg a,.sign .sign-up-msg a:hover {
color: #3194d0
}
.sign .overseas input {
padding-left: 110px!important
}
.sign .overseas .overseas-number {
position: absolute;
top: 0;
left: 0;
width: 100px;
height: 50px;
font-size: 18px;
color: #969696;
border-right: 1px solid #c8c8c8
}
.sign .overseas .overseas-number span {
margin-top: 17px;
padding-left: 35px;
text-align: left;
font-size: 14px;
display: block
}
.sign .overseas .dropdown-menu {
width: 100%;
max-height: 285px;
font-size: 14px;
border-radius: 0 0 4px 4px;
overflow-y: auto
}
.sign .overseas .dropdown-menu li .nation-code {
width: 65px;
display: inline-block
}
.sign .overseas .dropdown-menu li a {
padding: 6px 20px;
font-size: 14px;
line-height: 20px
}
.sign .overseas .dropdown-menu li a::hover {
color: #fff;
background-color: #f5f5f5
}
.sign .more-sign {
margin-top: 50px
}
.sign .more-sign h6 {
position: relative;
margin: 0 0 10px;
font-size: 12px;
color: #b5b5b5
}
.sign .more-sign h6:before {
left: 30px
}
.sign .more-sign h6:after,.sign .more-sign h6:before {
content: "";
border-top: 1px solid #b5b5b5;
display: block;
position: absolute;
width: 60px;
top: 5px
}
.sign .more-sign h6:after {
right: 30px
}
.sign .more-sign ul {
margin-bottom: 10px;
list-style: none
}
.sign .more-sign ul li {
margin: 0 5px;
display: inline-block
}
.sign .more-sign ul a {
width: 50px;
height: 50px;
line-height: 50px;
display: block
}
.sign .more-sign ul i {
font-size: 28px
}
.sign .more-sign .ic-weibo {
color: #e05244
}
.sign .more-sign .ic-wechat {
color: #00bb29
}
.sign .more-sign .ic-qq_connect {
color: #498ad5
}
.sign .more-sign .ic-douban {
color: #00820f
}
.sign .more-sign .ic-more {
color: #999
}
.sign .more-sign .weibo-loading {
pointer-events: none;
cursor: pointer;
position: relative
}
.sign .more-sign .weibo-loading:after {
content: "";
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: #fff
}
body.reader-night-mode .sign .more-sign .weibo-loading:after {
background-color: #3f3f3f
}
.sign .more-sign .weibo-loading:before {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 20px;
height: 20px;
margin: -10px 0 0 -10px;
border-radius: 10px;
border: 2px solid #e05244;
border-bottom-color: transparent;
vertical-align: middle;
-webkit-animation: rolling .8s infinite linear;
animation: rolling .8s infinite linear;
z-index: 1
}
@keyframes rolling {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg)
}
to {
-webkit-transform: rotate(1turn);
transform: rotate(1turn)
}
}
@-webkit-keyframes rolling {
0% {
-webkit-transform: rotate(0deg)
}
to {
-webkit-transform: rotate(1turn)
}
}
.sign .reset-password-input {
border-radius: 4px!important
}
.sign .return {
margin-left: -8px;
color: #969696
}
.sign .return:hover {
color: #333
}
.sign .return i {
margin-right: 5px
}
.sign .icheckbox_square-green {
display: inline-block;
*display: inline;
vertical-align: middle;
margin: 0;
padding: 0;
width: 18px;
height: 18px;
background: url(/static/image/green.png) no-repeat;
border: none;
cursor: pointer;
background-position: 0 0
}
.sign .icheckbox_square-green.hover {
background-position: -20px 0
}
.sign .icheckbox_square-green.checked {
background-position: -40px 0
}
.sign .icheckbox_square-green.disabled {
background-position: -60px 0;
cursor: default
}
.sign .icheckbox_square-green.checked.disabled {
background-position: -80px 0
}
.geetest_panel_box>* {
box-sizing: content-box
}
@media (max-width:768px) {
body {
min-width: 0
}
.sign {
height: auto;
min-height: 0;
background-color: transparent
}
.sign .logo {
display: none
}
.sign .main {
position: absolute;
left: 50%;
margin: 0 0 0 -200px;
box-shadow: none
}
}
</style>
前端注册路由:
import Register from "../components/Register"
// 配置路由列表
export default new Router({
mode:"history",
routes:[
// 路由列表
...
{
name:"Register",
path: "/register",
component:Register,
}
]
})
修改首页头部 Header.vue
的连接:
<span class="header-register"><router-link to="/register">注册</router-link></span>
还有登录页 Login.vue
中的链接:
<router-link id="js-sign-up-btn" class="" to="/register">注册</router-link>
...
<p class="go_login" >没有账号 <router-link to="/register">立即注册</router-link></p>
手机号码的唯一校验
服务端api接口
users/utils.py
代码:
def get_user_by_data(**kwargs):
"""根据字段信息获取用户"""
User = get_user_model()
try:
return User.objects.get(**kwargs)
except:
return None
users/views.py
代码:
from .utils import get_user_by_data
from rest_framework import status
class CheckMobileAPIView(APIView):
def get(self,request,mobile):
user = get_user_by_data(mobile=mobile)
if user is None:
return Response({"err_msg":"ok", "err_status":1})
else:
return Response({"err_msg":"当前手机号已经被注册","err_status": 0}, status=status.HTTP_400_BAD_REQUEST)
users/urls.py
代码:
from django.urls import path,re_path
from rest_framework_jwt.views import obtain_jwt_token,refresh_jwt_token
from . import views
urlpatterns = [
path("login/", obtain_jwt_token ),
path("refresh/", refresh_jwt_token ),
path("captcha/", views.CaptchaAPIView.as_view() ),
re_path("^mobile/(?P<mobile>1[3-9]\d{9})/$", views.CheckMobileAPIView.as_view()),
]
客户端在用户输入手机号,输入框失去焦点时,使用ajax发送验证请求,代码:
<template>
<div class="sign">
<div class="logo"><router-link to="/"><img src="/static/image/nav-logo.png" alt="Logo"></router-link></div>
<div class="main">
<h4 class="title">
<div class="normal-title">
<router-link to="/user/login">登录</router-link>
<b>·</b>
<router-link id="js-sign-up-btn" class="active" to="/user/register">注册</router-link>
</div>
</h4>
<div class="js-sign-up-container">
<form class="new_user" id="new_user" action="" accept-charset="UTF-8" method="post">
<div class="input-prepend restyle">
<input placeholder="你的昵称" type="text" value="" v-model="nickname" id="user_nickname">
<i class="iconfont ic-user"></i>
</div>
<div class="input-prepend restyle no-radius js-normal">
<input placeholder="手机号" type="tel" @blur="check_mobile" v-model="mobile" id="user_mobile_number">
<i class="iconfont ic-phonenumber"></i>
</div>
<div class="input-prepend restyle no-radius security-up-code js-security-number" v-if="is_show_sms_code">
<input type="text" v-model="sms_code" id="sms_code" placeholder="手机验证码">
<i class="iconfont ic-verify"></i>
<a tabindex="-1" class="btn-up-resend js-send-code-button" :class="{disable:send_able}" href="javascript:void(0);" id="send_code">{{sms_code_text}}</a>
</div>
<input type="hidden" name="security_number" id="security_number">
<div class="input-prepend">
<input placeholder="设置密码" type="password" v-model="password" id="user_password">
<i class="iconfont ic-password"></i>
</div>
<input type="submit" name="commit" value="注册" class="sign-up-button" id="sign_up_btn" data-disable-with="注册">
<p class="sign-up-msg">点击 “注册” 即表示您同意并愿意遵守荏苒<br> <a target="_blank" href="">用户协议</a> 和 <a target="_blank" href="">隐私政策</a> 。</p>
</form>
<!-- 更多注册方式 -->
<div class="more-sign">
<h6>社交帐号直接注册</h6>
<ul>
<li><a id="weixin" class="weixin" target="_blank" href=""><i class="iconfont ic-wechat"></i></a></li>
<li><a id="qq" class="qq" target="_blank" href=""><i class="iconfont ic-qq_connect"></i></a></li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Register",
data(){
return {
nickname:"",
mobile:"",
sms_code:"",
password:"",
sms_code_text:"发送验证码",
is_show_sms_code:false,
send_able:false,
}
},
watch:{
mobile(){
// js里面的正则,类似于python re.match
if(/^1[3-9]\d{9}$/.test(this.mobile)){
this.is_show_sms_code = true;
this.send_able = true;
}else{
this.is_show_sms_code = false;
}
}
},
methods:{
check_mobile(){
// 验证手机号是否唯一
if(this.is_show_sms_code){
// 发送ajax到服务端验证手机号是否可用
this.$axios.get(`${this.$settings.Host}/users/mobile/${this.mobile}/`).then(response=>{
}).catch(error=>{
this.$message.error(error.response.data.err_msg);
this.send_able = false;
});
}
}
}
}
</script>
注册功能的实现
实现基本的账号信息注册
视图代码:
from rest_framework.generics import CreateAPIView
from .serializers import UserCreateModelSerializer
from .models import User
class UserAPIView(CreateAPIView):
"""添加用户"""
queryset = User.objects.all()
serializer_class = UserCreateModelSerializer
序列化器中,进行验证和保存数据,并返回 jwt 登录认证给客户端:
from rest_framework import serializers
from .models import User
import re
from .utils import get_user_by_data
class UserCreateModelSerializer(serializers.ModelSerializer):
# 接收字段/ 返回客户端的字段
sms_code = serializers.CharField(write_only=True, min_length=4, max_length=4, label="短信验证码")
token = serializers.CharField(read_only=True,label="jwt_token")
class Meta:
model = User
fields = ["nickname","mobile","sms_code","password","username","id","avatar","token"]
read_only_fields = ["id","username","avatar"]
extra_kwargs = {
"mobile":{"write_only":True, },
"password":{"write_only":True, "min_length": 8,"max_length": 16, },
}
def validate(self, attrs):
# 1. 验证手机格式是否正确
mobile = attrs.get("mobile")
if not re.match("^1[3-9]\d{9}$",mobile):
raise serializers.ValidationError("手机号码格式有误!","mobile")
# 2. 验证手机是否注册了
user = get_user_by_data(mobile=mobile)
if user:
raise serializers.ValidationError("手机号码已经被注册!", "mobile")
# todo 3. 短信是否正确
return attrs
def create(self, validated_data):
"""保存用户信息"""
try:
user = User.objects.create_user(
username=validated_data.get("mobile"),
password=validated_data.get("password"),
nickname=validated_data.get("nickname"),
mobile=validated_data.get("mobile"),
)
except:
raise serializers.ValidationError("用户注册失败!")
from rest_framework_jwt.settings import api_settings
# 首次注册,免登录,手动生成jwt
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
payload = jwt_payload_handler(user)
user.token = jwt_encode_handler(payload)
return user
路由,代码:
from django.urls import path,re_path
from rest_framework_jwt.views import obtain_jwt_token,refresh_jwt_token
from . import views
urlpatterns = [
path("login/", obtain_jwt_token ),
path("refresh/", refresh_jwt_token ),
path("captcha/", views.CaptchaAPIView.as_view() ),
re_path("^mobile/(?P<mobile>1[3-9]\d{9})/$", views.CheckMobileAPIView.as_view()),
path("",views.UserAPIView.as_view()),
]
客户端提交注册信息
register.vue,代码:
<template>
<div class="sign">
<div class="logo"><router-link to="/"><img src="/static/image/nav-logo.png" alt="Logo"></router-link></div>
<div class="main">
<h4 class="title">
<div class="normal-title">
<router-link to="/user/login">登录</router-link>
<b>·</b>
<router-link id="js-sign-up-btn" class="active" to="/user/register">注册</router-link>
</div>
</h4>
<div class="js-sign-up-container">
<form class="new_user" id="new_user" action="" accept-charset="UTF-8" method="post">
<div class="input-prepend restyle">
<input placeholder="你的昵称" type="text" value="" v-model="nickname" id="user_nickname">
<i class="iconfont ic-user"></i>
</div>
<div class="input-prepend restyle no-radius js-normal">
<input placeholder="手机号" type="tel" @blur="check_mobile" v-model="mobile" id="user_mobile_number">
<i class="iconfont ic-phonenumber"></i>
</div>
<div class="input-prepend restyle no-radius security-up-code js-security-number" v-if="is_show_sms_code">
<input type="text" v-model="sms_code" id="sms_code" placeholder="手机验证码">
<i class="iconfont ic-verify"></i>
<a tabindex="-1" class="btn-up-resend js-send-code-button" :class="{disable: disable_sms_code}" href="javascript:void(0);" id="send_code">{{sms_code_text}}</a>
</div>
<input type="hidden" name="security_number" id="security_number">
<div class="input-prepend">
<input placeholder="设置密码" type="password" v-model="password" id="user_password">
<i class="iconfont ic-password"></i>
</div>
<input type="submit" name="commit" @click.prevent="showCaptcha" value="注册" class="sign-up-button" id="sign_up_btn" data-disable-with="注册">
<p class="sign-up-msg">点击 “注册” 即表示您同意并愿意遵守荏苒<br> <a target="_blank" href="">用户协议</a> 和 <a target="_blank" href="">隐私政策</a> 。</p>
</form>
<!-- 更多注册方式 -->
<div class="more-sign">
<h6>社交帐号直接注册</h6>
<ul>
<li><a id="weixin" class="weixin" target="_blank" href=""><i class="iconfont ic-wechat"></i></a></li>
<li><a id="qq" class="qq" target="_blank" href=""><i class="iconfont ic-qq_connect"></i></a></li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Register",
data(){
return {
nickname: "",
mobile: "",
sms_code: "",
password: "",
sms_code_text: "发送验证码",
is_show_sms_code: false,
disable_sms_code: false,
}
},
watch:{
mobile(){
this.is_show_sms_code = /^1[3-9]\d{9}$/.test(this.mobile);
if (this.is_show_sms_code) {this.check_mobile()}
},
},
methods: {
check_mobile () {
if (this.is_show_sms_code) {
// 发送ajax到服务端验证手机号是否唯一可用
this.$axios.get(`${this.$settings.Host}/users/mobile/${this.mobile}/`).then(response=>{
this.disable_sms_code = false
}).catch(errors=>{
if (errors.response.status === 400) {
this.$message.error(errors.response.data.msg);
this.disable_sms_code = true
}
})
}
},
showCaptcha () {
if (!this.is_show_sms_code || this.sms_code.length !== 4){
this.$message.error('请确保手机号和验证码填写正确');
return false;
} else if (this.password.length < 8 || this.password.length > 16) {
this.$message.error('密码长度应该在 8 到 16 位~');
return false;
}
// 初步数据校验完成,调用腾讯防水墙验证码
var captcha1 = new TencentCaptcha(this.$settings.TC_captcha.app_id, res => {
if (res.ret === 0) { // 初步通过腾讯防水墙验证
// 将验证码数据和用户注册信息提交到后台进行处理
this.$axios.post(`${this.$settings.Host}/users/`, {
ret: res.ret,
ticket: res.ticket,
randstr: res.randstr,
nickname: this.nickname,
mobile: this.mobile,
sms_code: this.sms_code,
password: this.password,
}).then(response=>{
// 注册成功,则保存用户登录状态,以临时存储方式保存
sessionStorage.user_token = response.data.token;
sessionStorage.user_name = response.data.username;
sessionStorage.user_id = response.data.id;
sessionStorage.user_nickname = response.data.nickname;
sessionStorage.user_avatar = response.data.avatar;
localStorage.removeItem("user_token");
localStorage.removeItem("user_name");
localStorage.removeItem("user_id");
localStorage.removeItem("user_nickname");
localStorage.removeItem("user_avatar");
this.$confirm(`欢迎${response.data.nickname}加入荏苒~`, '注册成功!', {
confirmButtonText: '去个人中心',
cancelButtonText: this.go_back_msg,
type: 'warning'
}).then(() => {
// 跳转到个人中心
this.$router.push('/user');
}).catch(() => {
// 跳转到上一页
this.$router.back();
});
}).catch(errors=>{
// 登录失败
if (error.response.status === 400) {
this.$message.error('会员注册失败,请检查您输入的内容是否正确!')
} else {
console.log(error)
}
})
} else {
this.$meesage.error("验证码验证失败!请重新操作验证码");
}
});
captcha1.show();
},
}
}
</script>
在注册功能中集成短信验证码功能
接下来,我们把注册过程中一些注册信息(例如:短信验证码)和 session 缓存到 redis 数据库中。
如果之前没有安装 django-redis 的话,可以使用命令安装:
pip install django-redis
在 settings/dev.py
配置中添加一下代码,最好配置在数据库 DATABASES 配置下面:
# 设置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"
关于 django-redis 的使用,说明文档可参见:http://django-redis-chs.readthedocs.io/zh_CN/latest/
django-redis 提供了 get_redis_connection 的方法,通过调用 get_redis_connection 方法传递 redis 的配置名称可获取到 redis 的连接对象,通过 redis 连接对象可以执行 redis 命令
https://redis-py.readthedocs.io/en/latest/
使用范例:
from django_redis import get_redis_connection
// 链接redis数据库
redis_conn = get_redis_connection("default")
redis_conn.set("name","xiaoming")
使用云通讯发送短信
官网:https://www.yuntongxun.com/
在登录后的平台上面获取一下信息:
ACCOUNT SID:8a216da863f8e6c20164139687e80c1b
AUTH TOKEN : 6dd01b2b60104b3dbc88b2b74158bac6
AppID(默认):8a216da863f8e6c20164139688400c21
Rest URL(生产): app.cloopen.com:8883 [项目上线时使用真实短信发送服务器]
Rest URL(开发): sandboxapp.cloopen.com:8883 [项目开发时使用沙箱短信发送服务器]
找到sdkdemo进行下载

在开发过程中,为了节约发送短信的成本,可以把自己的或者同事的手机加入到测试号码中.

后端生成短信验证码
把素材里面的 yuntongxun 目录放到项目的 utils 目录下,然后在 settings/dev.py
下添加短信接口的配置信息,代码:
# 云通讯的短信配置
SMS = {
"_accountSid": "8a216da863f8e6c20164139687e80c1b",
"_accountToken": "6dd01b2b60104b3dbc88b2b74158bac6",
"_appId": "8a216da863f8e6c20164139688400c21",
"_serverIP": "sandboxapp.cloopen.com",
"_serverPort": "8883",
}
users/views.py
,代码:
import random
from renranapi.utils.yuntongxun.sms import CCP
from django_redis import get_redis_connection
from renranapi.settings import constants
import logging
loger = logging.getLogger("django")
class SMSCodeAPIView(APIView):
"""
短信验证码
"""
def get(self,request,mobile):
# 1. 验证数据[短信发送间隔]
redis = get_redis_connection("sms_code")
result = redis.get("interval_%s" % mobile)
if result:
return Response({"message": "短信已经发送中,请留意您的手机,不要频繁点击!"}, status=status.HTTP_400_BAD_REQUEST)
# 2. 生成随机短信验证码
sms_code = "%04d" % random.randint(0,9999)
# 3. 发送短信验证码
try:
ccp = CCP()
ret = ccp.send_template_sms(mobile, [sms_code, constants.SMS_EXPIRE_TIME // 60], constants.SMS_TEMPLATE_ID)
except:
ret = False
if not ret:
loger.error("发送短信失败!")
return Response("短信发送失败!")
# 4. 保存短信验证码到redis中
redis.setex("sms_%s" % mobile, constants.SMS_EXPIRE_TIME, sms_code)
redis.setex("interval_%s" % mobile, constants.SMS_INTERVAL_TIME, "_")
# 5. 返回操作结果
return Response({"message":"短信已经发送,请留意您的手机"})
settings/constants.py
代码:
# 短信有效期时间,单位:秒
SMS_EXPIRE_TIME = 300
# 短信模板ID,测试阶段只能是1
SMS_TEMPLATE_ID = 1
# 短信发送冷却时间,单位: 秒
SMS_INTERVAL_TIME = 60
路由代码,users/urls.py
:
re_path("^sms/(?P<mobile>1[3-9]\d{9})/$",views.SMSCodeAPIView.as_view()),
客户端发送注册信息和发送短信
<template>
<div class="sign">
<div class="logo"><router-link to="/"><img src="/static/image/nav-logo.png" alt="Logo"></router-link></div>
<div class="main">
<h4 class="title">
<div class="normal-title">
<router-link to="/user/login">登录</router-link>
<b>·</b>
<router-link id="js-sign-up-btn" class="active" to="/user/register">注册</router-link>
</div>
</h4>
<div class="js-sign-up-container">
<form class="new_user" id="new_user" action="" accept-charset="UTF-8" method="post">
<div class="input-prepend restyle">
<input placeholder="你的昵称" type="text" value="" v-model="nickname" id="user_nickname">
<i class="iconfont ic-user"></i>
</div>
<div class="input-prepend restyle no-radius js-normal">
<input placeholder="手机号" type="tel" @blur="check_mobile" v-model="mobile" id="user_mobile_number">
<i class="iconfont ic-phonenumber"></i>
</div>
<div class="input-prepend restyle no-radius security-up-code js-security-number" v-if="is_show_sms_code">
<input type="text" v-model="sms_code" id="sms_code" placeholder="手机验证码">
<i class="iconfont ic-verify"></i>
<a tabindex="-1" @click="get_sms_code" class="btn-up-resend js-send-code-button" :class="{disable: disable_sms_code}" href="javascript:void(0);" id="send_code">{{sms_code_text}}</a>
</div>
<input type="hidden" name="security_number" id="security_number">
<div class="input-prepend">
<input placeholder="设置密码" type="password" v-model="password" id="user_password">
<i class="iconfont ic-password"></i>
</div>
<input type="submit" name="commit" @click.prevent="showCaptcha" value="注册" class="sign-up-button" id="sign_up_btn" data-disable-with="注册">
<p class="sign-up-msg">点击 “注册” 即表示您同意并愿意遵守荏苒<br> <a target="_blank" href="">用户协议</a> 和 <a target="_blank" href="">隐私政策</a> 。</p>
</form>
<!-- 更多注册方式 -->
<div class="more-sign">
<h6>社交帐号直接注册</h6>
<ul>
<li><a id="weixin" class="weixin" target="_blank" href=""><i class="iconfont ic-wechat"></i></a></li>
<li><a id="qq" class="qq" target="_blank" href=""><i class="iconfont ic-qq_connect"></i></a></li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Register",
data(){
return {
nickname: "",
mobile: "",
sms_code: "",
password: "",
sms_code_text: "发送验证码",
is_show_sms_code: false,
disable_sms_code: false,
}
},
watch:{
mobile(){
this.is_show_sms_code = /^1[3-9]\d{9}$/.test(this.mobile);
if (this.is_show_sms_code) {this.check_mobile()}
},
},
methods: {
check_mobile () {
if (this.is_show_sms_code) {
// 发送ajax到服务端验证手机号是否唯一可用
this.$axios.get(`${this.$settings.Host}/users/mobile/${this.mobile}/`).then(response=>{
this.disable_sms_code = false
}).catch(errors=>{
if (errors.response.status === 400) {
this.$message.error(errors.response.data.error_msg);
this.disable_sms_code = true
}
})
}
},
showCaptcha () {
if (!this.is_show_sms_code || this.sms_code.length !== 4){
this.$message.error('请确保手机号和验证码填写正确');
return false;
} else if (this.password.length < 8 || this.password.length > 16) {
this.$message.error('密码长度应该在 8 到 16 位~');
return false;
}
// 初步数据校验完成,调用腾讯防水墙验证码
var captcha1 = new TencentCaptcha(this.$settings.TC_captcha.app_id, res => {
if (res.ret === 0) { // 初步通过腾讯防水墙验证
// 将验证码数据和用户注册信息提交到后台进行处理
this.$axios.post(`${this.$settings.Host}/users/`, {
ret: res.ret,
ticket: res.ticket,
randstr: res.randstr,
nickname: this.nickname,
mobile: this.mobile,
sms_code: this.sms_code,
password: this.password,
}).then(response=>{
// 注册成功,则保存用户登录状态,以临时存储方式保存
sessionStorage.user_token = response.data.token;
sessionStorage.user_name = response.data.username;
sessionStorage.user_id = response.data.id;
sessionStorage.user_nickname = response.data.nickname;
sessionStorage.user_avatar = response.data.avatar;
localStorage.removeItem("user_token");
localStorage.removeItem("user_name");
localStorage.removeItem("user_id");
localStorage.removeItem("user_nickname");
localStorage.removeItem("user_avatar");
this.$confirm(`欢迎${response.data.nickname}加入荏苒~`, '注册成功!', {
confirmButtonText: '去个人中心',
cancelButtonText: "返回上一页",
type: 'warning'
}).then(() => {
// 跳转到个人中心
this.$router.push('/user');
}).catch(() => {
// 跳转到上一页
this.$router.back();
});
}).catch(errors=>{
// 注册失败
if (errors.response.status === 400) {
this.$message.error(errors.response.data.error_msg);
} else {
console.log(errors)
}
})
} else {
this.$meesage.error("验证码验证失败!请重新操作验证码");
}
});
captcha1.show();
},
get_sms_code () {
if (!this.is_show_sms_code || this.disable_sms_code) {
this.$message.error('请正确填写手机号!');
return false;
}
this.$axios.get(`${this.$settings.Host}/users/sms/${this.mobile}`).then(response=>{
this.$message.success("验证码发送成功,请注意查收~")
}).catch(errors=>{
if (errors.response.status === 400) {
this.$message.error(errors.response.data.error_msg);
} else {
this.$message.error("验证码发送失败,请稍后重试!");
console.log(errors.response);
}
})
},
}
}
</script>
服务端注册功能中完成手机短信的校验
在序列化器中增加验证码的判断逻辑,代码:
from rest_framework import serializers
from .models import User
import re
from .utils import get_user_by_data
from django_redis import get_redis_connection
class UserCreateModelSerializer(serializers.ModelSerializer):
# 接收字段/ 返回客户端的字段
sms_code = serializers.CharField(write_only=True, min_length=4, max_length=4, label="短信验证码")
token = serializers.CharField(read_only=True,label="jwt_token")
class Meta:
model = User
fields = ["nickname","mobile","sms_code","password","username","id","avatar","token"]
read_only_fields = ["id","username","avatar"]
extra_kwargs = {
"mobile":{"write_only":True, },
"password":{"write_only":True, "min_length": 8,"max_length": 16, },
}
def validate(self, attrs):
# 1. 验证手机格式是否正确
mobile = attrs.get("mobile")
if not re.match("^1[3-9]\d{9}$",mobile):
raise serializers.ValidationError("手机号码格式有误!","mobile")
# 2. 验证手机是否注册了
user = get_user_by_data(mobile=mobile)
if user:
raise serializers.ValidationError("手机号码已经被注册!", "mobile")
# 3. 短信是否正确
redis_conn = get_redis_connection("sms_code")
redis_sms_code = redis_conn.get("sms_%s" % mobile)
if redis_sms_code:
sms_code = attrs.get("sms_code")
if redis_sms_code.decode() != sms_code:
raise serializers.ValidationError("验证码错误!")
else:
raise serializers.ValidationError("验证码已过期!")
return attrs
def create(self, validated_data):
"""保存用户信息"""
try:
user = User.objects.create_user(
username=validated_data.get("mobile"),
password=validated_data.get("password"),
nickname=validated_data.get("nickname"),
mobile=validated_data.get("mobile"),
)
except:
raise serializers.ValidationError("用户注册失败!")
from rest_framework_jwt.settings import api_settings
# 首次注册,免登录,手动生成jwt
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
payload = jwt_payload_handler(user)
user.token = jwt_encode_handler(payload)
return user
当然,也可以把校验短信验证码的操作整合在视图中,这时,views.py
可以写成这样:
class UserAPIView(CaptchaAPIView, CreateAPIView):
queryset = User.objects.all()
serializer_class = UserCreateModelSerializer
def post(self, request, *args, **kwargs):
# 校验腾讯防水墙验证码,若验证码不正确,则报错,不去注册
if not super().check_captcha():
return Response({'error_msg': '验证码有误!'}, status=status.HTTP_400_BAD_REQUEST)
# 校验短信验证码
redis = get_redis_connection('sms_code')
sms_code = redis.get('sms_%s' % request.data.get('mobile'))
if sms_code and (sms_code != request.data.get('sms_code').encode()):
return Response({'error_msg': '短信验证码有误!'}, status=status.HTTP_400_BAD_REQUEST)
# 若腾讯防水墙验证码和短信验证码都通过验证,则继续执行创建用户的操作
return super().post(request, *args, **kwargs)
给 redis 操作设置事务
在 redis 中,如果出现连续操作多条命令, 那么这些命令,我们可以把他们设置成一个整体,采用redis提供的事务来让它们一起执行。执行过程中保证数据的原子性,要么一起成功,要么一起失败。
所以在注册的验证码功能中,我们就可以使用事务来完成验证码的记录工作。
redis_conn = get_redis_connection("sms_code")
# redis的事务是通过管道对象来设置命令
pipe = redis_conn.pipeline()
pipe.multi() # 开启事务
pipe.命令()
pipe.命令()
pipe.execute() # 提交事务
视图 users/views.py
,代码:
class SMSCodeAPIView(APIView):
"""
短信验证码
"""
def get(self,request,mobile):
# 1. 验证数据[短信发送间隔]
redis = get_redis_connection("sms_code")
result = redis.get("interval_%s" % mobile)
if result:
return Response({"error_msg": "短信已经发送中,请留意您的手机,不要频繁点击!"}, status=status.HTTP_400_BAD_REQUEST)
# 2. 生成随机短信验证码
sms_code = "%04d" % random.randint(0,9999)
# 3. 发送短信验证码
try:
ccp = CCP()
ret = ccp.send_template_sms(mobile, [sms_code, constants.SMS_EXPIRE_TIME // 60], constants.SMS_TEMPLATE_ID)
except:
ret = False
if not ret:
loger.error("发送短信失败!")
return Response("短信发送失败!")
# 4. 保存短信验证码到redis中,因为涉及两条数据库操作,最好还是加上事务
pipe = redis.pipeline() # 创建管道对象
pipe.multi() # 开启事务
# 设置事务的相关操作命令
pipe.setex("sms_%s" % mobile, constants.SMS_EXPIRE_TIME, sms_code)
pipe.setex("interval_%s" % mobile, constants.SMS_INTERVAL_TIME, "_")
# 提交事务
pipe.execute()
# 5. 返回操作结果
return Response({"message":"短信已经发送,请留意您的手机"})
验证码冷却倒计时
用户点击发送验证码后,按钮暂时变为不可用状态,出现倒计时,告诉用户按钮冷却剩余时间,避免用户频繁点击,给后端造成不必要的压力。
Login.vue
的 js 代码:
<script>
export default {
name: "Register",
data(){
return {
nickname: "",
mobile: "",
sms_code: "",
password: "",
sms_code_text: "发送验证码",
is_show_sms_code: false,
disable_sms_code: false,
interval_id: 0,
interval_time: 0,
}
},
watch:{
mobile(){
this.is_show_sms_code = /^1[3-9]\d{9}$/.test(this.mobile);
if (this.is_show_sms_code) {this.check_mobile()}
},
},
methods: {
check_mobile () {
if (this.is_show_sms_code) {
// 发送ajax到服务端验证手机号是否唯一可用
this.$axios.get(`${this.$settings.Host}/users/mobile/${this.mobile}/`).then(response=>{
this.disable_sms_code = false
}).catch(errors=>{
if (errors.response.status === 400) {
this.$message.error(errors.response.data.error_msg);
this.disable_sms_code = true
}
})
}
},
showCaptcha () {
if (!this.is_show_sms_code || this.sms_code.length !== 4){
this.$message.error('请确保手机号和验证码填写正确');
return false;
} else if (this.password.length < 8 || this.password.length > 16) {
this.$message.error('密码长度应该在 8 到 16 位~');
return false;
}
// 初步数据校验完成,调用腾讯防水墙验证码
var captcha1 = new TencentCaptcha(this.$settings.TC_captcha.app_id, res => {
if (res.ret === 0) { // 初步通过腾讯防水墙验证
// 将验证码数据和用户注册信息提交到后台进行处理
this.$axios.post(`${this.$settings.Host}/users/`, {
ret: res.ret,
ticket: res.ticket,
randstr: res.randstr,
nickname: this.nickname,
mobile: this.mobile,
sms_code: this.sms_code,
password: this.password,
}).then(response=>{
// 注册成功,则保存用户登录状态,以临时存储方式保存
sessionStorage.user_token = response.data.token;
sessionStorage.user_name = response.data.username;
sessionStorage.user_id = response.data.id;
sessionStorage.user_nickname = response.data.nickname;
sessionStorage.user_avatar = response.data.avatar;
localStorage.removeItem("user_token");
localStorage.removeItem("user_name");
localStorage.removeItem("user_id");
localStorage.removeItem("user_nickname");
localStorage.removeItem("user_avatar");
this.$confirm(`欢迎${response.data.nickname}加入荏苒~`, '注册成功!', {
confirmButtonText: '去个人中心',
cancelButtonText: "返回上一页",
type: 'warning'
}).then(() => {
// 跳转到个人中心
this.$router.push('/user');
}).catch(() => {
// 跳转到上一页
this.$router.back();
});
}).catch(errors=>{
// 注册失败
if (errors.response.status === 400) {
this.$message.error(errors.response.data.error_msg);
} else {
console.log(errors)
}
})
} else {
this.$meesage.error("验证码验证失败!请重新操作验证码");
}
});
captcha1.show();
},
get_sms_code () {
if (!this.is_show_sms_code || this.disable_sms_code) {
this.$message.error('请正确填写手机号!');
return false;
}
this.$axios.get(`${this.$settings.Host}/users/sms/${this.mobile}`).then(response=>{
this.$message.success("验证码发送成功,请注意查收~");
this.interval_time = 60;
this.start_interval();
}).catch(errors=>{
if (errors.response.status === 400) {
this.$message.error(errors.response.data.error_msg);
} else {
this.$message.error("验证码发送失败,请稍后重试!");
console.log(errors.response);
}
})
},
start_interval () {
this.interval_id = setInterval(()=>{
if (this.interval_time){
this.disable_sms_code = true;
this.sms_code_text = `${this.interval_time} 秒后可重发`;
this.interval_time--;
}else{
this.disable_sms_code = false;
this.sms_code_text = '重新发送';
clearInterval(this.interval_id);
}
}, 1000);
}
}
}
</script>