欢迎来到十九分快乐的博客

生死看淡,不服就干。

3.用户登陆注册

1.用户的登陆认证

1.1 前端显示登陆页面

登录页组件

Login.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">
    <a class="active" href="/login">登录</a>
    <b>·</b>
    <a id="js-sign-up-btn" class="" href="/register">注册</a>
  </div>
</h4>
<div class="js-sign-in-container">
  <form id="new_session" action="" method="post">
      <div class="input-prepend restyle js-normal">
        <input placeholder="手机号或邮箱" type="text" name="session[email_or_mobile_number]" id="session_email_or_mobile_number">
        <i class="iconfont ic-user"></i>
      </div>
    <!-- 海外登录登录名输入框 -->

    <div class="input-prepend">
      <input placeholder="密码" type="password" name="password" id="session_password">
      <i class="iconfont ic-password"></i>
    </div>
    <div class="remember-btn">
      <input type="checkbox" value="true" checked="checked" name="remember_me" id="session_remember_me"><span>记住我</span>
    </div>
    <div class="forget-btn">
      <a class="" data-toggle="dropdown" href="">登录遇到问题?</a>
    </div>
    <button class="sign-in-button" id="sign-in-form-submit-btn" type="button">
      <span id="sign-in-loading"></span>
      登录
    </button>
</form>
  <!-- 更多登录方式 -->
  <div class="more-sign">
    <h6>社交帐号登录</h6>
    <ul>
  <li id="weibo-link-wrap" class="">
    <a class="weibo" id="weibo-link">
      <i class="iconfont ic-weibo"></i>
    </a>
  </li>
  <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: "Login"
    }
</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>

把素材中的iconfont.css和iconfont.eof字体文件,放到项目中引入进来,因为在别的页面也会使用到图标,所以我们直接的入口文件index.html或者main.js中全局引入.

main.js,代码:

// 全局导入字体图标
import "../static/css/iconfont.css";
import "../static/css/iconfont.eot";

绑定登陆页面路由地址

router/index.js

import Vue from "vue"
import Router from "vue-router"

// 导入需要注册路由的组件
import Home from '@/components/Home'
import Login from '@/components/Login'
Vue.use(Router);

// 配置路由列表
export default new Router({
  mode:"history",
  routes:[
    // 路由列表
		...
    {
      name:"Login",
      path: "/login",
      component:Login,
    }
  ]
})

调整首页头部子组件中登陆按钮的链接信息

Header.vue

<router-link class="btn log-in" id="sign_in" to="/login">登录</router-link> <!--站内跳转用router-link(生成的也是a标签),站外跳转用a标签-->

Login.vue

<div class="logo"><router-link to="/"><img src="/static/image/nav-logo.png" alt="Logo"></router-link></div>


<router-link class="active" to="/user/login">登录</router-link>

1.2 后端实现登陆认证

Django默认已经提供了认证系统Auth模块。认证系统包含:

  • 用户管理
  • 权限【RBAC权限机制 Role-Base Access Control基于用户角色的访问控制机制】
  • 用户组
  • 密码哈希系统
  • 用户登录或内容显示的表单和视图
  • 一个可插拔的后台系统【admin后台运营站点,实际开发中大部分人使用的是xadmin后台运营站点】

Django默认用户的认证机制依赖Session机制,我们在项目中将引入JWT认证机制,将用户的身份凭据存放在Token中,然后对接Django的认证系统,帮助我们来实现:

  • 用户的数据模型
  • 用户密码的加密与验证
  • 用户的权限系统

Django用户模型类

django内置的用户模型文件: django/contrib/auth/model.py

Django认证系统中提供了用户模型类User保存用户的数据,默认的User包含以下常见的基本字段:

字段名 字段描述
username 必选。150个字符以内。 用户名可能包含字母数字,_@+ .-个字符。
first_name 可选(blank=True)。 少于等于30个字符。
last_name 可选(blank=True)。 少于等于30个字符。
email 可选(blank=True)。 邮箱地址。
password 必选。 密码的哈希加密串。 (Django 不保存原始密码)。 原始密码可以无限长而且可以包含任意字符。
groups Group 之间的多对多关系。
user_permissions Permission 之间的多对多关系。
is_staff 布尔值。 设置用户是否可以访问Admin 站点。
is_active 布尔值。 指示用户的账号是否激活。 它不是用来控制用户是否能够登录,而是描述一种帐号的使用状态。
is_superuser 是否是超级用户。超级用户具有所有权限。
last_login 用户最后一次登录的时间。
date_joined 账户创建的时间。 当账号创建时,默认设置为当前的date/time。
常用方法:
  • set_password(raw_password)

    设置用户的密码为给定的原始字符串,并负责密码的。 不会保存User 对象。当Noneraw_password 时,密码将设置为一个不可用的密码。

  • check_password(raw_password)

    如果给定的raw_password是用户的真实密码,则返回True,可以在校验用户密码时使用。

管理器方法:

管理器方法即可以通过User.objects. 进行调用的方法。

  • create_user(username, email=None, password=None, ***extra_fields*)

    创建普通用户、保存并返回一个User对象。

  • create_superuser(username, email, password, ***extra_fields*)

    create_user() 相同,创建超级用户,但是设置is_staffis_superuserTrue

创建用户模块的子应用

cd renranapi/apps
python ../../manage.py startapp users

在settings/dev.py文件中注册子应用。

INSTALLED_APPS = [
		...
  	'users',
]

解决因为我们调整子应用保存目录以后导致django无法识别子应用的BUG

# 运行错误: LookupError: No installed app with label 'admin'.
# 解决方案:通过把apps设置为导包路径即可解决。

import sys
sys.path.insert(0,os.path.join(BASE_DIR, "apps"))

INSTALLED_APPS = [
    ...省略,
    'users',
]
# ...后面省略

创建自定义的用户模型类

Django认证系统中提供的用户模型类及方法很方便,我们可以使用这个模型类,但是字段有些无法满足项目需求,如本项目中需要保存用户的手机号,需要给模型类添加额外的字段。

Django提供了django.contrib.auth.models.AbstractUser用户抽象模型类允许我们继承,扩展字段来使用Django认证系统的用户模型类。

在创建好的应用apps/users/models.py中定义用户的用户模型类。

from django.db import models
from django.contrib.auth.models import AbstractUser

# Create your models here.
class User(AbstractUser):
    """用户模型类"""
    mobile = models.CharField(max_length=15, null=True,blank=True, unique=True, help_text="手机号码", verbose_name="手机号码")
    wxchat = models.CharField(max_length=100, null=True,blank=True, unique=True, help_text="微信账号", verbose_name="微信账号")
    alipay = models.CharField(max_length=100, null=True,blank=True, unique=True, help_text="支付宝账号", verbose_name="支付宝账号")
    qq_number = models.CharField(max_length=11, null=True, blank=True,unique=True, help_text="QQ号", verbose_name="QQ号")
    # 保存文件的子目录
    avatar = models.ImageField(upload_to="avatar", null=True, blank=True,default=None, verbose_name="头像")

    class Meta:
        db_table = "rr_users"  #表名
        verbose_name = "用户信息"
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.username

我们自定义的用户模型类还不能直接被Django的认证系统所识别,需要在配置文件中告知Django认证系统使用我们自定义的模型类。

在配置文件settings/dev.py,中进行设置

AUTH_USER_MODEL = 'users.User' #认证用户模型类
# 用户表继承AbstractUser加上此配置,Django后台与前端客户认证就用一张表了
# 参数:应用名.模型类名,中间不用写models,系统自动去找models下的User模型类

注意:Django强烈建议我们对于AUTH_USER_MODEL参数的设置一定要在第一次数据库迁移之前就设置好,否则后续使用可能出现未知错误。

执行数据库迁移

python manage.py makemigrations
python manage.py migrate

创建超级管理用户:

python manage.py createsuperuser #设置用户名和密码
运行项目,登陆后台http://api.renran.com:8000/admin验证,后台认证也用这个表数据

Xadmin

xadmin是Django的第三方扩展,可是使Django的admin站点使用更方便。

文档:https://xadmin.readthedocs.io/en/latest/index.html

1.1. 安装

通过如下命令安装xadmin的最新版

pip install https://codeload.github.com/sshwsfc/xadmin/zip/django2

在settings/dev.py配置文件中注册如下应用

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# 把apps目录设置环境变量中的导包路径
sys.path.append( os.path.join(BASE_DIR,"apps") )


INSTALLED_APPS = [
    ...
    'xadmin',
    'crispy_forms',
    'reversion',
    ...
]

# 修改使用中文界面
LANGUAGE_CODE = 'zh-Hans'

# 修改时区
TIME_ZONE = 'Asia/Shanghai'

xadmin有建立自己的数据库模型类,需要进行数据库迁移

python manage.py makemigrations
python manage.py migrate

在总路由中添加xadmin的路由信息

import xadmin
xadmin.autodiscover()

# version模块自动注册需要版本控制的 Model
from xadmin.plugins import xversion
xversion.register_models()

urlpatterns = [
    path(r'xadmin/', xadmin.site.urls),
]

创建超级用户 -- 有的话不用创建

python manage.py createsuperuser

1.2. 使用

  • xadmin不再使用Django的admin.py,而是需要编写代码在adminx.py文件中。
  • xadmin的站点管理类不用继承admin.ModelAdmin,而是直接继承object即可。

1.2.1 站点的全局配置

在子应用users中创建adminx.py文件

import xadmin
from xadmin import views

class BaseSetting(object):
    """xadmin的基本配置"""
    enable_themes = True  # 开启主题切换功能
    use_bootswatch = True

xadmin.site.register(views.BaseAdminView, BaseSetting)

class GlobalSettings(object):
    """xadmin的全局配置"""
    site_title = "荏苒后台"  # 设置站点标题
    site_footer = "红浪漫有限公司"  # 设置站点的页脚
    menu_style = "accordion"  # 设置菜单折叠

xadmin.site.register(views.CommAdminView, GlobalSettings)

1.2.2 站点Model管理

xadmin可以使用的页面样式控制基本与Django原生的admin一直。

  • list_display 控制列表展示的字段

    list_display = ['id', 'btitle', 'bread', 'bcomment']
    
  • search_fields 控制可以通过搜索框搜索的字段名称,xadmin使用的是模糊查询

    search_fields = ['id','btitle']
    
  • list_filter 可以进行过滤操作的列,对于分类、性别、状态

    list_filter = ['is_delete']
    
  • ordering 默认排序的字段

  • readonly_fields 在编辑页面的只读字段

  • exclude 在编辑页面隐藏的字段

  • list_editable 在列表页可以快速直接编辑的字段

  • show_detail_fields 在列表页提供快速显示详情信息

  • refresh_times 指定列表页的定时刷新

    refresh_times = [5, 10,30,60]  # 设置允许后端管理人员按多长时间(秒)刷新页面
    
  • list_export 控制列表页导出数据的可选格式

    list_export = ('xls', 'xml', 'json')   list_export设置为None来禁用数据导出功能
    list_export_fields = ('id', 'title', 'pub_date') # 允许导出的字段
    
  • show_bookmarks 控制是否显示书签功能

    show_bookmarks = True
    
  • data_charts 控制显示图表的样式

    data_charts = {
            "order_amount": {
              'title': '图书发布日期表', 
              "x-field": "bpub_date", 
              "y-field": ('btitle',),
              "order": ('id',)
            },
        #    支持生成多个不同的图表
        #    "order_amount": {
        #      'title': '图书发布日期表', 
        #      "x-field": "bpub_date", 
        #      "y-field": ('btitle',),
        #      "order": ('id',)
        #    },
        }
    
    • title 控制图标名称
    • x-field 控制x轴字段
    • y-field 控制y轴字段,可以是多个值
    • order 控制默认排序
  • model_icon 控制菜单的图标

    class BookInfoAdmin(object):
        model_icon = 'fa fa-gift'
    
    xadmin.site.register(models.BookInfo, BookInfodmin)
    
修改admin或者xadmin站点下的子应用成中文内容。
# 在子应用的apps下面的配置中,新增一个属性verbose_name
from django.apps import AppConfig

class StudentsConfig(AppConfig):
    name = 'students'
    verbose_name = "学生管理"


# 然后在子应用的__init__.py里面新增一下代码:
default_app_config = "students.apps.StudentsConfig"

JWT认证

Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

在用户注册或登录后,我们想记录用户的登录状态,或者为用户创建身份认证的凭证。我们不再使用Session认证机制,而使用Json Web Token认证机制。

JWT的构成

JWT就一段字符串,由三段信息构成的,将这三段信息文本用.链接一起就构成了Jwt字符串。就像这样:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature).

jwt的头部承载两部分信息:

  • 声明类型,这里是jwt
  • 声明加密的算法 通常直接使用(哈希算法不能解密) HMAC SHA256

完整的头部就像下面这样的JSON:

{
  'typ': 'JWT',
  'alg': 'HS256'
}

然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分.

# python代码:
headers = '{"tpy":"JWT","alg":"HS256"}'
import base64
# 加密:base64.b64encode()
# 解密:base64.b64decode()
header_str = base64.b64encode(headers.encode()).decode()
print(header_str)
# 打印效果:
# eyJ0cHkiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

payload

载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分

  • 标准声明
  • 公共声明
  • 私有声明

标准中注册的声明 (建议但不强制使用) :

  • iss: jwt签发者
  • sub: jwt所面向的用户
  • aud: 接收jwt的一方
  • exp: jwt的过期时间,这个过期时间必须要大于签发时间
  • nbf: 定义在什么时间之前,该jwt都是不可用的.
  • iat: jwt的签发时间
  • jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

公共的声明 : 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.

私有的声明 : 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。

定义一个payload:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true,
  "auth": "2dsg343sdaq223256ddd5454",
}

然后将其进行base64加密,得到JWT的第二部分。

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

signature

JWT的第三部分是一个签证信息,这个签证信息的主要作用并非防止解密,而是为了防止别人恶意串改,由三部分组成:

  • header (base64加密后的字符串)
  • payload (base64加密后的字符串)
  • secret密钥

这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。

// javascript如果要模拟生成你的jwttoken,可能可以采用以下代码生成[注意:伪代码]
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);

var signature = HMACSHA256(encodedString, 'secret'); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

将这三部分用.连接成一个完整的字符串,构成了最终的jwt:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。

关于签发和核验JWT,我们可以使用Django REST framework JWT扩展来完成。

安装配置JWT

安装

pip install djangorestframework-jwt 

settings/dev.py配置文件

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': ( #优先使用jwt认证(谁在前边优先使用谁)
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.BasicAuthentication',
    ),
}

import datetime
JWT_AUTH = {
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1), #指明token的有效期
}

手动生成jwt的token值

Django REST framework JWT 扩展的说明文档中提供了手动签发JWT的方法

from rest_framework_jwt.settings import api_settings

jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER

payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)

在用户注册或登录成功后,在序列化器中返回用户信息以后同时返回token即可。

后端实现登陆认证接口

Django REST framework JWT提供了登录获取token的视图,可以直接使用

在子应用users创建路由urls.py文件

from django.urls import path
from rest_framework_jwt.views import obtain_jwt_token
# obtain_jwt_token 生成token,验证用户名和密码
# username:'xx' password:'oo' 必须post请求 -- 去users应用下user表中查询
urlpatterns = [
    path('login/', obtain_jwt_token),
]

在主路由urls.p中,引入当前子应用的路由文件

from django.urls import path,include
urlpatterns = [
		...
    path('users/', include("users.urls")),
    # include 的值必须是 模块名.urls 格式,字符串中间只能出现一个圆点
]

接下来,我们可以通过postman来测试下功能,post请求,body中加: --看是否能拿到token值

{ 
    "username":"root",
    "password":"1"
}

1.3 前端实现登陆功能

在登陆组件Login.vue中找到登陆按钮,绑定点击事件

<button class="login_btn" @click="loginhander">登录</button>

在methods中请求后端

<input v-model="username" placeholder="手机号或邮箱" type="text" name="session[email_or_mobile_number]"id="session_email_or_mobile_number">
<input v-model="password" placeholder="密码" type="password" name="password" id="session_password">
<!--找到用户名和密码标签,加v-model,方便vue取值-->

<script>
export default {
  name: "Login",
  data(){
    return{
      username:'',
      password:'',
    }
  },
  methods:{
    loginhander(){//登陆
        // this.$settings.host 获取settings.js文件中配置的IP地址
      this.$axios.post(`${this.$settings.host}/users/login/`,{
        username:this.username, // v-model 提取页面数据
        password:this.password,
      }).then((res)=>{//登陆成功
        console.log('res>>>',res);
      }).catch((error)=>{//登陆失败
        console.log('error>>>',error);
      })
    },
  }
}
</script>

前端settings.js

export default {
  'host': 'http://api.renran.com:8000',

}

1. 前端保存jwt

我们可以将JWT保存在cookie中,也可以保存在浏览器的本地存储里,我们保存在浏览器本地存储中

浏览器的本地存储提供了sessionStorage 和 localStorage 两种js代码:

  • sessionStorage 浏览器关闭即失效
  • localStorage 长期有效

使用方法

sessionStorage.变量名 = 变量值   // 保存数据
sessionStorage.变量名  // 读取数据
sessionStorage.clear()  // 清除所有sessionStorage保存的数据

localStorage.变量名 = 变量值   // 保存数据
localStorage.变量名  // 读取数据
localStorage.clear()  // 清除所有localStorage保存的数据

登陆组件代码Login.vue

<input type="checkbox" v-model="remember_me"  name="remember_me"
id="session_remember_me"><span>记住我</span>
<!--找到记住我标签,加v-model,方便vue取值-->
<script>
export default {
  name: "Login",
  data(){
    return{
      username:'',
      password:'',
      remember_me:false,
    }
  },
  methods:{
    loginhander(){
      this.$axios.post(`${this.$settings.host}/users/login/`,{
        username:this.username,
        password:this.password,
      }).then((res)=>{//用户名密码输入成功
        console.log('res>>>',res);
        // 使用浏览器本地存储保存token
        if(this.remember_me){
            //记住登陆
          localStorage.token = res.data.token; 
          sessionStorage.removeItem('token');//清除临时存储的token值
        }else {//否则反过来,没记住登陆
          sessionStorage.token = res.data.token;
          localStorage.removeItem('token');
        }

        //登录成功给予提示信息,选择是否跳转到首页
        this.$confirm('恭喜登录成功, 是否继续访问?', '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {//点击确认
          this.$router.push('/') //跳转页面

        }).catch(() => {//点击取消
          this.$message({
            type: 'info',
            message: '问什么'
          });
        });

      }).catch((error)=>{//用户名密码输入错误,给予提示
          this.$message({
            type: 'error',//红色提示
            message: '用户名或密码有误!'
          });
      })
    },
  }
}
</script>

Django上传图片

settings/dev.py

# 设置上传文件路径 -- 主目录下创建media文件夹 
MEDIA_ROOT=os.path.join(BASE_DIR,'media')
avatar = models.ImageField(upload_to="avatar", null=True, blank=True,default=None, verbose_name="头像")
# 上传图片数据,会在media文件下创建avatar文件夹存储图片数据

2.自定义响应内容

默认的返回值仅有token,我们还需在返回值中增加username和id,方便在客户端页面中显示当前登陆用户

通过修改该视图的返回值可以完成我们的需求。

在users/utils.py 中,创建

def jwt_response_payload_handler(token, user=None, request=None):
    """
    user模型类对象
    自定义jwt认证成功返回数据
    """
    return {
        'token': token,
        'id': user.id,
        'username': user.username,
        'avatar': user.avatar.url, #图片路径数据
        'nickname': user.nickname,
    }

修改settings/dev.py配置文件

# JWT
import datatime
JWT_AUTH = {
    'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1),
    'JWT_RESPONSE_PAYLOAD_HANDLER': 'users.utils.jwt_response_payload_handler',
}

登陆组件代码Login.vue

# 把自定义响应的字段保存到本地
<script>
export default {
  name: "Login",
  data(){
    return{
      username:'',
      password:'',
      remember_me:false,
    }
  },
  methods:{
    loginhander(){
      this.$axios.post(`${this.$settings.host}/users/login/`,{
        username:this.username,
        password:this.password,
      }).then((res)=>{//用户名密码输入成功
        console.log('res>>>',res);
        //判断怎样保存token
        if(this.remember_me){
          localStorage.token = res.data.token; //勾选了,长期保存token值到本地
          localStorage.id = res.data.id; 
          localStorage.username = res.data.username; 
          localStorage.avatar = res.data.avatar; 
          localStorage.nickname = res.data.nickname; 
          sessionStorage.removeItem('token');//清除临时存储的token值
          sessionStorage.removeItem('id');
          sessionStorage.removeItem('username');
          sessionStorage.removeItem('avatar');
          sessionStorage.removeItem('nickname');
        }else {//否则反过来
          sessionStorage.token = res.data.token;
          sessionStorage.id = res.data.id;
          sessionStorage.username = res.data.username;
          sessionStorage.avatar = res.data.avatar;
          sessionStorage.nickname = res.data.nickname;
          localStorage.removeItem('token');
          localStorage.removeItem('id');
          localStorage.removeItem('username');
          localStorage.removeItem('avatar');
          localStorage.removeItem('nickname');
        }

        //登录成功给予提示信息,选择是否跳转到首页
        this.$confirm('恭喜登录成功, 是否继续访问?', '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
          this.$router.push('/') //跳转页面

        }).catch(() => {
          this.$message({
            type: 'info',
            message: '问什么'
          });
        });

      }).catch((error)=>{//用户名密码输入错误,给予提示
          this.$message({
            type: 'error',//红色提示
            message: '用户名或密码有误!'
          });
      })
    },
  }

}
</script>

3. 多条件登录

JWT扩展的登录视图,在收到用户名与密码时,也是调用Django的认证系统中提供的authenticate()来检查用户名与密码是否正确。

我们可以通过修改Django认证系统的认证后端(主要是authenticate方法)来支持登录账号既可以是用户名也可以是手机号。

修改Django认证系统的认证后端需要继承django.contrib.auth.backends.ModelBackend,并重写authenticate方法。

authenticate(self, request, username=None, password=None, **kwargs)方法的参数说明:

  • request 本次认证的请求对象
  • username 本次认证提供的用户账号
  • password 本次认证提供的密码

我们想要让用户既可以以用户名登录,也可以以手机号登录,那么对于authenticate方法而言,username参数即表示用户名或者手机号。

重写authenticate方法的思路:

  1. 根据username参数查找用户User对象,username参数可能是用户名,也可能是手机号
  2. 若查找到User对象,调用User对象的 check_password 方法检查密码是否正确

在users/utils.py中编写:

# 自定义ModelBackend类,使多条件登录认证

from django.contrib.auth.backends import ModelBackend
from .models import User # 引入用户表
from django.db.models import Q # 引入Q查询

def get_user_object(account):
    try:
        # 设置用户名,手机号,邮箱都能登录
        # 根据账号信息获取用户模型
        user = User.objects.get(Q(username=account) | Q(mobile=account) | Q(email=account))
        return user
    except Exception:
        return None

class CustomModelBackend(ModelBackend):
    def authenticate(self, request, username=None, password=None, **kwargs):
        user = get_user_object(username)
        # 进行帐号与密码验证
        if user and user.check_password(password):
             return user

在配置文件settings/dev.py中告知Django使用我们自定义的认证后端

AUTHENTICATION_BACKENDS = [
    'users.utils.CustomModelBackend',
]

1.4 接入腾讯防水墙验证码

官网: https://007.qq.com

使用微信扫码登录腾讯云控制台,然后根据官方文档,把验证码集成到项目中

快速接入:https://007.qq.com/python-access.html?ADTAG=acces.start

  1. 访问地址: https://cloud.tencent.com/document/product/1110/36839

  2. 访问云API秘钥

  3. 访问验证码控制台: https://console.cloud.tencent.com/captcha

  4. 新建验证[ 获取AppID ]

1.客户端接入

1.防水墙的前端核心js文件

下载地址:https://ssl.captcha.qq.com/TCaptcha.js

文件代码复制到static/js/captcha.js文件中

在index.html中引入captcha.js文件

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>renranweb</title>
    <!--引入腾讯防水墙验证js文件-->
    <script src="static/js/captcha.js"></script>
  </head>
  <body>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

2.在settings.js中添加配置:验证码APPID

export default {
 	'host': 'http://api.renran.com:8000',
	'captcha_app_id':'2032172811 ',//验证码APPID
}

3.前端接入回调函数

//方法1: 直接生成一个验证码对象。
var captcha1 = new TencentCaptcha('appId', function(res) {/* callback */});
captcha1.show(); // 显示验证码

# 验证码对象调用方法
show	显示验证码。	
destroy	隐藏验证码。	
getTicket	获取验证码验证成功后的 ticket。

# 验证成功返回结果:
ret		Int		验证结果,0:验证成功。2:用户主动关闭验证码。
ticket	String	验证成功的票据,当且仅当 ret = 0 时 ticket 有值。
appid	String	场景 ID。
randstr	String	本次验证的随机串,请求后台接口时需带上。

4.Login.vue代码 --前端获取显示并校验验证码

<template>
...
        <div class="normal-title">
          <router-link class="active" to="/login">登录</router-link>
          <b>·</b>
          <router-link id="js-sign-up-btn" class="" to="/register">注册</router-link>
        </div>
...
          <button @click="show_captcha" class="sign-in-button" id="sign-in-form-submit-btn" type="button">
            <span id="sign-in-loading"></span>
            登录
          </button>
...
</template>

<script>
export default {
  name: "Login",
  data(){
    return{
      username:'',
      password:'',
      remember_me:false,
    }
  },
  methods:{
    show_captcha(){
      let self = this
      // 直接生成一个验证码对象。--回调函数
      var captcha1 = new TencentCaptcha(`${this.$settings.captcha_app_id}`, function(res) {
      // 滑动成功 向后台发送数据
        self.$axios.get(`${self.$settings.host}/users/check_captcha_data/`,{
          // 回调结果 ret -- 验证结果,0:验证成功。2:用户主动关闭验证码。
          params:{
          randstr:res.randstr,//本次验证的随机串,请求后台接口时需带上。
          ticket:res.ticket, //验证成功的票据,当且仅当 ret = 0 时 ticket 有值。
          }
        }).then((res)=>{
          // 客户端校验用户名和密码是否符合格式
          self.loginhander();
        }).catch((error)=>{
          self.$message.error("验证码校验错误!");
        })
      });
      captcha1.show(); // 显示验证码
    },

    loginhander(){
      this.$axios.post(`${this.$settings.host}/users/login/`,{
        username:this.username,
        password:this.password,
      }).then((res)=>{//用户名密码输入成功
        console.log('res>>>',res);
        //判断怎样保存token
        if(this.remember_me){
          localStorage.token = res.data.token; //勾选了,长期保存token值到本地
          localStorage.id = res.data.id; //勾选了,长期保存token值到本地
          localStorage.username = res.data.username; //勾选了,长期保存token值到本地
          localStorage.avatar = res.data.avatar; //勾选了,长期保存token值到本地
          localStorage.nickname = res.data.nickname; //勾选了,长期保存token值到本地
          sessionStorage.removeItem('token');//清除临时存储的token值
          sessionStorage.removeItem('id');//清除临时存储的token值
          sessionStorage.removeItem('username');//清除临时存储的token值
          sessionStorage.removeItem('avatar');//清除临时存储的token值
          sessionStorage.removeItem('nickname');//清除临时存储的token值
        }else {//否则反过来
          sessionStorage.token = res.data.token;
          sessionStorage.id = res.data.id;
          sessionStorage.username = res.data.username;
          sessionStorage.avatar = res.data.avatar;
          sessionStorage.nickname = res.data.nickname;
          localStorage.removeItem('token');
          localStorage.removeItem('id');
          localStorage.removeItem('username');
          localStorage.removeItem('avatar');
          localStorage.removeItem('nickname');
        }

        //登录成功给予提示信息,选择是否跳转到首页
        this.$confirm('恭喜登录成功, 是否继续访问?', '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
          this.$router.push('/') //跳转页面
        }).catch(() => {
          this.$message({
            type: 'info',
            message: '问什么'
          });
        });

      }).catch((error)=>{//用户名密码输入错误,给予提示
          this.$message({
            type: 'error',//红色提示
            message: '用户名或密码有误!'
          });
      })
    },
  }
}
</script>

2.服务端(后台)接入

api服务端接入验证码的文档说明: https://007.qq.com/python-access.html?ADTAG=acces.start

1.票据验证接口

GET接口
URL: https://ssl.captcha.qq.com/ticket/verify 

# 验证路径下发请求,并携带以下数据        
字段名					描述
aid(必填)				验证AppID	
AppSecretKey (必填)	验证秘钥
Ticket (必填)			验证码客户端验证回调的票据
Randstr (必填)		验证码客户端验证回调的随机串
UserIP (必填)			提交验证的用户的IP地址

# 返回值
Json格式,	eg:{response:1, evil_level:70, err_msg:""}

字段名			描述
response	1:验证成功,0:验证失败,100:AppSecretKey参数校验错误
evil_level	[0,100],恶意等级[optional]
err_msg	  	验证错误信息[optional],查看详细说明

2.把验证路径,秘钥和AppID 保存到settings/dev.py配置文件中.

# 腾讯防水墙设置
CAPTCHA_INFO = {
    # 第三方验证路径
    'check_url':'https://ssl.captcha.qq.com/ticket/verify',
    'APPID':'2032172811 ',
    'AppSecretKey':'0o_BkrkEsPa-4m3pFjXABqQ**',
}

3.服务端接口实现

子路由users/urls.py

from django.urls import path
from rest_framework_jwt.views import obtain_jwt_token
from . import views

urlpatterns = [
    path('check_captcha_data/', views.CaptchaView.as_view()),# 防水墙验证
]

视图函数users/views.py

import json
from urllib.request import urlopen  # 引入方法向第三方发送数据
from urllib.parse import urlencode  # 加工数据格式
from rest_framework.views import APIView
from rest_framework.response import Response
from django.conf import settings
from rest_framework import status

# 接收客户端滑动验证码数据并进行第三方验证
class CaptchaView(APIView):
    def get(self, request):
        params = { # 发送给第三方验证键不可变动
            'Randstr': request.query_params.get('randstr'),
            'Ticket': request.query_params.get('ticket'),
            'UserIP': request.META.get('REMOTE_ADDR'),  # 获取用户IP
            'aid': settings.CAPTCHA_INFO.get('APPID'),
            'AppSecretKey': settings.CAPTCHA_INFO.get('AppSecretKey'),
        }
        # 第三方路径
        check_url = settings.CAPTCHA_INFO.get('check_url')
        
        # 数据需要加工消息格式,才能发给第三方
        # 例如:{'a':1,'b':2}-->a=1&b=2
        params = urlencode(params)

        # get请求发送数据方法得到的响应对象
        f = urlopen("%s?%s" % (check_url, params))
        # post请求方法:
        # f = urlopen(check_url, params)
        
        content = f.read() # 读取响应内容json格式
        res = json.loads(content) # 反序列化

        if res.get('response') == '1': # 验证通过
            return Response({'code': 1, 'msg': '验证成功!'})
        else:
            return Response({'code': 0, 'msg': '验证失败!'}, status=status.HTTP_400_BAD_REQUEST)

2.用户的注册认证

1.注册页面

前端显示注册页面并调整首页头部和登陆页面的注册按钮的链接。

注册页面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="/login">登录</router-link>
          <b>·</b>
          <router-link id="js-sign-up-btn" class="active" to="/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>

前端注册路由routers/index.js:


import Register from '@/components/Register'

Vue.use(Router)

export default new Router({
  // 设置路由模式为‘history’,去掉默认的#
  mode: "history",
  routes: [
    {
      path: '/register',
      name: 'Register',
      component: Register
    },
  ]
})

修改首页头部的连接:

# Header.vue
<span class="header-register"><router-link to="/register">注册</router-link></span>

# Login.vue
<div class="normal-title">
    <router-link class="active" to="/login">登录</router-link>
    <b>·</b>
    <router-link id="js-sign-up-btn" class="" to="/register">注册</router-link>
</div>

# Register.vue
<div class="normal-title">
    <router-link to="/login">登录</router-link>
    <b>·</b>
    <router-link id="js-sign-up-btn" class="active" to="/register">注册</router-link>
</div>

2.注册功能的实现

实现基本的账号信息注册

后台路由users/urls.py,代码:

path('register/', views.RegisterView.as_view()),

后台视图代码users/views.py:

from rest_framework.generics import CreateAPIView #引用视图
from .models import User #引用用户模型类
from .serializers import RegisterModelSerializer #引用自定义序列化器类
class UserCreateAPIView(CreateAPIView):
    queryset = User.objects.all()
    serializer_class = RegisterModelSerializer

序列化器中,验证和保存数据,并返回jwt登录认证

创建文件users/serializers.py,自定义序列化器类

from rest_framework import serializers
from .models import User
from django.contrib.auth.hashers import make_password # 系统加密密码
import re

class RegisterModelSerializer(serializers.ModelSerializer):
    # 有些数据User表中是没有的,例如:短信验证码sms_code,token指需要单独声明
    token = serializers.CharField(max_length=126,read_only=True)  # 数据不需要提交,需要返回值
    sms_code = serializers.CharField(max_length=12,write_only=True) # 数据需要提交,不需要返回值

    class Meta:
        model = User
        # 取出那些字段进行序列化
        fields = ['id','nickname','token','username','password','mobile','sms_code']
        extra_kwargs = { # 字段声明需不需要返回提交
            'id':{'read_only':True,},
            'username':{'read_only':True,},
            'password':{'write_only':True,},
            'mobile':{'write_only':True,},
        }

    # 全局钩子进行校验
    def validate(self, attrs):
        # 1.昵称不能重复
        nickname = attrs.get('nickname')
        res = User.objects.filter(nickname=nickname)
        if res:
            raise serializers.ValidationError('昵称不能重复')

        # todo 2.密码格式校验



        # 3.手机号不能重复,唯一性校验
        mobile = attrs.get('mobile')
        res = User.objects.filter(mobile=mobile)
        if res:
            raise serializers.ValidationError('手机号以被注册')
        # 4.手机号格式校验
        mobile = attrs.get("mobile")
        if not re.match("^1[3-9]\d{9}$", mobile):
            raise serializers.ValidationError("手机号码格式错误!")

        # todo 短信验证码校验

        return attrs

    # 验证过后保存数据,save()方法自动保持只保存校验成功数据
    def create(self, validated_data):

        # user模型类对象写想要保存的数据
        user = User.objects.create(**{
            'nickname':validated_data.get('nickname'),
            'username':validated_data.get('nickname'),
            'password':make_password(validated_data.get('password')), # 保存密文密码
            'mobile':validated_data.get('mobile'),
        })

        # 手动生成jwt中的token值 -- 为了用户注册完直接进入登录状态,不需要重复登录了
        from rest_framework_jwt.settings import api_settings

        jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
        jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
        payload = jwt_payload_handler(user)
        token = jwt_encode_handler(payload)

        user.token = token # 把token值加到user模型类对象中,进行序列化,好向客户端返回token值

        return user

客户端提交注册信息

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" @click.prevent="registerHandler">
          <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;
            }
          }
        },
        methods:{
          registerHandler(){
            // todo 数据验证

            // 注册处理
            this.$axios.post(`${this.$settings.Host}/users/`,{
                mobile: this.mobile,
                nickname: this.nickname,
                password: this.password,
                sms_code: this.sms_code,
            }).then(response=> {
              sessionStorage.user_token = response.data.token;
              sessionStorage.user_id = response.data.id;
              sessionStorage.user_name = response.data.username;
              sessionStorage.user_avatar = response.data.avatar;
              sessionStorage.user_nickname = response.data.nickname;

              this.$confirm('注册成功, 欢迎加入荏苒!', '提示', {
                confirmButtonText: '跳转到首页',
                cancelButtonText: '跳转上一页',
                type: 'success'
              }).then(() => {
                this.$router.push("/");
              }).catch(() => {
                this.$router.go(-1);
              });

            }).catch(error=>{
                this.$message.error("用户注册失败!");
            })
          }
        }
    }
</script>

3.在注册功能中集成短信验证码功能

接下来,我们把注册过程中一些注册信息(例如:短信验证码)和session缓存到redis数据库中。

安装django-redis。

pip install django-redis
redis-server # 终端启动服务
redis-cli # 进入redis数据库
select 索引(0-15) # 选择数据库(一共有16个库)

在settings/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"

django-redis提供了get_redis_connection的方法,通过调用get_redis_connection方法传递redis的配置名称可获取到redis的连接对象,通过redis连接对象可以执行redis命令

使用范例:

from django_redis import get_redis_connection
// 链接redis数据库
redis_conn = get_redis_connection("default")

Redis官方命令手册:http://www.redis.cn/

Redis-py文档 https://redis-py.readthedocs.io/en/stable/ -- 可查看Redis-py与Redis之间对应的命令

使用云联云发送短信

1.后台python sdk 安装

网址: https://doc.yuntongxun.com/p/5f029ae7a80948a1006e776e

安装SDK

pip install ronglian_sms_sdk

初始化SDK

from ronglian_sms_sdk import SmsSDK
sdk = SmsSDK(accId, accToken, appId)

参数	      类型	  说明
accId	   String	可在控制台首页看到开发者主账号ACCOUNT SID
accToken   String	可在控制台首页看到主账号令牌AUTH TOKEN
appId	   String	请使用管理控制台中已创建应用的APPID

调用发送短信方法

sdk.sendMessage(tid, mobile, datas)

参数		类型		说明
tid		String	短信模板 ID
mobile	String	发送手机号,多个以英文逗号分隔,最多 200 个号码
datas	tuple	替换短信模板占位符的内容变量

响应参数

参数				类型		说明
statusCode		String	状态码,000000 为发送成功
dateCreated		String	短信的创建时间,格式:yyyyMMddHHmmss
smsMessageSid	tuple	短信唯一标识符

示例:

from ronglian_sms_sdk import SmsSDK

# 可以把主账号信息配置的配置文件中
accId = '容联云通讯分配的主账号ID'
accToken = '容联云通讯分配的主账号TOKEN'
appId = '容联云通讯分配的应用ID'
 
def send_message():
    sdk = SmsSDK(accId, accToken, appId)
    tid = '容联云通讯创建的模板ID'
    mobile = '手机号1,手机号2'
    datas = ('验证码', '时效/分钟')
    resp = sdk.sendMessage(tid, mobile, datas)
    print(resp)

2.后端短信验证

荣联云主账号信息配置到settings/dev.py配置文件中

# 荣联云主帐号信息
SMS_INFO = {
    'accId' : '8aaf0708780055cd0178252959bb0e5d',
    'accToken' :'0e3f59a9f6fa4cf2944ca18e9c2e363d',
    'appId' : '8aaf0708780055cd017825295ae60e63',
    'tid':{ # 模板id
        'register':'1',
        'login':'2',
}

常量信息配置settings/comtains.py中

# 短信验证码有效期/秒
SMS_CODE_EXPIRE_TIME = 5 * 60
# 短信验证码发送时间间隔/秒
SMS_CODE_INTERVAL_TIME = 60

路由users/urls.py

urlpatterns = [
    path('sms_code/', views.SmsView.as_view()),# 短信验证
]

视图users/views.py -- 短信验证码逻辑

import random
import longging
from ronglian_sms_sdk import SmsSDK
from django.conf import settings
from renranapi.settings import contains # 引入常量
from django_redis import get_redis_connection  # 引入redis库
from .utils import get_user_object # 验证手机号


# 短信验证码
class SmsView(APIView):

    def get(self, request):
        try:
            mobile = request.query_params.get('mobile')  # 手机号
            accId = settings.SMS_INFO.get('accId')
            accToken = settings.SMS_INFO.get('accToken')
            appId = settings.SMS_INFO.get('appId')
            tid = settings.SMS_INFO.get('tid').get('register')  # 荣联云短信模板id
            expire_time = contains.SMS_CODE_EXPIRE_TIME   # 短信过期时间
            interval_time = contains.SMS_CODE_INTERVAL_TIME # 短信间隔时间

            conn = get_redis_connection('sms_code') # 拿到储存短信验证码库

            # 先验证手机号是否已经被注册
            user = get_user_object(mobile)
            if user:
                return Response({'error':'该手机号已经被注册,请核实'},status=400)

            # 短信间隔内,短信是否已经发送过
            inter_code = conn.get(f'sms_interval_{mobile}')
            if inter_code:
                return Response({'error':'60秒内已经发送过短信了,请稍等!'},status=400)

            # 生成短信验证码
            code = '%04d' % random.randint(0, 9999)  # 随机4位短信验证码

            # 保存短信验证码
            # redis优化 一次链接储存多个数据,尽量减少链接
            pipe = conn.pipeline()
            pipe.multi() # 开启redis的批量操作

            pipe.setex(f'sms_{mobile}',expire_time,code) # 存有效期数据
            pipe.setex(f'sms_interval_{mobile}',interval_time,code) # 存间隔时间数据

            pipe.execute() #执行批量操作

            # 发送短信验证码
            sdk = SmsSDK(accId, accToken, appId)
            sdk.sendMessage(tid, mobile, (code, expire_time//60))

            return Response({'msg': 'ok'})
        except Exception as e:
            # 发生错误,记录日志
            logger = logging.getLogger('django')
            logger.error(f'{mobile}--短信发送失败--{str(e)}')

            return Response({'error':'短信发送失败'},status=500)

序列化器类users/serializers.py -- 短信验证码校验

from rest_framework import serializers
from .models import User
from django.contrib.auth.hashers import make_password # 系统加密密码
import re
from django_redis import get_redis_connection

class RegisterModelSerializer(serializers.ModelSerializer):
    # 有些数据User表中是没有的,例如:短信验证码sms_code,token指需要单独声明
    token = serializers.CharField(max_length=126,read_only=True)  # 数据不需要提交,需要返回值
    sms_code = serializers.CharField(max_length=12,write_only=True) # 数据需要提交,不需要返回值

    class Meta:
        model = User
        # 取出那些字段进行序列化
        fields = ['id','nickname','token','username','password','mobile','sms_code']
        extra_kwargs = { # 字段声明需不需要返回提交
            'id':{'read_only':True,},
            'username':{'read_only':True,},
            'password':{'write_only':True,},
            'mobile':{'write_only':True,},
        }

    # 全局钩子进行校验
    def validate(self, attrs):
        # 1.昵称不能重复
        nickname = attrs.get('nickname')
        res = User.objects.filter(nickname=nickname)
        if res:
            raise serializers.ValidationError('昵称不能重复')

        # todo 2.密码格式校验

        # 3.手机号不能重复,唯一性校验
        mobile = attrs.get('mobile')
        res = User.objects.filter(mobile=mobile)
        if res:
            raise serializers.ValidationError('手机号以被注册')
        
        # 4.手机号格式校验
        mobile = attrs.get("mobile")
        if not re.match("^1[3-9]\d{9}$", mobile):
            raise serializers.ValidationError("手机号码格式错误!")

        # 5.短信验证码校验
        sms_code = attrs.get('sms_code') # 取出用户发过来的验证码
        conn = get_redis_connection('sms_code')
        redis_code = conn.get(f'sms_{mobile}') # redis取出来的是bytes数据,取不到值返回none

        if not redis_code:
            raise serializers.ValidationError('短信验证码已经过期!请重新发送')
        if sms_code != redis_code.decode():
            raise serializers.ValidationError('短信验证码输入有误!')

        return attrs

    # 验证过后保存数据,save()方法自动保持只保存校验成功数据
    def create(self, validated_data):

        # user模型类对象写想要保存的数据
        user = User.objects.create(**{
            'nickname':validated_data.get('nickname'),
            'username':validated_data.get('nickname'),
            'password':make_password(validated_data.get('password')), # 保存密文密码
            'mobile':validated_data.get('mobile'),
        })

        # 手动生成jwt中的token值 -- 为了用户注册完直接进入登录状态,不需要重复登录了
        from rest_framework_jwt.settings import api_settings

        jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
        jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
        payload = jwt_payload_handler(user)
        token = jwt_encode_handler(payload)

        user.token = token # 把token值加到user模型类对象中,进行序列化,好向客户端返回token值

        return user

3.客户端发送注册信息和发送短信

<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"> 
<!--1.登陆注册切换页面-->
          <router-link to="/login">登录</router-link>
          <b>·</b>
          <router-link id="js-sign-up-btn" class="active" to="/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">  
<!--2.手机号监听-->
                <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>
<!--3.发送手机验证码逻辑--> 
            <a @click.prevent="SendSmsCode" tabindex="-1"   class="btn-up-resend js-send-code-button" 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>
<!--4.点击注册按钮-->
          <input type="button" @click="RegisterHander" 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;
            }
          }
        },
        methods:{
          //短信验证码
          SendSmsCode(){

            this.$axios.get(`${this.$settings.host}/users/sms_code/`,{
              params:{
                mobile:this.mobile,

              }
            }).then((res)=>{
              // 冷却倒计时效果
              let inter_time = 60
              // 定时器
              let t = setInterval( ()=>{
                this.sms_code_text = `${inter_time}秒后重新发送!`
                inter_time--;
                if (inter_time <= 0){
                  clearInterval(t); //清掉定时器
                  this.sms_code_text = '发送验证码';

                }
              },1000)

            }).catch((error)=>{
              this.$message.error(error.response.data.error) //可以取出后台试图定义过的错误
            })
          },
          //注册
          RegisterHander(){
            this.$axios.post(`${this.$settings.host}/users/register/`,{
              nickname:this.nickname,
              mobile:this.mobile,
              sms_code:this.sms_code,
              password:this.password,
            }).then((res)=>{ //注册成功
              //临时保存登录信息
              sessionStorage.token = res.data.token;
              sessionStorage.id = res.data.id;
              sessionStorage.username = res.data.username;
              sessionStorage.nickname = res.data.nickname;

              //注册成功给予提示信息,选择是否跳转到首页
              this.$confirm('恭喜注册成功, 是否跳转到首页?', '提示', {
                confirmButtonText: '确定',
                cancelButtonText: '取消',
                type: 'warning'
              }).then(() => {
                this.$router.push('/') //跳转到首页
              }).catch(() => {
                this.$router.push('/login') //跳转到登录页面
              });
            }).catch((error)=>{ //注册失败
              this.$message({
                type: 'error',//红色提示
                message: '注册失败!请认真填写信息'
            })
          })
    },
        },
    }

</script>

posted @ 2021-03-28 20:00  十九分快乐  阅读(563)  评论(0编辑  收藏  举报