在用户注册或登录后,我们想记录用户的登录状态,或者为用户创建身份认证的凭证。我们不再使用Session认证机制,而使用Json Web Token认证机制。
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
JWT就是一段字符串,由三段信息构成的,将这三段信息文本用.
链接一起就构成了Jwt字符串。就像这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature).
jwt的头部承载两部分信息:
完整的头部就像下面这样的JSON:
{
'typ': 'JWT',
'alg': 'HS256'
}
然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分.
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分
标准中注册的声明 (建议但不强制使用) :
公共的声明 : 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
私有的声明 : 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
定义一个payload:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
然后将其进行base64
加密,得到JWT的第二部分。
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
JWT的第三部分是一个签证信息,这个签证信息由三部分组成:
这个部分需要base64加密后的header和base64加密后的payload使用.
连接组成的字符串,然后通过header中声明的加密方式进行加盐secret
组合加密,然后就构成了jwt的第三部分。
# python 使用base64模块的算法,对数据进行编码转换[可以理解是加密]
import base64
data_dict = {
"name":"xiaoming"
}
data_str = str(data_dict)
data1 = base64.b64encode(data_str)
print(data1) # 'eyduYW1lJzogJ3hpYW9taW5nJ30='
data2 = base64.b64decode(data1)
print(data2) # "{'name': 'xiaoming'}"
// javascript中也可以使用base64算法的,甚至还可以手动伪造jwt的token
// 因此这就是jwt的token为什么要有签名的原因!!!
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://jpadilla.github.io/django-rest-framework-jwt/
安装
pip install djangorestframework-jwt
配置settings/dev.py
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
),
}
import datetime
JWT_AUTH = {
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1),
}
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的视图,可以直接使用
在子应用路由urls.py中
from rest_framework_jwt.views import obtain_jwt_token
urlpatterns = [
path(r'authorizations/', obtain_jwt_token, name='authorizations'),
]
在主路由中,引入当前子应用的路由文件
urlpatterns = [
...
path('users/', include("users.urls")),
# include 的值必须是 模块名.urls 格式,字符串中间只能出现一个圆点
]
接下来,我们可以通过postman来测试下功能
在登陆组件中找到登陆按钮,绑定点击事件
<button class="login_btn" @click="loginhander">登录button>
在methods
中请求后端
export default {
name: 'Login',
data(){
return {
login_type: 0,
username:"",
password:"",
}
},
methods:{
// 发送登录请求
loginHeader(){
if(
(this.username.length >=4 && this.username.length < 16) &&
(this.password.length >= 3 && this.password.length <= 18)
) {
// 发送ajax
this.$axios.post(this.$settings.Host+"/users/login/", {
"username": this.username,
"password": this.password,
}).then(response=>{
console.log(response.data)
}).catch(error=>{
console.log(error.response)
})
}
}
},
};
我们可以将JWT保存在cookie中,也可以保存在浏览器的本地存储里,我们保存在浏览器本地存储中
浏览器的本地存储提供了sessionStorage
和 localStorage
两种:
使用方法
sessionStorage.变量名 = 变量值 // 保存数据
sessionStorage.变量名 // 读取数据
sessionStorage.removeItem("变量名") // 删除指定数据
sessionStorage.clear() // 清除所有sessionStorage保存的数据
localStorage.变量名 = 变量值 // 保存数据
localStorage.变量名 // 读取数据
localStorage.removeItem("变量名") // 删除指定数据
localStorage.clear() // 清除所有localStorage保存的数据
保存了登录状态信息以后,我们可以使用弹出进行提示,并使用路由功能进行页面的跳转
# elementUI本身就提供了页面弹窗和提示功能。
this.$confirm() # 弹出确认框
this.$alert() # 弹出警告框
this.$prompt() # 弹出对话框
this.$message() # 弹出提示框
登陆组件代码Login.vue
<template>
<div class="login box">
<img src="/static/image/Loginbg.3377d0c.jpg" alt="">
<div class="login">
<div class="login-title">
<img src="/static/image/Logotitle.1ba5466.png" alt="">
<p>帮助有志向的年轻人通过努力学习获得体面的工作和生活!p>
div>
<div class="login_box">
<div class="title">
<span @click="login_type=0" :class="login_type==0?'current':''">密码登录span>
<span @click="login_type=1" :class="login_type==1?'current':''">短信登录span>
div>
<div class="inp" v-if="login_type==0">
<input v-model="username" type="text" placeholder="用户名 / 手机号码" class="user">
<input v-model="password" type="password" name="" class="pwd" placeholder="密码">
<div id="geetest1">div>
<div class="rember">
<p>
<input type="checkbox" class="no" v-model="remember"/>
<span>记住密码span>
p>
<p>忘记密码p>
div>
<button class="login_btn" @click="loginHander">登录button>
<p class="go_login" >没有账号 <span>立即注册span>p>
div>
<div class="inp" v-show="login_type==1">
<button id="get_code">获取验证码button>
<button class="login_btn">登录button>
<p class="go_login" >没有账号 <span>立即注册span>p>
div>
div>
div>
div>
template>
<script>
export default {
name: 'Login',
data(){
return {
login_type: 0,
username:"",
password:"",
remember:false,
}
},
methods:{
loginHander(){
// 登录功能实现
// 1. 从vue中提取username和password, 验证数据
if(this.username.length<1 || this.password.length<1){
this.$message("对不起,请填写账号或密码!");
return false;// 阻止函数继续执行
}
// 3. 发送ajax提交登录信息
this.$axios.post(`${this.$settings.Host}/user/authorizations/`,{
username:this.username,
password:this.password,
}).then(response=>{
// 4. 接受jwt的token字符串
// console.log(response.data);
// 4.1 根据用户是否勾选了 记住密码 来判断使用不用的存储对象保存数据
if(this.remember){
// 永久存储
// localStorage.setItem("user_token",response.data.token);
localStorage.user_token = response.data.token; // 上面一句和当前一句是同样意思
sessionStorage.removeItem("user_token");
}else{
// 回话存储
sessionStorage.user_token = response.data.token;
localStorage.removeItem("user_token");
}
// 5.跳转到其他页面[首页]
let self = this;
this.$alert("欢迎回到路飞学城!~~","登录成功!",{
callback(){
self.$router.push("/"); // 跳转到指定地址的页面
}
});
// this.$router.push("/"); // 跳转到指定地址的页面
// // this.$router.go(-1); // 跳转返回上一页
}).catch(error=>{
// 获取后端相应的错误信息
this.$alert(error.response.data.non_field_errors[0],"警告!");
})
}
},
};
script>
默认的返回值仅有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
配置文件
import datetime
# 设置jwt的格式
JWT_AUTH = {
# 设置jwt的过期时间
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1),
# 设置jwt的返回内容
'JWT_RESPONSE_PAYLOAD_HANDLER': 'users.utils.jwt_response_payload_handler'
}
登陆组件代码Login.vue
if(this.remember){
// 永久存储
// localStorage.setItem("user_token",response.data.token);
localStorage.user_token = response.data.token; // 上面一句和当前一句是同样意思
localStorage.user_id = response.data.user_id;
localStorage.user_name = response.data.user_name;
sessionStorage.removeItem("user_token");
sessionStorage.removeItem("user_id");
sessionStorage.removeItem("user_name");
}else{
// 回话存储
sessionStorage.user_token = response.data.token; // 上面一句和当前一句是同样意思
sessionStorage.user_id = response.data.user_id;
sessionStorage.user_name = response.data.user_name;
localStorage.removeItem("user_token");
localStorage.removeItem("user_id");
localStorage.removeItem("user_name");
}
JWT扩展的登录视图,在收到用户名与密码时,也是调用Django的认证系统中提供的authenticate()
来检查用户名与密码是否正确。
我们可以通过修改Django认证系统的认证后端(主要是authenticate方法)来支持登录账号既可以是用户名也可以是手机号。
修改Django认证系统的认证后端需要继承django.contrib.auth.backends.ModelBackend
,并重写authenticate
方法。
authenticate(self, request, username=None, password=None, **kwargs)
方法的参数说明:
我们想要让用户既可以以用户名登录,也可以以手机号登录,那么对于authenticate方法而言,username参数即表示用户名或者手机号。
重写authenticate方法的思路:
在users/utils.py
中编写:
def jwt_response_payload_handler(token, user=None, request=None):
# 自定义登录以后的返回数据
return {
"token": token,
"user_id": user.id,
"user_name": user.username
}
from django.contrib.auth.backends import ModelBackend
from .models import User
from django.db.models import Q
class UsernameMobileAuthBackend(ModelBackend):
def authenticate(self, request, username=None, password=None):
try:
user = User.objects.filter( Q(username=username) | Q(mobile=username) | Q(email=username) ).first()
except User.DoesNotExist:
return None
if user and user.check_password(password) and self.user_can_authenticate(user):
return user
在配置文件settings/dev.py
中告知Django使用我们自定义的认证后端
AUTHENTICATION_BACKENDS = [
'users.utils.UsernameMobileAuthBackend',
]
Header子组件根据登陆状态显示不同登录信息。
Header.vue
<template>
<div class="header-box">
<div class="header">
<div class="content">
<div class="logo full-left">
<router-link to="/"><img src="/static/image/logo.svg" alt="">router-link>
div>
<ul class="nav full-left">
<li :key="key" v-for="nav,key in nav_list">
<router-link v-if="nav.link.search('://') == -1" :to="nav.link">{{nav.name}}router-link>
<a v-else :href="nav.link">{{nav.name}}a>
li>
ul>
<div v-if="token" class="login-bar full-right">
<div class="shop-cart full-left">
<span class="shop-cart-total">span>
<img src="/static/image/cart.svg" alt="">
<span><router-link to="/cart">购物车router-link>span>
div>
<div class="login-box login-box1 full-left">
<router-link to="">学习中心router-link>
<el-menu width="200" class="member el-menu-demo" mode="horizontal">
<el-submenu index="2">
<template slot="title"><router-link to=""><img src="/static/image/[email protected]" alt="">router-link>template>
<el-menu-item index="2-1">我的账户el-menu-item>
<el-menu-item index="2-2"><router-link to="/user/order">我的订单router-link>el-menu-item>
<el-menu-item index="2-3">我的优惠卷el-menu-item>
<el-menu-item index="2-3"><span>退出登录span>el-menu-item>
el-submenu>
el-menu>
div>
div>
<div v-if="!token" class="login-bar full-right">
<div class="shop-cart full-left">
<img src="/static/image/cart.svg" alt="">
<span><router-link to="/cart">购物车router-link>span>
div>
<div class="login-box login-box2 full-left">
<router-link to="/user/login">登录router-link>
|
<span>注册span>
div>
div>
div>
div>
div>
template>
<script>
export default {
name: "Header",
data(){
return{
token:"",
nav_list:[]
}
},
created(){
this.checkUserLogin();
this.get_header_nav();
},
methods:{
get_header_nav(){
this.$axios.get(`${this.$settings.Host}/nav/header/`).then(response=>{
console.log(response.data);
this.nav_list = response.data;
})
},
checkUserLogin(){
this.token = localStorage.user_token || sessionStorage.user_token;
}
}
}
script>
<style scoped>
.header-box{
height: 80px;
}
.header{
width: 100%;
height: 80px;
box-shadow: 0 0.5px 0.5px 0 #c9c9c9;
position: fixed;
top:0;
left: 0;
right:0;
margin: auto;
z-index: 99;
background: #fff;
}
.header .content{
max-width: 1200px;
width: 100%;
margin: 0 auto;
}
.header .content .logo{
height: 80px;
line-height: 80px;
margin-right: 50px;
cursor: pointer; /* 设置光标的形状为爪子 */
}
.header .content .logo img{
vertical-align: middle;
}
.header .nav li{
float: left;
height: 80px;
line-height: 80px;
margin-right: 30px;
font-size: 16px;
color: #4a4a4a;
cursor: pointer;
}
.header .nav li span{
padding-bottom: 16px;
padding-left: 5px;
padding-right: 5px;
}
.header .nav li span a{
display: inline-block;
}
.header .nav li .this{
color: #4a4a4a;
border-bottom: 4px solid #ffc210;
}
.header .nav li:hover span{
color: #000;
}
.header .login-bar{
height: 80px;
}
.header .login-bar .shop-cart{
margin-right: 20px;
border-radius: 17px;
background: #f7f7f7;
cursor: pointer;
font-size: 14px;
height: 28px;
width: 88px;
margin-top: 30px;
line-height: 32px;
text-align: center;
position: relative;
}
.header .login-bar .shop-cart:hover{
background: #f0f0f0;
}
.header .login-bar .shop-cart img{
width: 15px;
margin-right: 4px;
margin-left: 6px;
}
.header .login-bar .shop-cart span{
margin-right: 6px;
}
.header .login-bar .shop-cart-total{
width: 16px;
height: 16px;
line-height: 17px;
font-size: 12px;
color: #fff;
text-align: center;
background: #fa6240;
border-radius: 50%;
transform: scale(.8);
position: absolute;
left: 16px;
top: -1px;
}
.header .login-bar .login-box1{
margin-top: 16px;
}
.header .login-bar .login-box2{
margin-top: 34px;
}
.header .login-bar .login-box span{
color: #4a4a4a;
cursor: pointer;
}
.header .login-bar .login-box span:hover{
color: #000000;
}
.member{
display: inline-block;
height: 34px;
margin-left: 20px;
}
.member img{
width: 26px;
height: 26px;
border-radius: 50%;
display: inline-block;
}
.member img:hover{
border: 1px solid yellow;
}
style>
因为elementUI提供的下拉菜单样式需要调整,所以我们在app.vue
中,修改elementUI样式。
官网: https://www.geetest.com/first_page/
注册登录以后,即进入登录后台,选择行为验证。
ID: 6e15c7e*********************57790
KEY: db327f*********************ff057
接下来,就可以根据官方文档,把验证码集成到项目中了
文档地址:https://docs.geetest.com/install/overview/start/
下载和安装验证码模块包,不是要我们安装这个第三方模块,而是使用git复制sdk到电脑中。
git clone https://github.com/GeeTeam/gt3-python-sdk.git
分析完官方的sdk目录以后,我们就可以把验证码功能集成到项目中了,首先我们在项目安装验证码的python依赖模块
pip install requests
把验证码模块放置在libs
目录中
并在users
子应用下创建验证码视图类,并提供验证码和校验验证码的视图方法。
views.py
from rest_framework.views import APIView
from luffyapi.libs.geetest import GeetestLib
from rest_framework.response import Response
from django.conf import settings
class CaptchaAPIView(APIView):
def get(self,request):
"""生成验证码的流水号和状态"""
gt = GeetestLib(settings.PC_GEETEST_ID, settings.PC_GEETEST_KEY)
status = gt.pre_process(settings.PC_GEETEST_USER_ID)
response_str = gt.get_response_str()
return Response(response_str)
def post(self,request):
"""二次验证"""
gt = GeetestLib(settings.PC_GEETEST_ID, settings.PC_GEETEST_KEY)
challenge = request.data.get(gt.FN_CHALLENGE, '')
validate = request.data.get(gt.FN_VALIDATE, '')
seccode = request.data.get(gt.FN_SECCODE, '')
result = gt.success_validate(challenge, validate, seccode, settings.PC_GEETEST_USER_ID)
if not result:
result = gt.failback_validate(challenge, validate, seccode)
return Response({"status": result})
配置settings/dev.py
# 极验验证码配置[ Ctrl+shift+U,把选中内容中的字母转换成大小写 ]
PC_GEETEST_ID = "6e1***************************790"
PC_GEETEST_KEY = "db3***************************057"
PC_GEETEST_USER_ID = 'test'
路由注册:
urls.py
from django.urls import path, re_path
from . import views
from rest_framework_jwt.views import obtain_jwt_token
urlpatterns = [
path(r'authorizations/', obtain_jwt_token, name='authorizations'),
path(r'captcha/', views.CaptchaAPIView.as_view()),
]
把下载回来的验证码模块包中的gt.js
放置到前端项目中,并在main.js
中引入
// 导入极验验证码的js文件
import '../static/js/gt.js'
Login.vue
代码
<template>
<div class="login box">
<img src="/static/image/Loginbg.3377d0c.jpg" alt="">
<div class="login">
<div class="login-title">
<img src="/static/image/Logotitle.1ba5466.png" alt="">
<p>帮助有志向的年轻人通过努力学习获得体面的工作和生活!p>
div>
<div class="login_box">
<div class="title">
<span @click="login_type=0" :class="login_type==0?'current':''">密码登录span>
<span @click="login_type=1" :class="login_type==1?'current':''">短信登录span>
div>
<div class="inp" v-if="login_type==0">
<input v-model="username" type="text" placeholder="用户名 / 手机号码" class="user">
<input v-model="password" type="password" name="" class="pwd" placeholder="密码">
<div id="geetest1">div>
<div class="rember">
<p>
<input type="checkbox" class="no" v-model="remember"/>
<span>记住密码span>
p>
<p>忘记密码p>
div>
<button class="login_btn" @click="loginHander">登录button>
<p class="go_login" >没有账号 <span>立即注册span>p>
div>
<div class="inp" v-show="login_type==1">
<button id="get_code">获取验证码button>
<button class="login_btn">登录button>
<p class="go_login" >没有账号 <span>立即注册span>p>
div>
div>
div>
div>
template>
<script>
export default {
name: 'Login',
data(){
return {
login_type: 0,
username:"",
password:"",
remember:false,
}
},
methods:{
handlerPopup(captchaObj) {
// 把vue对象保存到一个变量,方便其他对象使用
let self = this;
console.log(captchaObj);
// 成功的回调
captchaObj.onSuccess(function () {
var validate = captchaObj.getValidate();
// 使用axios发送ajax
self.$axios.post(`${self.$settings.Host}/user/captcha/`,{
geetest_challenge: validate.geetest_challenge,
geetest_validate: validate.geetest_validate,
geetest_seccode: validate.geetest_seccode
}).then(response=>{
if(response.data.status){
// 验证成功! 把登录信息发送给后端!
self.ajax_login();
}else{
// 验证失败!
self.$message("对不起,验证码验证失败!");
}
});
});
// 将验证码加到id为captcha的元素里
document.querySelector("#geetest1").innerHTML = "";
// document.getElementById("geetest1").innerHTML = ""; // 这一句也可以
captchaObj.appendTo("#geetest1");
},
loginHander(){
// 登录功能实现
// 1. 从vue中提取username和password, 验证数据
if(this.username.length<1 || this.password.length<1){
this.$message("对不起,请填写账号或密码!");
return false;// 阻止函数继续执行
}
// 2. 提供极验验证码
this.$axios.get(`${this.$settings.Host}/user/captcha`,{
responseType:"json",
}).then(response=>{
// 获取到流水号以后,就要对验证码的配置进行初始化
initGeetest({
gt: response.data.gt,
challenge: response.data.challenge,
product: "popup", // 产品形式,包括:float,embed,popup。注意只对PC版验证码有效
offline: !response.data.success // 表示用户后台检测极验服务器是否宕机,一般不需要关注
}, this.handlerPopup);
});
},
ajax_login(){
// 发送ajax提交登录信息
this.$axios.post(`${this.$settings.Host}/user/authorizations/`,{
username:this.username,
password:this.password,
}).then(response=>{
// 4. 接受jwt的token字符串
// console.log(response.data);
// 4.1 根据用户是否勾选了 记住密码 来判断使用不用的存储对象保存数据
if(this.remember){
// 永久存储
// localStorage.setItem("user_token",response.data.token);
localStorage.user_token = response.data.token; // 上面一句和当前一句是同样意思
localStorage.user_id = response.data.user_id;
localStorage.user_name = response.data.user_name;
sessionStorage.removeItem("user_token");
sessionStorage.removeItem("user_id");
sessionStorage.removeItem("user_name");
}else{
// 回话存储
sessionStorage.user_token = response.data.token; // 上面一句和当前一句是同样意思
sessionStorage.user_id = response.data.user_id;
sessionStorage.user_name = response.data.user_name;
localStorage.removeItem("user_token");
localStorage.removeItem("user_id");
localStorage.removeItem("user_name");
}
// 5.跳转到其他页面[首页]
let self = this;
this.$alert("欢迎回到路飞学城!~~","登录成功!",{
callback(){
self.$router.push("/"); // 跳转到指定地址的页面
}
});
// this.$router.push("/"); // 跳转到指定地址的页面
// // this.$router.go(-1); // 跳转返回上一页
}).catch(error=>{
// 获取后端相应的错误信息
this.$alert(error.response.data.non_field_errors[0],"警告!");
})
}
},
};
script>
<style scoped>
.box{
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
}
.box img{
width: 100%;
min-height: 100%;
}
.box .login {
position: absolute;
width: 500px;
height: 400px;
left: 0;
margin: auto;
right: 0;
bottom: 0;
top: -338px;
}
.login .login-title{
width: 100%;
text-align: center;
}
.login-title img{
width: 190px;
height: auto;
}
.login-title p{
font-size: 18px;
color: #fff;
letter-spacing: .29px;
padding-top: 10px;
padding-bottom: 50px;
}
.login_box{
width: 400px;
height: auto;
background: #fff;
box-shadow: 0 2px 4px 0 rgba(0,0,0,.5);
border-radius: 4px;
margin: 0 auto;
padding-bottom: 40px;
}
.login_box .title{
font-size: 20px;
color: #9b9b9b;
letter-spacing: .32px;
border-bottom: 1px solid #e6e6e6;
display: flex;
justify-content: space-around;
padding: 50px 60px 0 60px;
margin-bottom: 20px;
cursor: pointer;
}
.login_box .title .current{
color: #4a4a4a;
border-bottom: 2px solid #84cc39;
}
.inp{
width: 350px;
margin: 0 auto;
}
.inp input{
outline: 0;
width: 100%;
height: 45px;
border-radius: 4px;
border: 1px solid #d9d9d9;
text-indent: 20px;
font-size: 14px;
background: #fff !important;
}
.inp input.user{
margin-bottom: 16px;
}
.inp .rember{
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
margin-top: 10px;
}
.inp .rember p:first-of-type{
font-size: 12px;
color: #4a4a4a;
letter-spacing: .19px;
margin-left: 22px;
display: -ms-flexbox;
display: flex;
-ms-flex-align: center;
align-items: center;
/*position: relative;*/
}
.inp .rember p:nth-of-type(2){
font-size: 14px;
color: #9b9b9b;
letter-spacing: .19px;
cursor: pointer;
}
.inp .rember input{
outline: 0;
width: 30px;
height: 45px;
border-radius: 4px;
border: 1px solid #d9d9d9;
text-indent: 20px;
font-size: 14px;
background: #fff !important;
}
.inp .rember p span{
display: inline-block;
font-size: 12px;
width: 100px;
/*position: absolute;*/
/*left: 20px;*/
}
#geetest{
margin-top: 20px;
}
.login_btn{
width: 100%;
height: 45px;
background: #84cc39;
border-radius: 5px;
font-size: 16px;
color: #fff;
letter-spacing: .26px;
margin-top: 30px;
}
.inp .go_login{
text-align: center;
font-size: 14px;
color: #9b9b9b;
letter-spacing: .26px;
padding-top: 20px;
}
.inp .go_login span{
color: #84cc39;
cursor: pointer;
}
style>