登录注册
登录页组件
Login.vue
1 <template> 2 <div class="login-box"> 3 <img src="https://www.luffycity.com/static/img/Loginbg.3377d0c.jpg" alt=""> 4 <div class="login"> 5 <div class="login-title"> 6 <img src="https://www.luffycity.com/static/img/Logotitle.1ba5466.png" alt=""> 7 <p>帮助有志向的年轻人通过努力学习获得体面的工作和生活!</p> 8 </div> 9 <div class="login_box"> 10 <div class="title"> 11 <span @click="login_type=0">密码登录</span> 12 <span @click="login_type=1">短信登录</span> 13 </div> 14 <div class="inp" v-if="login_type==0"> 15 <input v-model = "username" type="text" placeholder="用户名 / 手机号码" class="user"> 16 <input v-model = "password" type="password" name="" class="pwd" placeholder="密码"> 17 <div id="geetest1"></div> 18 <div class="rember"> 19 <p> 20 <input type="checkbox" class="no" name="a"/> 21 <span>记住密码</span> 22 </p> 23 <p>忘记密码</p> 24 </div> 25 <button class="login_btn">登录</button> 26 <p class="go_login" >没有账号 <span>立即注册</span></p> 27 </div> 28 <div class="inp" v-show="login_type==1"> 29 <input v-model = "username" type="text" placeholder="手机号码" class="user"> 30 <input v-model = "password" type="text" class="pwd" placeholder="短信验证码"> 31 <button id="get_code">获取验证码</button> 32 <button class="login_btn">登录</button> 33 <p class="go_login" >没有账号 <span>立即注册</span></p> 34 </div> 35 </div> 36 </div> 37 </div> 38 </template> 39 40 <script> 41 export default { 42 name: 'Login', 43 data(){ 44 return { 45 login_type: 0, 46 username:"", 47 password:"", 48 } 49 }, 50 51 methods:{ 52 53 }, 54 55 }; 56 </script> 57 <style scoped> 58 .login-box{ 59 width: 100%; 60 height: 100%; 61 position: relative; 62 overflow: hidden; 63 margin-top: -80px; 64 } 65 .login-box img{ 66 width: 100%; 67 min-height: 100%; 68 } 69 .login-box .login { 70 position: absolute; 71 width: 500px; 72 height: 400px; 73 left: 0; 74 margin: auto; 75 right: 0; 76 bottom: 0; 77 top: -220px; 78 } 79 .login .login-title{ 80 width: 100%; 81 text-align: center; 82 } 83 .login-title img{ 84 width: 190px; 85 height: auto; 86 } 87 .login-title p{ 88 font-size: 18px; 89 color: #fff; 90 letter-spacing: .29px; 91 padding-top: 10px; 92 padding-bottom: 50px; 93 } 94 .login_box{ 95 width: 400px; 96 height: auto; 97 background: #fff; 98 box-shadow: 0 2px 4px 0 rgba(0,0,0,.5); 99 border-radius: 4px; 100 margin: 0 auto; 101 padding-bottom: 40px; 102 } 103 .login_box .title{ 104 font-size: 20px; 105 color: #9b9b9b; 106 letter-spacing: .32px; 107 border-bottom: 1px solid #e6e6e6; 108 display: flex; 109 justify-content: space-around; 110 padding: 50px 60px 0 60px; 111 margin-bottom: 20px; 112 cursor: pointer; 113 } 114 .login_box .title span:nth-of-type(1){ 115 color: #4a4a4a; 116 border-bottom: 2px solid #84cc39; 117 } 118 119 .inp{ 120 width: 350px; 121 margin: 0 auto; 122 } 123 .inp input{ 124 outline: 0; 125 width: 100%; 126 height: 45px; 127 border-radius: 4px; 128 border: 1px solid #d9d9d9; 129 text-indent: 20px; 130 font-size: 14px; 131 background: #fff !important; 132 } 133 .inp input.user{ 134 margin-bottom: 16px; 135 } 136 .inp .rember{ 137 display: flex; 138 justify-content: space-between; 139 align-items: center; 140 position: relative; 141 margin-top: 10px; 142 } 143 .inp .rember p:first-of-type{ 144 font-size: 12px; 145 color: #4a4a4a; 146 letter-spacing: .19px; 147 margin-left: 22px; 148 display: -ms-flexbox; 149 display: flex; 150 -ms-flex-align: center; 151 align-items: center; 152 /*position: relative;*/ 153 } 154 .inp .rember p:nth-of-type(2){ 155 font-size: 14px; 156 color: #9b9b9b; 157 letter-spacing: .19px; 158 cursor: pointer; 159 } 160 161 .inp .rember input{ 162 outline: 0; 163 width: 30px; 164 height: 45px; 165 border-radius: 4px; 166 border: 1px solid #d9d9d9; 167 text-indent: 20px; 168 font-size: 14px; 169 background: #fff !important; 170 } 171 172 .inp .rember p span{ 173 display: inline-block; 174 font-size: 12px; 175 width: 100px; 176 /*position: absolute;*/ 177 /*left: 20px;*/ 178 179 } 180 #geetest{ 181 margin-top: 20px; 182 } 183 .login_btn{ 184 width: 100%; 185 height: 45px; 186 background: #84cc39; 187 border-radius: 5px; 188 font-size: 16px; 189 color: #fff; 190 letter-spacing: .26px; 191 margin-top: 30px; 192 } 193 .inp .go_login{ 194 text-align: center; 195 font-size: 14px; 196 color: #9b9b9b; 197 letter-spacing: .26px; 198 padding-top: 20px; 199 } 200 .inp .go_login span{ 201 color: #84cc39; 202 cursor: pointer; 203 } 204 </style>
index.js
绑定登陆页面路由地址
main.js
1 import Vue from "vue" 2 import Router from "vue-router" 3 4 // 导入需要注册路由的组件 5 import Home from "../components/Home" 6 import Login from "../components/Login" 7 Vue.use(Router); 8 9 // 配置路由列表 10 export default new Router({ 11 mode:"history", 12 routes:[ 13 // 路由列表 14 ... 15 { 16 name:"Login", 17 path: "/login", 18 component:Login, 19 } 20 ] 21 })
调整首页头部子组件中登陆按钮的链接信息
Header.vue
<router-link to="/login">登录</router-link>
后端实现登陆认证
Django默认已经提供了认证系统。认证系统包含:
-
用户管理
-
权限
-
用户组
-
密码哈希系统
-
用户登录或内容显示的表单和视图
-
一个可插拔的后台系统
Django默认用户的认证机制依赖Session机制,我们在项目中将引入JWT认证机制,将用户的身份凭据存放在Token中,然后对接Django的认证系统,帮助我们来实现:
-
用户的数据模型
-
用户密码的加密与验证
-
用户的权限系统
Django用户模型类
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
对象。当None
为raw_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_staff
和is_superuser
为True
。
创建用户模块的子应用
cd luffy/apps
python ../../manage.py startapp users
在settings.py文件中注册子应用。
INSTALLED_APPS = [
...
'users',
]
创建自定义的用户模型类
Django认证系统中提供的用户模型类及方法很方便,我们可以使用这个模型类,但是字段有些无法满足项目需求,如本项目中需要保存用户的手机号,需要给模型类添加额外的字段。
Django提供了django.contrib.auth.models.AbstractUser
用户抽象模型类允许我们继承,扩展字段来使用Django认证系统的用户模型类。
我们可以在apps中创建Django应用users,并在配置文件中注册users应用。
在创建好的应用models.py中定义用户的用户模型类。
为什么用charfield,长数字的存储有可能会发现溢位
1 from django.contrib.auth.models import AbstractUser 2 3 class User(AbstractUser): 4 """用户模型类""" 5 mobile = models.CharField(max_length=11, unique=True, verbose_name='手机号') 6 7 class Meta: 8 db_table = 'ly_users' 9 verbose_name = '用户' 10 verbose_name_plural = verbose_name
我们自定义的用户模型类还不能直接被Django的认证系统所识别,需要在配置文件中告知Django认证系统使用我们自定义的模型类。
在配置文件中进行设置
AUTH_USER_MODEL = 'users.User'
AUTH_USER_MODEL
参数的设置以点.
来分隔,表示应用名.模型类名
。
注意:Django建议我们对于AUTH_USER_MODEL参数的设置一定要在第一次数据库迁移之前就设置好,否则后续使用可能出现未知错误。
执行数据库迁移
python manage.py makemigrations
python manage.py migrate
当迁移出现问题,报错了,可以从django的解析器下找到site-package目录下的django里面的contrib里面的admin和auth,以及site-package下面的reversion以及xadmin里面的migrations目录
注意,防止手动操作误删数据,建议先备份。
1将users.User配置调到配置文件的中间件前面
2.确定users应用是否注册
3.删除数据库迁移记录和数据库所有的表结构
4 site-packages/django/contrib/admin文件夹复制备份好
5.site-packages/django/contrib/auth文件夹备份
6.xadmin备份
7.reversion备份
8.将各个auth/admin/xadmin下的所有migrations数据库迁移记录删掉
重新执行数据库迁移指令
Django REST framework JWT
在用户注册或登录后,我们想记录用户的登录状态,或者为用户创建身份认证的凭证。我们不再使用Session认证机制,而使用Json Web Token认证机制。
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).
该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供
者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所
必须的声明信息,该token也可直接被用于认证,也可被加密。
JWT的构成
JWT就一段字符串,由三段信息构成的,将这三段信息文本用.
链接一起就构成了Jwt字符串。就像这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature).
header
jwt的头部承载两部分信息:
-
声明类型,这里是jwt
-
声明加密的算法 通常直接使用 HMAC SHA256
完整的头部就像下面这样的JSON:
{ 'typ': 'JWT', 'alg': 'HS256' }
然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分.
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
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 }
然后将其进行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扩展来完成。
文档网站http://getblimp.github.io/django-rest-framework-jwt/
安装配置JWT
安装
pip install djangorestframework-jwt
settings/dev.py配置文件
1 REST_FRAMEWORK = { 2 'DEFAULT_AUTHENTICATION_CLASSES': ( 3 'rest_framework_jwt.authentication.JSONWebTokenAuthentication', 4 'rest_framework.authentication.SessionAuthentication', 5 'rest_framework.authentication.BasicAuthentication', 6 ), 7 }
-
JWT_EXPIRATION_DELTA 指明token的有效期
1 import datetime 2 JWT_AUTH = { 3 'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1), 4 }
生成jwt
Django REST framework JWT 扩展的说明文档中提供了手动签发JWT的方法
1 from rest_framework_jwt.settings import api_settings 2 3 jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER 4 jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER 5 6 payload = jwt_payload_handler(user) 7 token = jwt_encode_handler(payload)
在用户注册或登录成功后,在序列化器中返回用户信息以后同时返回token即可。
后端实现登陆认证接口
Django REST framework JWT提供了登录获取token的视图,可以直接使用
在子应用users路由urls.py中
1 from rest_framework_jwt.views import obtain_jwt_token 2 3 urlpatterns = [ 4 path(r'authorizations/', obtain_jwt_token, name='authorizations'), 5 ]
在主路由中,引入当前子应用的路由文件
1 urlpatterns = [ 2 ... 3 path('users/', include("users.urls")), 4 # include 的值必须是 模块名.urls 格式,字符串中间只能出现一个圆点 5 ]
接下来,我们可以通过postman来测试下功能
注意,测试之前因为我们上面自定义用户模型,所以删除了数据库信息,所以需要我们再次补充。
前端实现登陆功能
在登陆组件中找到登陆按钮,绑定点击事件
<button class="login_btn" @click="loginhander">登录</button>
在methods中请求后端
1 export default { 2 name: 'Login', 3 data(){ 4 return { 5 login_type: 0, 6 remember:false, // 记住密码 7 username:"", 8 password:"", 9 } 10 }, 11 12 methods:{ 13 // 登录 14 loginhander(){ 15 this.$axios.post(this.$settings.Host+"/users/login/",{"username":this.username,"password":this.password}).then(response=>{ 16 console.log(response.data) 17 }).catch(error=>{ 18 console.log(error) 19 }) 20 } 21 }, 22 };
2.3.7 前端保存jwt
我们可以将JWT保存在cookie中,也可以保存在浏览器的本地存储里,我们保存在浏览器本地存储中
浏览器的本地存储提供了sessionStorage 和 localStorage 两种:
-
sessionStorage 浏览器关闭即失效
-
localStorage 长期有效
使用方法
1 sessionStorage.变量名 = 变量值 // 保存数据 2 sessionStorage.变量名 // 读取数据 3 sessionStorage.clear() // 清除所有sessionStorage保存的数据 4 5 localStorage.变量名 = 变量值 // 保存数据 6 localStorage.变量名 // 读取数据 7 localStorage.clear() // 清除所有localStorage保存的数据
登陆组件代码Login.vue
1 // 使用浏览器本地存储保存token 2 if (this.remember) { 3 // 记住登录 4 sessionStorage.clear(); 5 localStorage.token = response.data.token; 6 } else { 7 // 未记住登录 8 localStorage.clear(); 9 sessionStorage.token = response.data.token; 10 } 11 // 页面跳转回到上一个页面 也可以使用 this.$router.push("/") 回到首页 12 this.$router.go(-1)
默认的返回值仅有token,我们还需在返回值中增加username和id,方便在客户端页面中显示当前登陆用户
通过修改该视图的返回值可以完成我们的需求。
在users/utils.py 中,创建
def jwt_response_payload_handler(token, user=None, request=None): """ 自定义jwt认证成功返回数据 """ return { 'token': token, 'id': user.id, 'username': user.username }
修改settings/dev.py配置文件
# JWT JWT_AUTH = { 'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1), 'JWT_RESPONSE_PAYLOAD_HANDLER': 'users.utils.jwt_response_payload_handler', }
登陆组件代码Login.vue
1 // 使用浏览器本地存储保存token 2 if (this.remember) { 3 // 记住登录 4 sessionStorage.clear(); 5 localStorage.token = response.data.token; 6 localStorage.id = response.data.id; 7 localStorage.username = response.data.username; 8 } else { 9 // 未记住登录 10 localStorage.clear(); 11 sessionStorage.token = response.data.token; 12 sessionStorage.id = response.data.id; 13 sessionStorage.username = response.data.username; 14 }
2.3.9 多条件登录
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方法的思路:
-
根据username参数查找用户User对象,username参数可能是用户名,也可能是手机号
-
若查找到User对象,调用User对象的 check_password 方法检查密码是否正确
在users/utils.py中编写:
1 from django.contrib.auth.backends import ModelBackend 2 from .models import User 3 from django.db.models import Q 4 import re 5 6 def get_user_by_account(account): 7 """根据账号信息获取用户模型""" 8 try: 9 # if re.match('^1[3-9]\d{9}$', account): 10 # # 手机号 11 # user = User.objects.get(mobile=account) 12 # else: 13 # # 用户名 14 # user = User.objects.get(username=account) 15 16 user = User.objects.get(Q(mobile=account) | Q(username=account)) 17 18 except User.DoesNotExist: 19 user = None 20 21 return user 22 23 24 class UsernameMobileAuthBackend(ModelBackend): 25 def authenticate(self, request, username=None, password=None, **kwargs): 26 # 进行登录判断 27 user = get_user_by_account(username) 28 29 # 账号通过了还要进行密码的验证,以及判断当前站好是否是激活状态 30 if isinstance(user,User) and user.check_password(password) and self.user_can_authenticate(user): 31 return user
在配置文件settings.py中告知Django使用我们自定义的认证后端
AUTHENTICATION_BACKENDS = [ 'users.utils.UsernameMobileAuthBackend', ]
前端首页实现登陆状态的判断
Home组件代码:
1 <template> 2 <div class="home"> 3 <Header :is_login="is_login" @logout="logout" :current_page="current_page"/> 4 <Banner/> 5 <Footer/> 6 </div> 7 </template> 8 9 <script> 10 import Header from "./common/Header" 11 import Banner from "./common/Banner" 12 import Footer from "./common/Footer" 13 export default{ 14 name:"Home", 15 data(){ 16 return { 17 id: sessionStorage.id || localStorage.id, 18 username: sessionStorage.username || localStorage.username, 19 token: sessionStorage.token || localStorage.token, 20 current_page:0, 21 is_login:false, 22 }; 23 }, 24 components:{ 25 Header, 26 Banner, 27 Footer, 28 }, 29 mounted(){ 30 if(this.id && this.token){ 31 this.is_login=true; 32 } 33 }, 34 methods:{ 35 logout(){ 36 this.is_login=false; 37 } 38 } 39 } 40 </script>
头部组件中实现退出登录
实现的思路:头部子组件是通过token值进行判断登录状态,所以当用户点击"退出登录",则需要移出token的值,并使用elementUI里面的弹窗组件进行提示。
Header.vue组件代码:
1 <template> 2 <div class="header"> 3 <el-container> 4 <el-header> 5 <el-row> 6 <el-col class="logo" :span="3"> 7 <a href="/"> 8 <img src="@/assets/head-logo.svg" alt=""> 9 </a> 10 </el-col> 11 <el-col class="nav" :span="16"> 12 <el-row> 13 <el-col v-for="nav in nav_list" :span="3"><a :class="check(nav.link)?'current':''" :href="nav.link">{{nav.name}}</a></el-col> 14 </el-row> 15 </el-col> 16 <el-col class="login-bar" :span="5"> 17 <el-row v-if="token"> 18 <el-col class="cart-ico" :span="9"> 19 <router-link to=""> 20 <b class="goods-number">0</b> 21 <img class="cart-icon" src="@/assets/cart.svg" alt=""> 22 <span><router-link to="/cart">购物车</router-link></span> 23 </router-link> 24 </el-col> 25 <el-col class="study" :span="8" :offset="2"><router-link to="">学习中心</router-link></el-col> 26 <el-col class="member" :span="5"> 27 <el-menu class="el-menu-demo" mode="horizontal"> 28 <el-submenu index="2"> 29 <template slot="title"><router-link to=""><img src="@/assets/logo@2x.png" alt=""></router-link></template> 30 <el-menu-item index="2-1">我的账户</el-menu-item> 31 <el-menu-item index="2-2">我的订单</el-menu-item> 32 <el-menu-item index="2-3">我的优惠卷</el-menu-item> 33 <el-menu-item index="2-3"><span @click="logout">退出登录</span></el-menu-item> 34 </el-submenu> 35 </el-menu> 36 </el-col> 37 </el-row> 38 <el-row v-else> 39 <el-col class="cart-ico" :span="9"> 40 <router-link to=""> 41 <img class="cart-icon" src="@/assets/cart.svg" alt=""> 42 <span><router-link to="/cart">购物车</router-link></span> 43 </router-link> 44 </el-col> 45 <el-col :span="10" :offset="5"> 46 <span class="register"> 47 <router-link to="/login">登录</router-link> 48 | 49 <router-link to="/register">注册</router-link> 50 </span> 51 </el-col> 52 </el-row> 53 </el-col> 54 </el-row> 55 </el-header> 56 </el-container> 57 </div> 58 </template> 59 60 <script> 61 export default { 62 name: "Header", 63 data(){ 64 return { 65 // 设置一个登录标识,表示是否登录 66 token: sessionStorage.token || localStorage.token, 67 user_name: sessionStorage.user_name || localStorage.user_name, 68 user_id: sessionStorage.user_id || localStorage.user_id, 69 nav_list:[], 70 }; 71 }, 72 created() { 73 // 获取导航 74 this.$axios.get(this.$settings.Host+"/nav/").then(response=>{ 75 this.nav_list = response.data 76 console.log(this.nav_list) 77 }).catch(error=>{ 78 console.log(error.response) 79 }) 80 }, 81 methods:{ 82 check(link){ 83 return link==window.location.pathname 84 }, 85 logout(){ 86 87 this.token = false; 88 this.user_id=false; 89 this.user_name=false; 90 91 sessionStorage.removeItem("token"); 92 sessionStorage.removeItem("user_id"); 93 sessionStorage.removeItem("user_name"); 94 95 localStorage.removeItem("token"); 96 localStorage.removeItem("user_id"); 97 localStorage.removeItem("user_name"); 98 99 this.$alert('退出登录成功!', '路飞学城', { 100 confirmButtonText: '确定' 101 }); 102 } 103 } 104 } 105 </script>
在登录认证中接入极验验证
官网: https://www.geetest.com/first_page/
注册登录以后,即进入登录后台,选择行为验证。
接下来,就可以根据官方文档,把验证码集成到项目中了
''文档地址:https://docs.geetest.com/install/overview/start/
下载和安装验证码模块包。
git clone https://github.com/GeeTeam/gt3-python-sdk.git
安装依赖模块
pip install requests
把验证码模块放置到在libs目录中
users/views.py文件下方:
1 from rest_framework.views import APIView 2 from luffy.libs.geetest import GeetestLib 3 from django.conf import settings 4 import random 5 from rest_framework.response import Response 6 7 class CaptchaAPIView(APIView): 8 """极验验证码""" 9 def get(self,request): 10 """提供生成验证码的配置信息""" 11 user_id = '%06d' % random.randint(1,9999) 12 gt = GeetestLib(settings.PC_GEETEST_ID, settings.PC_GEETEST_KEY) 13 status = gt.pre_process(user_id) 14 print(status) 15 16 # 把这两段数据不要保存在session里面, 保存到redis里面 17 request.session[gt.GT_STATUS_SESSION_KEY] = status 18 request.session["user_id"] = user_id 19 20 response_str = gt.get_response_str() 21 return Response(response_str) 22 23 def post(self,request): 24 """进行二次验证""" 25 pass
users/urls.py路由注册:
path(r'captcha/', views.CaptchaAPIView.as_view() ),
配置文件settings/dev.py代码:
PC_GEETEST_ID = '5f4ab1914455506edffaffd4da37fea5'
PC_GEETEST_KEY ='460e13a49d687e5e44e25c383f0473a6'
前端获取显示并校验验证码
把下载会哦图的验证码模块包中的gt.js放置到前端项目中,并在main.js中引入
// 导入gt极验
import '../static/globals/gt.js'
显示验证码
1 <template> 2 <div class="login-box"> 3 <img src="../../static/img/Loginbg.3377d0c.jpg" alt=""> 4 <div class="login"> 5 <div class="login-title"> 6 <img src="../../static/img/Logotitle.1ba5466.png" alt=""> 7 <p>帮助有志向的年轻人通过努力学习获得体面的工作和生活!</p> 8 </div> 9 <div class="login_box"> 10 <div class="title"> 11 <span @click="login_type=0">密码登录</span> 12 <span @click="login_type=1">短信登录</span> 13 </div> 14 <div class="inp" v-if="login_type==0"> 15 <input v-model = "username" type="text" placeholder="用户名 / 手机号码" class="user"> 16 <input v-model = "password" type="password" name="" class="pwd" placeholder="密码"> 17 <div id="geetest1"></div> 18 <div class="rember"> 19 <p> 20 <input type="checkbox" class="no" v-model="remember"/> 21 <span>记住密码</span> 22 </p> 23 <p>忘记密码</p> 24 </div> 25 <button class="login_btn" @click="loginhander">登录</button> 26 <p class="go_login" >没有账号 <router-link to="/reg">立即注册</router-link></p> 27 </div> 28 <div class="inp" v-show="login_type==1"> 29 <input v-model = "username" type="text" placeholder="手机号码" class="user"> 30 <input v-model = "password" type="text" class="pwd" placeholder="短信验证码"> 31 <button id="get_code">获取验证码</button> 32 <button class="login_btn">登录</button> 33 <p class="go_login" >没有账号 <router-link to="/reg">立即注册</router-link></p> 34 </div> 35 </div> 36 </div> 37 </div> 38 </template> 39 40 <script> 41 export default { 42 name: 'Login', 43 data(){ 44 return { 45 login_type: 0, 46 username:"", 47 password:"", 48 remember:"", 49 } 50 }, 51 mounted(){ 52 // 请求后端获取生成验证码的流水号 53 this.$axios.get(this.$settings.Host + "/users/captcha/",{ 54 responseType: 'json', // 希望返回json数据 55 }).then(response => { 56 let data = response.data; 57 58 // 验证初始化配置 59 initGeetest({ 60 gt: data.gt, 61 challenge: data.challenge, 62 product: "popup", // 产品形式,包括:float,embed,popup。注意只对PC版验证码有效 63 offline: !data.success 64 },this.handlerPopup) 65 }).catch(error => { 66 console.log(error.response); 67 }); 68 }, 69 methods:{ 70 // 用户登录 71 loginhander(){ 72 。。。。 73 }, 74 // 验证码的成功验证事件方法 75 handlerPopup(captchaObj){ 76 // 把验证码添加到模板中制定的页面 77 captchaObj.appendTo("#geetest1"); 78 79 }, 80 }, 81 82 }; 83 </script>
效果:
后端提供二次验证的API接口
1 from django.shortcuts import render 2 3 # Create your views here. 4 from .serializers import UserModelSerializer 5 from rest_framework.generics import CreateAPIView 6 from .models import User 7 class UserAPIView(CreateAPIView): 8 serializer_class = UserModelSerializer 9 queryset = User.objects.all() 10 11 12 from rest_framework.views import APIView 13 from luffy.libs.geetest import GeetestLib 14 from django.conf import settings 15 import random 16 from rest_framework.response import Response 17 18 class CaptchaAPIView(APIView): 19 """极验验证码""" 20 gt = GeetestLib(settings.PC_GEETEST_ID, settings.PC_GEETEST_KEY) 21 def get(self,request): 22 """提供生成验证码的配置信息""" 23 user_id = '%06d' % random.randint(1,9999) 24 status = self.gt.pre_process(user_id) 25 print(status) 26 27 # 把这两段数据不要保存在session里面, 保存到redis里面 28 request.session[self.gt.GT_STATUS_SESSION_KEY] = status 29 request.session["user_id"] = user_id 30 31 response_str = self.gt.get_response_str() 32 return Response(response_str) 33 34 def post(self,request): 35 """进行二次验证""" 36 challenge = request.data.get(self.gt.FN_CHALLENGE, '') 37 validate = request.data.get(self.gt.FN_VALIDATE, '') 38 seccode = request.data.get(self.gt.FN_SECCODE, '') 39 40 status = request.session.get(self.gt.GT_STATUS_SESSION_KEY) 41 user_id = request.session.get("user_id") 42 43 if status: 44 result = self.gt.success_validate(challenge, validate, seccode, user_id) 45 else: 46 result = self.gt.failback_validate(challenge, validate, seccode) 47 48 # 返回一个随机字符串,在用户登录提供数据时一并发送到后端,进行验证 49 # 后面可以使用redis保存 50 51 52 return Response({"message":result})
Login.vue代码
1 <template> 2 <div class="login-box"> 3 <img src="../../static/img/Loginbg.3377d0c.jpg" alt=""> 4 <div class="login"> 5 <div class="login-title"> 6 <img src="../../static/img/Logotitle.1ba5466.png" alt=""> 7 <p>帮助有志向的年轻人通过努力学习获得体面的工作和生活!</p> 8 </div> 9 <div class="login_box"> 10 <div class="title"> 11 <span @click="login_type=0">密码登录</span> 12 <span @click="login_type=1">短信登录</span> 13 </div> 14 <div class="inp" v-if="login_type==0"> 15 <input v-model = "username" type="text" placeholder="用户名 / 手机号码" class="user"> 16 <input v-model = "password" type="password" name="" class="pwd" placeholder="密码"> 17 <div id="geetest1"></div> 18 <div class="rember"> 19 <p> 20 <input type="checkbox" class="no" v-model="remember"/> 21 <span>记住密码</span> 22 </p> 23 <p>忘记密码</p> 24 </div> 25 <button class="login_btn" @click="loginhander">登录</button> 26 <p class="go_login" >没有账号 <router-link to="/reg">立即注册</router-link></p> 27 </div> 28 <div class="inp" v-show="login_type==1"> 29 <input v-model = "username" type="text" placeholder="手机号码" class="user"> 30 <input v-model = "password" type="text" class="pwd" placeholder="短信验证码"> 31 <button id="get_code">获取验证码</button> 32 <button class="login_btn">登录</button> 33 <p class="go_login" >没有账号 <router-link to="/reg">立即注册</router-link></p> 34 </div> 35 </div> 36 </div> 37 </div> 38 </template> 39 40 <script> 41 export default { 42 name: 'Login', 43 data(){ 44 return { 45 login_type: 0, 46 username:"", 47 password:"", 48 remember:"", 49 is_geek:false, 50 } 51 }, 52 mounted(){ 53 // 请求后端获取生成验证码的流水号 54 this.$axios.get(this.$settings.Host + "/users/captcha/",{ 55 responseType: 'json', // 希望返回json数据 56 }).then(response => { 57 let data = response.data; 58 59 // 验证初始化配置 60 initGeetest({ 61 gt: data.gt, 62 challenge: data.challenge, 63 product: "popup", // 产品形式,包括:float,embed,popup。注意只对PC版验证码有效 64 offline: !data.success 65 },this.handlerPopup) 66 }).catch(error => { 67 console.log(error.response); 68 }); 69 }, 70 methods:{ 71 // 用户登录 72 loginhander(){ 73 // 判断用户是否已经通过了极验验证 74 if(!this.is_geek){ 75 return false; 76 } 77 78 this.$axios.post(this.$settings.Host+"/users/login/",{ 79 username:this.username, 80 password:this.password, 81 }).then(response=>{ 82 let data = response.data 83 // 根据用户是否勾选了记住密码来保存用户认证信息 84 if(this.remember){ 85 // 记住密码 86 localStorage.token = data.token; 87 localStorage.user_id = data.id; 88 localStorage.user_name = data.username; 89 90 }else{ 91 // 不需要记住密码 92 sessionStorage.token = data.token; 93 sessionStorage.user_id = data.id; 94 sessionStorage.user_name = data.username; 95 } 96 97 // 登录成功以后,跳转会上一个页面 98 this.$router.go(-1); 99 100 }).catch(error=>{ 101 console.log(error.response) 102 }) 103 }, 104 // 验证码的成功验证事件方法 105 handlerPopup(captchaObj){ 106 // 把验证码添加到模板中制定的页面 107 captchaObj.appendTo("#geetest1"); 108 109 // 记录vue对象 110 let _this = this; 111 112 // 监听用户对于验证码的操作是否成功了 113 captchaObj.onSuccess(()=>{ 114 var validate = captchaObj.getValidate(); 115 116 _this.$axios.post(_this.$settings.Host+"/users/captcha/",{ 117 geetest_challenge: validate.geetest_challenge, 118 geetest_validate: validate.geetest_validate, 119 geetest_seccode: validate.geetest_seccode 120 }).then(response=>{ 121 // 在用户成功添加数据以后,可以允许点击登录按钮 122 _this.is_geek = true; 123 124 }).catch(error=>{ 125 console.log(error.response) 126 }) 127 128 }); 129 130 }, 131 }, 132 133 }; 134 </script> 135 <style scoped> 136 .login-box{ 137 width: 100%; 138 height: 100%; 139 position: relative; 140 overflow: hidden; 141 margin-top: -80px; 142 } 143 .login-box img{ 144 width: 100%; 145 min-height: 100%; 146 } 147 .login-box .login { 148 position: absolute; 149 width: 500px; 150 height: 400px; 151 left: 0; 152 margin: auto; 153 right: 0; 154 bottom: 0; 155 top: -220px; 156 } 157 .login .login-title{ 158 width: 100%; 159 text-align: center; 160 } 161 .login-title img{ 162 width: 190px; 163 height: auto; 164 } 165 .login-title p{ 166 font-size: 18px; 167 color: #fff; 168 letter-spacing: .29px; 169 padding-top: 10px; 170 padding-bottom: 50px; 171 } 172 .login_box{ 173 width: 400px; 174 height: auto; 175 background: #fff; 176 box-shadow: 0 2px 4px 0 rgba(0,0,0,.5); 177 border-radius: 4px; 178 margin: 0 auto; 179 padding-bottom: 40px; 180 } 181 .login_box .title{ 182 font-size: 20px; 183 color: #9b9b9b; 184 letter-spacing: .32px; 185 border-bottom: 1px solid #e6e6e6; 186 display: flex; 187 justify-content: space-around; 188 padding: 50px 60px 0 60px; 189 margin-bottom: 20px; 190 cursor: pointer; 191 } 192 .login_box .title span:nth-of-type(1){ 193 color: #4a4a4a; 194 border-bottom: 2px solid #84cc39; 195 } 196 197 .inp{ 198 width: 350px; 199 margin: 0 auto; 200 } 201 .inp input{ 202 outline: 0; 203 width: 100%; 204 height: 45px; 205 border-radius: 4px; 206 border: 1px solid #d9d9d9; 207 text-indent: 20px; 208 font-size: 14px; 209 background: #fff !important; 210 } 211 .inp input.user{ 212 margin-bottom: 16px; 213 } 214 .inp .rember{ 215 display: flex; 216 justify-content: space-between; 217 align-items: center; 218 position: relative; 219 margin-top: 10px; 220 } 221 .inp .rember p:first-of-type{ 222 font-size: 12px; 223 color: #4a4a4a; 224 letter-spacing: .19px; 225 margin-left: 22px; 226 display: -ms-flexbox; 227 display: flex; 228 -ms-flex-align: center; 229 align-items: center; 230 /*position: relative;*/ 231 } 232 .inp .rember p:nth-of-type(2){ 233 font-size: 14px; 234 color: #9b9b9b; 235 letter-spacing: .19px; 236 cursor: pointer; 237 } 238 239 .inp .rember input{ 240 outline: 0; 241 width: 30px; 242 height: 45px; 243 border-radius: 4px; 244 border: 1px solid #d9d9d9; 245 text-indent: 20px; 246 font-size: 14px; 247 background: #fff !important; 248 } 249 250 .inp .rember p span{ 251 display: inline-block; 252 font-size: 12px; 253 width: 100px; 254 /*position: absolute;*/ 255 /*left: 20px;*/ 256 257 } 258 #geetest{ 259 margin-top: 20px; 260 } 261 .login_btn{ 262 width: 100%; 263 height: 45px; 264 background: #84cc39; 265 border-radius: 5px; 266 font-size: 16px; 267 color: #fff; 268 letter-spacing: .26px; 269 margin-top: 30px; 270 } 271 .inp .go_login{ 272 text-align: center; 273 font-size: 14px; 274 color: #9b9b9b; 275 letter-spacing: .26px; 276 padding-top: 20px; 277 } 278 .inp .go_login span{ 279 color: #84cc39; 280 cursor: pointer; 281 } 282 </style>
修改验证码框的样式位置。
static/css/reset.css,代码:
.geetest_holder{
padding-top: 15px;
width: 100%!important;
}
用户的注册认证
前端显示注册页面并调整首页头部和登陆页面的注册按钮的链接。
注册页面Register,主要是通过登录页面进行改成而成./
1 <template> 2 <div class="box"> 3 <img src="https://www.luffycity.com/static/img/Loginbg.3377d0c.jpg" alt=""> 4 <div class="register"> 5 <div class="register_box"> 6 <div class="register-title">注册路飞学城</div> 7 <div class="inp"> 8 <input v-model = "mobile" type="text" placeholder="手机号码" class="user"> 9 <div id="geetest"></div> 10 <input v-model = "sms" type="text" placeholder="输入验证码" class="user"> 11 <button class="register_btn" >注册</button> 12 <p class="go_login" >已有账号 <router-link to="/login">直接登录</router-link></p> 13 </div> 14 </div> 15 </div> 16 </div> 17 </template> 18 19 <script> 20 export default { 21 name: 'Register', 22 data(){ 23 return { 24 sms:"", 25 mobile:"", 26 validateResult:false, 27 } 28 }, 29 created(){ 30 }, 31 methods:{}, 32 33 }; 34 </script> 35 36 <style scoped> 37 .box{ 38 width: 100%; 39 height: 100%; 40 position: relative; 41 overflow: hidden; 42 } 43 .box img{ 44 width: 100%; 45 min-height: 100%; 46 } 47 .box .register { 48 position: absolute; 49 width: 500px; 50 height: 400px; 51 top: 0; 52 left: 0; 53 margin: auto; 54 right: 0; 55 bottom: 0; 56 top: -338px; 57 } 58 .register .register-title{ 59 width: 100%; 60 font-size: 24px; 61 text-align: center; 62 padding-top: 30px; 63 padding-bottom: 30px; 64 color: #4a4a4a; 65 letter-spacing: .39px; 66 } 67 .register-title img{ 68 width: 190px; 69 height: auto; 70 } 71 .register-title p{ 72 font-family: PingFangSC-Regular; 73 font-size: 18px; 74 color: #fff; 75 letter-spacing: .29px; 76 padding-top: 10px; 77 padding-bottom: 50px; 78 } 79 .register_box{ 80 width: 400px; 81 height: auto; 82 background: #fff; 83 box-shadow: 0 2px 4px 0 rgba(0,0,0,.5); 84 border-radius: 4px; 85 margin: 0 auto; 86 padding-bottom: 40px; 87 } 88 .register_box .title{ 89 font-size: 20px; 90 color: #9b9b9b; 91 letter-spacing: .32px; 92 border-bottom: 1px solid #e6e6e6; 93 display: flex; 94 justify-content: space-around; 95 padding: 50px 60px 0 60px; 96 margin-bottom: 20px; 97 cursor: pointer; 98 } 99 .register_box .title span:nth-of-type(1){ 100 color: #4a4a4a; 101 border-bottom: 2px solid #84cc39; 102 } 103 104 .inp{ 105 width: 350px; 106 margin: 0 auto; 107 } 108 .inp input{ 109 border: 0; 110 outline: 0; 111 width: 100%; 112 height: 45px; 113 border-radius: 4px; 114 border: 1px solid #d9d9d9; 115 text-indent: 20px; 116 font-size: 14px; 117 background: #fff !important; 118 } 119 .inp input.user{ 120 margin-bottom: 16px; 121 } 122 .inp .rember{ 123 display: flex; 124 justify-content: space-between; 125 align-items: center; 126 position: relative; 127 margin-top: 10px; 128 } 129 .inp .rember p:first-of-type{ 130 font-size: 12px; 131 color: #4a4a4a; 132 letter-spacing: .19px; 133 margin-left: 22px; 134 display: -ms-flexbox; 135 display: flex; 136 -ms-flex-align: center; 137 align-items: center; 138 /*position: relative;*/ 139 } 140 .inp .rember p:nth-of-type(2){ 141 font-size: 14px; 142 color: #9b9b9b; 143 letter-spacing: .19px; 144 cursor: pointer; 145 } 146 147 .inp .rember input{ 148 outline: 0; 149 width: 30px; 150 height: 45px; 151 border-radius: 4px; 152 border: 1px solid #d9d9d9; 153 text-indent: 20px; 154 font-size: 14px; 155 background: #fff !important; 156 } 157 158 .inp .rember p span{ 159 display: inline-block; 160 font-size: 12px; 161 width: 100px; 162 /*position: absolute;*/ 163 /*left: 20px;*/ 164 165 } 166 #geetest{ 167 margin-top: 20px; 168 } 169 .register_btn{ 170 width: 100%; 171 height: 45px; 172 background: #84cc39; 173 border-radius: 5px; 174 font-size: 16px; 175 color: #fff; 176 letter-spacing: .26px; 177 margin-top: 30px; 178 } 179 .inp .go_login{ 180 text-align: center; 181 font-size: 14px; 182 color: #9b9b9b; 183 letter-spacing: .26px; 184 padding-top: 20px; 185 } 186 .inp .go_login span{ 187 color: #84cc39; 188 cursor: pointer; 189 } 190 </style>