项目1
python项目中必须包含一个 requirements.txt 文件,用于记录所有依赖包及其精确的版本号。以便新环境部署。 requirements.txt可以通过pip命令自动生成和安装 生成requirements.txt文件 pip freeze > requirements.txt 安装requirements.txt依赖 pip install -r requirements.txt
cd ~/Desktop
mkvirtualenv luffy
如果自己的开发机子中存在多个版本的python,则可以指定解析器的版本
mkvirtualenv luffy -p python3
创建虚拟环境: mkvirtualenv 虚拟环境名称 创建虚拟环境(指定python版本): mkvirtualenv -p python3 虚拟环境名称 查看所有虚拟环境: workon+2次tab键 使用虚拟环境: workon 虚拟环境名称 退出虚拟环境: deactivate 删除虚拟环境(必须先退出虚拟环境内部才能删除当前虚拟环境): rmvirtualenv 虚拟环境名称 其他相关命令: 查看虚拟环境中安装的包: pip freeze 或者 pip list 收集当前环境中安装的包及其版本: pip freeze > requirements.txt 在部署项目的服务器中安装项目使用的模块: pip install -r requirements.txt
-
虚拟环境只会管理环境内部的模块和python解析器,对于源代码是毫无关系
-
创建虚拟环境需要联网
-
创建成功后, 会自动工作在这个虚拟环境上
-
工作在虚拟环境上, 提示符最前面会出现 “(虚拟环境名称)”
外部依赖
-
注册支付宝的开发者账号
-
注册阿里云账号,如果可以购买一个服务器,或者第一次使用的可以申请一个免费外网服务器
-
注册容联云短信接口平台的账号
-
注册保利威视频服务平台的账号[先不要注册,有个免费的测试开发时间,7天]
-
注册gitee[码云]的账号
-
如果有条件的,可以申请一个域名进行备案[ICP备案和公安部备案],如果没有的话, 可以注册natapp
pip install django -i https://pypi.douban.com/simple pip install djangorestframework -i https://pypi.douban.com/simple pip install PymySQL -i https://pypi.douban.com/simple pip install Pillow -i https://pypi.douban.com/simple pip install django-redis -i https://pypi.douban.com/simple
cd ~/Desktop
mkdir luffy
cd luffy
django-admin startproject luffyapi
在pycharm中打开项目
设置虚拟环境
启动django项目
效果:
提示:
在pycharm中如果要使用已经创建好的虚拟环境,则必须设置pycharm中的python解释器,设置为虚拟环境中的python。
luffy/ ├── docs/ # 项目相关资料保存目录 ├── luffycity/ # 前端项目目录 ├── luffyapi/ # api服务端项目目录 ├── logs/ # 项目运行时/开发时日志目录 ├── manage.py ├── luffyapi/ # 项目主应用,开发时的代码保存 │ ├── apps/ # 开发者的代码保存目录,以模块[子应用]为目录保存 │ ├── libs/ # 第三方类库的保存目录[第三方组件、模块] │ ├── settings/ │ ├── dev.py # 项目开发时的本地配置[不需要上传到线上或者服务器] │ ├── prod.py # 项目上线时的运行配置 │ ├── urls.py # 总路由 │ ├── utils/ # 多个模块[子应用]的公共函数类库[自己开发的组件] └── scripts/ # 保存项目运营时的脚本文件
在编辑中开发项目时,必须指定项目目录才能运行,例如,开发后端项目,则必须选择的目录是luffyapi
开发者本地的环境、目录、数据库密码和线上的服务器都会不一样,所以我们的配置文件可以针对不同的系统分成多分.
-
在项目主应用下,创建一个settings的配置文件存储目录
-
根据线上线下两种情况分别创建2个配置文件 dev.py和prod.py
-
把原来项目主应用的 settings.py配置内容复制2份到dev.py和prod.py里面
-
把原来的settings.py配置文件修改文件名或者删除
新的目录settings:
接下来,就可以根据在manage.py中根据不同的情况导入对应的配置文件了.
在settings/dev.py文件中追加如下配置:
# 日志配置 LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'formatters': { # 日志的处理格式 'verbose': { 'format': '%(levelname)s %(asctime)s %(module)s %(lineno)d %(message)s' }, 'simple': { 'format': '%(levelname)s %(module)s %(lineno)d %(message)s' }, }, 'filters': { 'require_debug_true': { '()': 'django.utils.log.RequireDebugTrue', }, }, 'handlers': { 'console': { 'level': 'DEBUG', 'filters': ['require_debug_true'], 'class': 'logging.StreamHandler', 'formatter': 'simple' }, 'file': { 'level': 'INFO', 'class': 'logging.handlers.RotatingFileHandler', # 日志位置,日志文件名,日志保存目录必须手动创建 'filename': os.path.join(os.path.dirname(BASE_DIR), "logs/luffy.log"), # 单个日志文件的最大值,这里我们设置300M 'maxBytes': 300 * 1024 * 1024, # 备份日志文件的数量,设置最大日志数量为10 'backupCount': 10, # 日志格式:详细格式 'formatter': 'verbose' }, }, # 日志对象 'loggers': { 'django': { # 固定,将来django内部也会有异常的处理,只会调用django下标的日志对象 'handlers': ['console', 'file'], 'propagate': True, # 是否让日志信息继续冒泡给其他的日志处理系统 }, } }
新建utils/exceptions.py
from rest_framework.views import exception_handler from django.db import DatabaseError from rest_framework.response import Response from rest_framework import status import logging logger = logging.getLogger('django') def custom_exception_handler(exc, context): """ 自定义异常处理 :param exc: 异常类 :param context: 抛出异常的上下文 :return: Response响应对象 """ # 调用drf框架原生的异常处理方法 response = exception_handler(exc, context) if response is None: view = context['view'] if isinstance(exc, DatabaseError): # 数据库异常 logger.error('[%s] %s' % (view, exc)) response = Response({'message': '服务器内部错误'}, status=status.HTTP_507_INSUFFICIENT_STORAGE) return response
settings/dev.py配置文件中添加
REST_FRAMEWORK = { # 异常处理 'EXCEPTION_HANDLER': 'luffyapi.utils.exceptions.custom_exception_handler', }
create database luffy default charset=utf8mb4;
为当前项目创建数据库用户[这个用户只能看到这个数据库]
create user luffy_user identified by 'luffy'; # 账号名luffy_user 密码luffy grant all privileges on luffy.* to 'luffy_user'@'%'; flush privileges; # 数据库名luffy 表名* 用户名luffy_user 登录ip %
打开settings/dev.py文件,并配置
DATABASES = { "default": { "ENGINE": "django.db.backends.mysql", "HOST": "127.0.0.1", "PORT": 3306, "USER": "luffy_user", "PASSWORD": "luffy", "NAME": "luffy", } }
import pymysql pymysql.install_as_MySQLdb()
数据库版本检测导致的错误
数据库的版本检测代码注释掉。
第二个错误也是因为数据库版本的默认编码导致,query返回的内容格式使用有误。
新增一行代码,把query查询结果转换格式为 bytes类型
cd 项目根目录
vue init webpack 客户端项目目录
例如,我要把项目保存在~/Desktop桌面目录下,可以如下操作:
cd ~/Desktop/luffy
vue init webpack lufei_pc
根据需要在生成项目时,我们选择对应的选项, 效果:
根据上面的提示,我们已经把vue项目构建好了,运行测试服务器。
打开项目已经,在pycharm的终端下运行vue项目,查看效果。
npm run dev
接下来,我们根据终端上效果显示的对应地址来访问项目(如果有多个vue项目在运行,8080端口被占据了,服务器会自动改端口,所以根据自己实际在操作中看到的地址来访问。)
我们也可以把我们的前端项目进行git源代码管理
清除默认的HelloWorld组件和APP.vue中的默认样式
接下来,我们可以查看效果了,一张白纸~
如果前面没有选择安装vue-router,则使用以下命令安装路由组件:
npm i vue-router -S
执行效果:
在src目录下创建routers路由目录,在routers目录下创建index.js路由文件
import Vue from "vue" import Router from "vue-router" // 这里导入可以让让用户访问的组件 Vue.use(Router); export default new Router({ // 设置路由模式为‘history’,去掉默认的# mode: "history", routes:[ // 路由列表 { path: '/', } ] })
打开main.js文件,把router对象注册到vue中.代码:
// The Vue build version to load with the `import` command // (runtime-only or standalone) has been set in webpack.base.conf with an alias. import Vue from 'vue' import App from './App' import router from './routers/index'; Vue.config.productionTip = false /* eslint-disable no-new */ new Vue({ el: '#app', router, components: { App }, template: '<App/>' });
在App.vue组件中,添加显示路由对应的内容。
代码:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<template> <div id="app"> <router-view/> </div> </template> <script> export default { name: 'App', components: { } } </script> <style> </style>
routers/index.js
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
// import Vue from "vue" // import Router from "vue-router" // // // // 这里导入可以让让用户访问的组件 import Home from "../components/Home" // Vue.use(Router); // // export default new Router({ // // 设置路由模式为‘history’,去掉默认的# // mode: "history", // routes:[ // // 路由列表 { name:"Home", path:"/", component:Home, }, { name:"Home", path:"/home", component:Home, }, ] // })
components/Home.vue
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<template> <div id="home"> 前端首页 </div> </template> <script> export default { name:"Home", data(){ return { } } } </script> <style scoped> </style>
在src目录下创建settings.js站点开发配置文件:
export default { Host:"http://127.0.0.1", }
在main.js中引入
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
// // The Vue build version to load with the `import` command // // (runtime-only or standalone) has been set in webpack.base.conf with an alias. // import Vue from 'vue' // import App from './App' // import router from './routers/index'; import settings from "./settings" // Vue.config.productionTip = false; Vue.prototype.$settings = settings; // // /* eslint-disable no-new */ // new Vue({ // el: '#app', // router, // components: { App }, // template: '<App/>' // });
npm i element-ui -S
上面的命令等同于
npm install element-ui --save
执行命令效果:
在main.js中导入ElementUI,并调用。
代码:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
// The Vue build version to load with the `import` command // (runtime-only or standalone) has been set in webpack.base.conf with an alias. // import Vue from 'vue' // import App from './App' // import router from './routers/index'; // 开发配置文件 // import settings from "./settings" // Vue.prototype.$settings = settings; // elementUI 导入 import ElementUI from 'element-ui'; // 调用插件 Vue.use(ElementUI); // Vue.config.productionTip = false; /* eslint-disable no-new */ // new Vue({ // el: '#app', // router, // components: { App }, // template: '<App/>' // });
index.html入口文件中引入elementUI组件的css库,代码:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
效果:
成功引入了ElementUI以后,接下来我们就可以开始进入前端页面开发,首先是首页。
接下来我们把之前完成的首页,直接拿过来使用[注意除了组件以外,还有静态文件也需要拿过来,包括App.vue里面的公共样式],并运行项目。
App.vue,全局css初始化代码
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<template> <div id="app"> <router-view/> </div> </template> <script> export default { name: 'App' } </script> <style> /* 声明全局样式和项目的初始化样式 */ body,h1,h2,h3,h4,p,table,tr,td,ul,li,a,form,input,select,option,textarea{ margin:0; padding: 0; font-size: 15px; } a{ text-decoration: none; color: #333; } ul,li{ list-style: none; } table{ border-collapse: collapse; /* 合并边框 */ } /* 工具的全局样式 */ .full-left{ float: left!important; } .full-right{ float: right!important; } [class*=" el-icon-"], [class^=el-icon-]{ font-size: 50px; } .el-carousel__arrow{ width: 120px; height: 120px; } .el-checkbox__input.is-checked .el-checkbox__inner, .el-checkbox__input.is-indeterminate .el-checkbox__inner{ background: #ffc210; border-color: #ffc210; border: none; } .el-checkbox__inner:hover{ border-color: #9b9b9b; } .el-checkbox__inner{ width: 16px; height: 16px; border: 1px solid #9b9b9b; border-radius: 0; } .el-checkbox__inner::after{ height: 9px; width: 5px; } </style>
Home.vue中添加代码:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<template> <div class="home"> <Header></Header> <Banner></Banner> <Footer></Footer> </div> </template> <script> import Header from "./common/Header" import Banner from "./common/Banner" import Footer from "./common/Footer" export default { name: "Home", data(){ return {} }, methods:{ }, components:{ Header, Footer, Banner, } } </script> <style scoped> </style>
components/common/Header.vue
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<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><span>免费课</span></li> <li><span>轻课</span></li> <li><span>学位课</span></li> <li><span>题库</span></li> <li><span>老男孩教育</span></li> </ul> <div 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 full-left"> <span>登录</span> | <span>注册</span> </div> </div> </div> </div> </div> </template> <script> export default { name: "Header", data(){ return{ } }, created(){ }, methods:{ } } </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; } .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 .login-box{ margin-top: 33px; } .header .login-bar .login-box span{ color: #4a4a4a; cursor: pointer; } .header .login-bar .login-box span:hover{ color: #000000; } </style>
components/common/Banner.vue
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<template> <el-carousel height="480px" :interval="3000" arrow="always"> <el-carousel-item> <img src="/static/image/alex.jpeg" alt=""> </el-carousel-item> <el-carousel-item> <img src="/static/image/banner1.png" alt=""> </el-carousel-item> <el-carousel-item> <img src="/static/image/banner1.png" alt=""> </el-carousel-item> </el-carousel> </template> <script> export default { name: "Banner", } </script> <style scoped> .el-carousel__item h3 { color: #475669; font-size: 18px; opacity: 0.75; line-height: 300px; margin: 0; } .el-carousel__item:nth-child(2n) { background-color: #99a9bf; } .el-carousel__item:nth-child(2n+1) { background-color: #d3dce6; } </style>
components/common/Footer.vue
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<template> <div class="footer"> <ul> <li>关于我们</li> <li>联系我们</li> <li>商务合作</li> <li>帮助中心</li> <li>意见反馈</li> <li>新手指南</li> </ul> <p>Copyright © luffycity.com版权所有 | 京ICP备17072161号-1</p> </div> </template> <script> export default { name: "Footer" } </script> <style scoped> .footer { width: 100%; height: 128px; background: #25292e; color: #fff; } .footer ul{ margin: 0 auto 16px; padding-top: 38px; width: 810px; } .footer ul li{ float: left; width: 112px; margin: 0 10px; text-align: center; font-size: 14px; } .footer ul::after{ content:""; display:block; clear:both; } .footer p{ text-align: center; font-size: 12px; } </style>
也可以把App.vue的style标签的css代码放到static外部目录下引用过来
import "../static/css/reset.css";
reset.css
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
body,h1,h2,h3,h4,h5,ul,p{ padding: 0; margin:0; font-weight: normal; } body{ margin-top: 80px; } a{ text-decoration: none; color: #4a4a4a; } a:hover{ color: #000; } ul{ list-style: none; } img{ width: 100%; } .header .el-menu li .el-submenu__title{ height: 26px!important; line-height: 26px!important; } .el-menu--popup{ min-width: 140px; } .el-checkbox__inner{ width:16px; height: 16px; border: 1px solid #999; } .el-checkbox__inner:after{ width: 6px; height: 8px; } .el-form-item__content{ margin-left:0px!important; width: 120px; } .full-left{ float: left; } .full-right{ float: right; }
我们现在为前端和后端分别设置两个不同的域名:
域名 | |
---|---|
前端 | www.luffycity.cn |
后端 | api.luffycity.cn |
sudo vim /etc/hosts
在文件中增加两条信息
127.0.0.1 localhost 127.0.0.1 api.luffycity.cn 127.0.0.1 www.luffycity.cn
# 查找nginx的进程 ps -ef|grep nginx # 关闭进程 sudo kill -9 nginx进程号
上面并不是错误,而是没人监听了这个地址和端口了,解决方法:
host: 'www.luffycity.cn', // can be overwritten by process.env.HOST port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined autoOpenBrowser: true,
保存修改信息,并重启项目
通过浏览器访问drf项目,会出现以下错误信息
可以通过settings/dev.py的ALLOWED_HOSTS,设置允许访问
# 设置哪些客户端可以通过地址访问到后端 ALLOWED_HOSTS = [ 'api.luffycity.cn', ]
让用户访问的时候,使用api.luffycity.cn:8000
1. 修改pycharm的manage.py的配置参数
现在,前端与后端分处不同的域名,我们需要为后端添加跨域访问的支持
否则前端无法使用axios无法请求后端提供的api数据,我们使用CORS来解决后端对跨域访问的支持。
使用django-cors-headers扩展
在 Response(headers={"Access-Control-Allow-Origin":'客户端地址/*'})
文档:https://github.com/ottoyiu/django-cors-headers/
安装
pip install django-cors-headers
settings/dev.py,添加应用
INSTALLED_APPS = ( ... 'corsheaders', ... )
中间层设置【必须写在第一个位置】
MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', ... ]
添加白名单
# CORS组的配置信息 CORS_ORIGIN_WHITELIST = ( 'www.luffycity.cn:8080', ) CORS_ALLOW_CREDENTIALS = False # 允许ajax跨域请求时携带cookie
前端使用 axios就可以访问到后端提供给的数据接口,但是如果要附带cookie信息,前端还要设置一下。
前端引入axios插件并配置允许axios发送cookie信息[axios本身也不允许ajax发送cookie到后端]
npm i axios -S
在main.js中引用 axios插件
import axios from 'axios'; // 从node_modules目录中导入包 // 允许ajax发送请求时附带cookie,设置为不允许 axios.defaults.withCredentials = false; Vue.prototype.$axios = axios; // 把对象挂载vue中
图片处理模块
前面已经安装了,如果没有安装则需要安装
pip install pillow
settings/dev.py
# 访问静态文件的url地址前缀 STATIC_URL = '/static/' # 设置django的静态文件目录 STATICFILES_DIRS = [ os.path.join(BASE_DIR,"static") ] # 项目中存储上传文件的根目录[暂时配置],注意,uploads目录需要手动创建否则上传文件时报错 MEDIA_ROOT=os.path.join(BASE_DIR,"uploads") # 访问上传文件的url地址前缀 MEDIA_URL ="/media/"
在项目下创建一个static和uploads文件
在xadmin中输出上传文件的Url地址
总路由urls.py新增代码:
from django.urls import re_path from django.conf import settings from django.views.static import serve urlpatterns = [ ... re_path(r'media/(?P<path>.*)', serve, {"document_root": settings.MEDIA_ROOT}), ]
因为当前功能是drf的第一个功能,所以我们先创建一个子应用home,创建在luffyapi/apps目录下
cd luffyapi/apps
python ../../manage.py startapp home
注册home子应用,因为子应用的位置发生了改变,所以为了原来子应用的注册写法,所以新增一个导包路径:
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # 新增一个系统导包路径 import sys sys.path.insert(0,os.path.join(BASE_DIR,"apps")) INSTALLED_APPS = [ # 注意,加上drf框架的注册 'rest_framework',# 子应用 'home', ]
注意,pycharm会路径错误的提示。
创建轮播图的模型
home/models.py
from django.db import models # Create your models here. class Banner(models.Model): """ 轮播图 """ # upload_to 存储子目录,真实存放地址会使用配置中的MADIE_ROOT+upload_to image = models.ImageField(upload_to='banner', verbose_name='轮播图', null=True,blank=True) name = models.CharField(max_length=150, verbose_name='轮播图名称') note = models.CharField(max_length=150, verbose_name='备注信息') link = models.CharField(max_length=150, verbose_name='轮播图广告地址') orders = models.IntegerField(verbose_name='显示顺序') is_show=models.BooleanField(verbose_name="是否上架",default=False) is_delete=models.BooleanField(verbose_name="逻辑删除",default=False) class Meta: db_table = 'ly_banner' # db_table 设置表面 verbose_name = '轮播图' verbose_name_plural = verbose_name def __str__(self): return self.name
数据迁移
python manage.py makemigrations
python manage.py migrate
home/serializers.py
from rest_framework import serializers from .models import Banner class BannerModelSerializer(serializers.ModelSerializer): """轮播图序列化器""" class Meta: model = Banner fields = ["image","link"]
views.py
from rest_framework.generics import ListAPIView from .models import Banner from .serializers import BannerModelSerializer from luffyapi.settings import constants class BannerListAPIView(ListAPIView): queryset = Banner.objects.filter(is_show=True, is_delete=False).order_by("-orders","-id")[:constants.HOME_BANNER_LENGTH] serializer_class = BannerModelSerializer
home/urls.py
from django.urls import path,re_path from . import views urlpatterns = [ path("banner/",views.BannerListAPIView.as_view()), ]
把home的路由urls.py注册到总路由
from django.contrib import admin from django.urls import path,re_path,include from django.conf import settings from django.views.static import serve urlpatterns = [ path('admin/', admin.site.urls), re_path(r'media/(?P<path>.*)', serve, {"document_root": settings.MEDIA_ROOT}), path('home/', include("home.urls") ), ]
settings目录新建一个constant.py常量文件:
# 首页轮播广告的显示数量 HOME_BANNER_LENGTH = 7
所以我们需要有一个后台提供数据.安装xadmin
pip install https://codeload.github.com/sshwsfc/xadmin/zip/django2
在配置文件中注册如下应用
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
在home子应用中创建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)
# 轮播图 from .models import Banner class BannerModelAdmin(object): list_display=["name","orders","is_show"] xadmin.site.register(Banner, BannerModelAdmin)
home/apps.py
class HomeConfig(AppConfig): name = 'home' verbose_name = '我的首页'
__init__.py
default_app_config = "home.apps.HomeConfig"
Banner.vue代码:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<template> <el-carousel height="480px" :interval="3000" arrow="always"> <el-carousel-item :key="key" v-for="banner,key in banner_list"> <a :href="banner.link"><img :src="banner.image" :alt="banner.name"></a> </el-carousel-item> </el-carousel> </template> <script> export default { name: "Banner", data(){ return { banner_list:[] } }, created() { this.get_banner_list(); }, methods:{ get_banner_list(){ // 获取轮播广告 this.$axios.get(`${this.$settings.Host}/home/banner`).then(response=>{ this.banner_list = response.data; }).catch(error=>{ console.log(error.response); }) } } } </script> <style scoped> .el-carousel__item h3 { color: #475669; font-size: 18px; opacity: 0.75; line-height: 300px; margin: 0; } .el-carousel__item:nth-child(2n) { background-color: #99a9bf; } .el-carousel__item:nth-child(2n+1) { background-color: #d3dce6; } </style>
创建模型
home/models下
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
from django.db import models from luffyapi.utils.models import BaseModel # Create your models here. class Banner(BaseModel): """轮播广告""" image = models.ImageField(upload_to="banner",blank=True, null=True, verbose_name="广告图片") name = models.CharField(max_length=255,blank=True, null=True, verbose_name="广告标题") link = models.CharField(max_length=512,blank=True, null=True, verbose_name="广告链接") note = models.TextField(verbose_name='备注信息') class Meta: db_table = 'ly_banner' verbose_name = '轮播图' verbose_name_plural = verbose_name def __str__(self): return self.name class Nav(BaseModel): """ 导航 """ NAV_POSITION = ( (0, '头部导航'), (1, '底部导航'), ) name = models.CharField(max_length=50, verbose_name='导航名称') link = models.CharField(max_length=250, verbose_name='导航地址') opt = models.SmallIntegerField(choices=NAV_POSITION, default=0, verbose_name='显示位置') is_http = models.BooleanField(default=False, verbose_name="是否站外导航") class Meta: db_table = 'ly_nav' verbose_name = '导航' verbose_name_plural = verbose_name def __str__(self): return self.name
公共模型,保存项目的公共代码库目录下luffyapi/utils.py文件中创建models。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
from django.db import models class BaseModel(models.Model): orders = models.IntegerField(default=1, verbose_name="显示顺序") is_show = models.BooleanField(verbose_name="是否上架", default=True) is_delete = models.BooleanField(verbose_name="逻辑删除", default=False) create_time = models.DateTimeField(auto_now_add=True,verbose_name="添加时间") update_time = models.DateTimeField(auto_now=True,verbose_name="更新时间") class Meta: # 设置当前模型在数据迁移的时候不要为它创建表 abstract = True
数据迁移
python manage.py makemigrations
python manage.py migrate
from .models import Nav class NavModelSerializer(serializers.ModelSerializer): """导航菜单序列化器""" class Meta: model = Nav fields = ["name","link","opt","is_http"]
home/views.py
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
# Create your views here. from rest_framework.generics import ListAPIView from .models import Banner from .serializers import BannerModelSerializer from luffyapi.settings import constants class BannerListAPIView(ListAPIView): """轮播图""" queryset = Banner.objects.filter(is_show=True, is_delete=False).order_by("-orders","-id")[:constants.HOME_BANNER_LENGTH] serializer_class = BannerModelSerializer from .models import Nav from .serializers import NavModelSerializer class NavHeaderListAPIView(ListAPIView): """头部导航""" queryset = Nav.objects.filter(is_show=True, is_delete=False, opt=0).order_by("-orders","-id")[:constants.HEADER_NAV_LENGTH] serializer_class = NavModelSerializer from .serializers import NavModelSerializer class NavFooterListAPIView(ListAPIView): """脚部导航""" queryset = Nav.objects.filter(is_show=True, is_delete=False, opt=1).order_by("-orders","-id")[:constants.FOOTER_NAV_LENGTH] serializer_class = NavModelSerializer
settings/constants.py,代码:
# 首页轮播广告的显示数量 HOME_BANNER_LENGTH = 7 # 头部导航的显示数据量 HEADER_NAV_LENGTH = 7 # 脚部导航的显示数据量 FOOTER_NAV_LENGTH = 7
urls.py
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
from django.urls import path,re_path from . import views urlpatterns = [ path("banner/",views.BannerListAPIView.as_view()), path("nav/header/",views.NavHeaderListAPIView.as_view()), path("nav/footer/",views.NavFooterListAPIView.as_view()), ]
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
# 导航 from .models import Nav class NavModelAdmin(object): list_display=["name","link","is_show","is_http"] xadmin.site.register(Nav, NavModelAdmin)
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<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"> <a :href="nav.link" v-if="nav.is_http"><span>{{nav.name}}</span></a> <router-link :to="nav.link" v-else><span>{{nav.name}}</span></router-link> </li> </ul> <div 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 full-left"> <span>登录</span> | <span>注册</span> </div> </div> </div> </div> </div> </template> <script> export default { name: "Header", data(){ return{ nav_list:[], } }, created(){ this.get_nav_list(); }, methods:{ get_nav_list(){ // 获取导航 this.$axios.get(`${this.$settings.Host}/home/nav/header/`).then(response=>{ this.nav_list = response.data; }).catch(error=>{ console.log(error.response); }); } } } </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; } .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 .login-box{ margin-top: 33px; } .header .login-bar .login-box span{ color: #4a4a4a; cursor: pointer; } .header .login-bar .login-box span:hover{ color: #000000; } </style>
Footer.vue
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<template> <div class="footer"> <ul> <li :key="key" v-for="nav,key in nav_list"> <a :href="nav.link" v-if="nav.is_http">{{nav.name}}</a> <router-link :to="nav.link" v-else>{{nav.name}}</router-link> </li> </ul> <p>Copyright © luffycity.com版权所有 | 京ICP备17072161号-1</p> </div> </template> <script> export default { name: "Footer", data(){ return{ nav_list:[], } }, created(){ this.get_nav_list(); }, methods:{ get_nav_list(){ // 获取导航 this.$axios.get(`${this.$settings.Host}/home/nav/footer/`).then(response=>{ this.nav_list = response.data; }).catch(error=>{ console.log(error.response); }); } } } </script> <style scoped> .footer { width: 100%; height: 128px; background: #25292e; color: #fff; } .footer ul{ margin: 0 auto 16px; padding-top: 38px; width: 810px; } .footer ul li{ float: left; width: 112px; margin: 0 10px; text-align: center; font-size: 14px; } .footer ul::after{ content:""; display:block; clear:both; } .footer p{ text-align: center; font-size: 12px; } .footer ul a{ color: #fff; } </style>
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<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">密码登录</span> <span @click="login_type=1">短信登录</span> </div> <div class="inp" v-if="login_type==0"> <input v-model = "username" type="text" placeholder="用户名 / 手机号码" class="user"> <input v-model = "password" type="password" name="" class="pwd" placeholder="密码"> <div id="geetest1"></div> <div class="rember"> <p> <input type="checkbox" class="no" name="a"/> <span>记住密码</span> </p> <p>忘记密码</p> </div> <button class="login_btn">登录</button> <p class="go_login" >没有账号 <span>立即注册</span></p> </div> <div class="inp" v-show="login_type==1"> <input v-model = "username" type="text" placeholder="手机号码" class="user"> <input v-model = "password" type="text" class="pwd" placeholder="短信验证码"> <button id="get_code">获取验证码</button> <button class="login_btn">登录</button> <p class="go_login" >没有账号 <span>立即注册</span></p> </div> </div> </div> </div> </template> <script> export default { name: 'Login', data(){ return { login_type: 0, username:"", password:"", } }, methods:{ }, }; </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 span:nth-of-type(1){ 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>
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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, } ] })
<router-link to="/login">登录</router-link>
-
用户管理
-
权限[RBAC]
-
用户组
-
密码哈希系统
-
用户登录或内容显示的表单和视图
-
一个可插拔的后台系统
Django默认用户的认证机制依赖Session机制,我们在项目中将引入JWT认证机制,将用户的身份凭据存放在Token中,然后对接Django的认证系统,帮助我们来实现:
-
-
用户密码的加密与验证
-
用户的权限系统
字段描述 | |
---|---|
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 luffyapi/apps/
python ../../manage.py startapp users
在settings.py文件中注册子应用。
INSTALLED_APPS = [ ... 'users', ]
Django认证系统中提供的用户模型类及方法很方便,我们可以使用这个模型类,但是字段有些无法满足项目需求,如本项目中需要保存用户的手机号,需要给模型类添加额外的字段。
Django提供了
我们可以在apps中创建Django应用users,并在配置文件中注册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, unique=True, verbose_name="手机号码", help_text="手机号码") avatar = models.ImageField(upload_to="avatar", blank=True, null=True, verbose_name="头像") wechat = models.CharField(max_length=100, unique=True,null=True ,verbose_name="微信号", help_text="微信号") class Meta: db_table = 'ly_users' verbose_name = '用户' verbose_name_plural = verbose_name
在配置文件中进行设置
AUTH_USER_MODEL = 'users.User'
注意:Django建议我们对于AUTH_USER_MODEL参数的设置一定要在第一次数据库迁移之前就设置好,否则后续使用可能出现未知错误。
执行数据库迁移
python manage.py makemigrations
python manage.py migrate
django.db.migrations.exceptions.InconsistentMigrationHistory: Migration reversion.0001_squashed_0004_auto_20160611_1202 is applied before its dependency users.0001_initial on database 'default'.
解决步骤: 1. 备份数据库,删除关于用户原来的数据表信息和表结构[如果刚开始开发,则直接清除库中所有数据表即可。] 2. 删除子应用users中migrations目录下除了__init__.py以外的所有迁移文件 3. 删除在django.contrib.admin和django.contrib.auth模块里面的migrations迁移文件,除了__init__.py 4. 删除在xadmin和reversion模块中的migrations的迁移文件,除了__init__.py。 5. 执行数据迁移,把备份数据,除了用户以外的全部恢复执行即可。 6. 使用manage.py createsuperuser创建管理员即可
1. 备份数据库,删除关于用户原来的数据表信息和表结构[如果刚开始开发,则直接清除库中所有数据表即可。]
# 备份 /Desktop/luffy/下创建docs,而luffy_2010_11_05.sql写入系统自动创建 mysqldump -uroot -p123 luffy > ~/Desktop/luffy/docs/luffy_2010_11_05.sql
选中全部删除
2. 删除子应用users中migrations目录下除了__init__.py以外的所有迁移文件
3. 删除在django.contrib.admin和django.contrib.auth模块里面的migrations迁移文件,除了__init__.py
删除External Libraries/Python 3.6(luffy)/site-packages/django/contrib/admin/migrations下迁移文件
删除External Libraries/Python 3.6(luffy)/site-packages/django/contrib/auth/migrations下迁移文件
4. 删除在xadmin和reversion模块中的migrations的迁移文件,除了__init__.py。
删除External Libraries/Python 3.6(luffy)/site-packages/xadmin/migrations
删除External Libraries/Python 3.6(luffy)/site-packages/reversion/migration
5. 执行数据迁移,把备份数据,除了用户以外的全部恢复执行即可。
6. 使用manage.py createsuperuser创建管理员即可
在用户注册或登录后,我们想记录用户的登录状态,或者为用户创建身份认证的凭证。我们不再使用Session认证机制,而使用Json Web Token认证机制。
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature).
jwt的头部承载两部分信息:
-
声明类型,这里是jwt
-
声明加密的算法 通常直接使用 HMAC SHA256
完整的头部就像下面这样的JSON:
{ 'typ': 'JWT', 'alg': 'HS256' }
然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分.
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分
-
标准中注册的声明
-
公共的声明
-
私有的声明
标准中注册的声明 (建议但不强制使用) :
-
iss: jwt签发者
-
sub: jwt所面向的用户
-
aud: 接收jwt的一方
-
exp: jwt的过期时间,这个过期时间必须要大于签发时间
-
nbf: 定义在什么时间之前,该jwt都是不可用的.
-
iat: jwt的签发时间
-
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
公共的声明 : 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
私有的声明 : 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
定义一个payload:
{ "sub": "1234567890", "name": "John Doe", "admin": 231sldsad2131 }
然后将其进行base64.b64encode() 加密,得到JWT的第二部分。
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
JWT的第三部分是一个防止客户端篡改信息的签证信息,这个签证信息由三个小部分组成:
-
header (base64后的)
-
payload (base64后的)
-
secret[秘钥]
这个部分需要base64加密后的header和base64加密后的payload使用.
连接组成的字符串,然后通过header中声明的加密方式进行加盐secret
组合加密,然后就构成了jwt的第三部分。
// javascript var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload); var signature = HMACSHA256(encodedString, 'secret'); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
jwt的优势:
1. 天生适用于SSO单点登录 2. 服务端只需要存储秘钥,不需要分别存放用户的jwt信息,降低了服务器的存储压力 3. 比cookie更加安全可靠。 4. 还可以用于提供数据加密传输的功能
jwt的缺点:
1. jwt存放在客户端的,所以一旦签发以后,我们服务端是无法控制或提前回收.
2. 增加客户端的技术难度
关于签发和核验JWT,我们可以使用Django REST framework JWT扩展来完成。
安装配置JWT
安装
pip install djangorestframework-jwt
配置
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), }
JWT_EXPIRATION_DELTA 指明token的有效期
生成jwt
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的视图,可以直接使用
from rest_framework_jwt.views import obtain_jwt_token from django.urls import path urlpatterns = [ path("login/", obtain_jwt_token), # 视图类.as_view() ]
在主路由中,引入当前子应用的路由文件
urlpatterns = [ ... path('users/', include("users.urls")), # include 的值必须是 模块名.urls 格式,字符串中间只能出现一个圆点 ]
接下来,我们可以通过postman来测试下功能
在登陆组件中找到登陆按钮,绑定点击事件
<router-link to="/login"><span>登录</span></router-link>
在methods中请求后端
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<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">密码登录</span> <span @click="login_type=1">短信登录</span> </div> <div class="inp" v-if="login_type==0"> <input v-model = "username" type="text" placeholder="用户名 / 手机号码" class="user"> <input v-model = "password" type="password" name="" class="pwd" placeholder="密码"> <div id="geetest1"></div> <div class="rember"> <p> <input type="checkbox" class="no" name="a"/> <span>记住密码</span> </p> <p>忘记密码</p> </div> <button class="login_btn">登录</button> <p class="go_login" >没有账号 <span>立即注册</span></p> </div> <div class="inp" v-show="login_type==1"> <input v-model = "username" type="text" placeholder="手机号码" class="user"> <input v-model = "password" type="text" class="pwd" placeholder="短信验证码"> <button id="get_code">获取验证码</button> <router-link to="/login"><span>登录</span></router-link> <p class="go_login" >没有账号 <span>立即注册</span></p> </div> </div> </div> </div> </template> <script> export default { name: 'Login', data(){ return { login_type: 0, username:"", password:"", } }, methods:{ }, }; </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 span:nth-of-type(1){ 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>
我们可以将JWT保存在cookie中,也可以保存在浏览器的本地存储里,我们保存在浏览器本地存储中
浏览器的本地存储提供了sessionStorage 和 localStorage 两种:
-
sessionStorage 浏览器关闭即失效
-
localStorage 长期有效
使用方法
sessionStorage.变量名 = 变量值 // 保存数据 // sessionStorage.setItem("变量名","变量值"); // 也是保存数据 sessionStorage.变量名 // 读取数据 // sessionStorage.getItem("变量名"); // 也是读取数据 sessionStorage.clear() // 清除所有sessionStorage保存的数据 sessionStorage.removeItem("变量名"); // 删除一个指定变量名对应的数据 localStorage.变量名 = 变量值 // 保存数据 // localStorage.setItem("变量名","变量值"); // 也是保存数据 localStorage.变量名 // 读取数据 // localStorage.getItem("变量名"); // 也是读取数据 localStorage.clear() // 清除所有localStorage保存的数据 localStorage.removeItem("变量名"); // 删除一个指定变量名对应的数据
登陆组件代码Login.vue
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<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">密码登录</span> <span @click="login_type=1">短信登录</span> </div> <div class="inp" v-if="login_type==0"> <input v-model="username" type="text" placeholder="用户名 / 手机号码" class="user"> <input v-model="password" type="password" name="" class="pwd" placeholder="密码"> <div id="geetest1"></div> <div class="rember"> <p> <input type="checkbox" class="no" v-model="remember"/> <span>记住密码</span> </p> <p>忘记密码</p> </div> <button class="login_btn" @click="loginHandler">登录</button> <p class="go_login" >没有账号 <span>立即注册</span></p> </div> <div class="inp" v-show="login_type==1"> <input v-model = "username" type="text" placeholder="手机号码" class="user"> <input v-model = "password" type="text" class="pwd" placeholder="短信验证码"> <button id="get_code">获取验证码</button> <button class="login_btn">登录</button> <p class="go_login" >没有账号 <span>立即注册</span></p> </div> </div> </div> </div> </template> <script> export default { name: 'Login', data(){ return { login_type: 0, username:"", password:"", remember:false, } }, methods:{ loginHandler(){ // 登录处理 this.$axios.post(`${this.$settings.Host}/user/login/`,{ username: this.username, password: this.password, }).then(response=>{ console.log(response.data.token); // 判断用户是否勾选了"记住密码" if(this.remember){ // 记住密码 localStorage.user_token = response.data.token; }else{ // 不记住密码 sessionStorage.user_token = response.data.token; } let self = this; this.$alert("欢迎回来!","路飞学城",{ callback(){ // 页面跳转 // this.$router.push("/"); // 使用vue跳转页面到指定地址 self.$router.go(-1); // 使用vue跳转页面返回上一页 } }) }).catch(error=>{ if(error.response.status == 400){ // 登录失败! this.$message.error("登录失败!账号或密码错误!"); }else{ // 其他未知,告知管理员 this.$message.error("登录过程中遇到了未知错误!请联系站点管理员或客服人员!"); }; }) } }, }; </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 span:nth-of-type(1){ 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>
默认的返回值仅有token,我们还需在返回值中增加username和id,方便在客户端页面中显示当前登陆用户
通过修改该视图的返回值可以完成我们的需求。
在users/utils.py 中,创建
def jwt_response_payload_handler(token, user=None, request=None): """ 自定义jwt认证成功返回数据 :param token jwt认证信息 :param user 根据客户端提交的信息查询出来的用户模型对象 :param request 本次客户端提交的请求对象 """ return { 'token': token, 'id': user.id, 'username': user.username }
修改settings.py配置文件
# JWT JWT_AUTH = { # jwt的有效期 'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1), # 自定义响应数据格式 'JWT_RESPONSE_PAYLOAD_HANDLER': 'users.utils.jwt_response_payload_handler', }
登陆组件代码Login.vue
// 使用浏览器本地存储保存token if (this.remember) { // 记住登录 sessionStorage.clear(); localStorage.user_token = response.data.token; localStorage.user_id = response.data.id; localStorage.user_name = response.data.username; } else { // 未记住登录 localStorage.clear(); sessionStorage.user_token = response.data.token; sessionStorage.user_id = response.data.id; sessionStorage.user_name = response.data.username; }
JWT扩展的登录视图,在收到用户名与密码时,也是调用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中编写:
def jwt_response_payload_handler(token, user=None, request=None): """ 自定义jwt认证成功返回数据 :param token jwt认证信息 :param user 根据客户端提交的信息查询出来的用户模型对象 :param request 本次客户端提交的请求对象 """ return { 'token': token, 'id': user.id, 'username': user.username } from django.contrib.auth.backends import ModelBackend from .models import User from django.db.models import Q def get_user_by_account(username): return User.objects.filter( Q(username=username) | Q(mobile=username) ).first() class UsernameMobileAuthBackend(ModelBackend): # 重写认证类里面的认证方法,实现多条件登录 def authenticate(self, request, username=None, password=None, **kwargs): user = get_user_by_account(username) if user is not None and user.check_password(password): return user
在配置文件settings/dev.py中告知Django使用我们自定义的认证后端
AUTHENTICATION_BACKENDS = [ 'users.utils.UsernameMobileAuthBackend', ]
前端首页实现登陆状态的判断
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<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"> <a :href="nav.link" v-if="nav.is_http"><span>{{nav.name}}</span></a> <router-link :to="nav.link" v-else><span>{{nav.name}}</span></router-link> </li> </ul> <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-box1 full-left"> <router-link to="" class="study-ceneter">学习中心</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/logo@2x.png" alt=""></router-link></template> <el-menu-item index="2-1">我的账户</el-menu-item> <el-menu-item index="2-2">我的订单</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-show="!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="/login"><span>登录</span></router-link> | <span>注册</span> </div> </div> </div> </div> </div> </template> <script> export default { name: "Header", data(){ return{ token: "", nav_list:[], } }, created(){ this.token = this.$settings.check_user_login(); this.get_nav_list(); }, methods:{ get_nav_list(){ // 获取导航 this.$axios.get(`${this.$settings.Host}/home/nav/header/`).then(response=>{ this.nav_list = response.data; }).catch(error=>{ console.log(error.response); }); } } } </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; } .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 .login-box1{ margin-top: 25px; } .header .login-bar .login-box2{ margin-top: 32px; } .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; } .el-menu.el-menu--horizontal{ border-bottom: none; } .study-ceneter{ display: inline-block; height: 34px; line-height: 34px; vertical-align: text-bottom; } </style>
reset.css重置elementUI的组件外观:
调整头部购物车位置
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
.el-submenu__icon-arrow::before{ content:""; } .el-submenu__title{ padding: 0; } .el-menu--popup{ min-width: 140px; margin-left: -100px; } .geetest_holder.geetest_wind{ min-width: 100%!important; margin-top: 20px; }
src/settings
export default { Host:"http://api.luffycity.cn:8000", check_user_login(){ return localStorage.user_token || sessionStorage.user_token; } }
当前我们判断是否已经登录,主要依赖于token值,所以我们直接把token值清空,同时把本地存储中的用户登录信息移除即可。
Header.vue组件代码:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<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"> <a :href="nav.link" v-if="nav.is_http"><span>{{nav.name}}</span></a> <router-link :to="nav.link" v-else><span>{{nav.name}}</span></router-link> </li> </ul> <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-box1 full-left"> <router-link to="" class="study-ceneter">学习中心</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/logo@2x.png" alt=""></router-link></template> <el-menu-item index="2-1">我的账户</el-menu-item> <el-menu-item index="2-2">我的订单</el-menu-item> <el-menu-item index="2-3">我的优惠卷</el-menu-item> <el-menu-item index="2-3"><span @click="logoutHandle">退出登录</span></el-menu-item> </el-submenu> </el-menu> </div> </div> <div v-show="!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="/login"><span>登录</span></router-link> | <span>注册</span> </div> </div> </div> </div> </div> </template> <script> export default { name: "Header", data(){ return{ token: "", nav_list:[], } }, created(){ this.token = this.$settings.check_user_login(); this.get_nav_list(); }, methods:{ get_nav_list(){ // 获取导航 this.$axios.get(`${this.$settings.Host}/home/nav/header/`).then(response=>{ this.nav_list = response.data; }).catch(error=>{ console.log(error.response); }); }, logoutHandle(){ this.token = ""; // 退出登录处理 localStorage.removeItem("user_token"); localStorage.removeItem("user_id"); localStorage.removeItem("user_name"); sessionStorage.removeItem("user_token"); sessionStorage.removeItem("user_id"); sessionStorage.removeItem("user_name"); // 提示 this.$message.success("退出登录成功!"); } } } </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; } .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 .login-box1{ margin-top: 25px; } .header .login-bar .login-box2{ margin-top: 32px; } .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; } .el-menu.el-menu--horizontal{ border-bottom: none; } .study-ceneter{ display: inline-block; height: 34px; line-height: 34px; vertical-align: text-bottom; } </style>
官网: 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子应用下创建验证码视图类,并提供验证码和校验验证码的视图方法。
users/views
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
from rest_framework.views import APIView from rest_framework.response import Response from luffyapi.libs.geetest import GeetestLib from django.conf import settings # Create your views here. class CaptchaAPIView(APIView): """验证码接口""" def get(self,request): """提供生成验证码流水号""" user_id = 'test' gt = GeetestLib(settings.GEETEST["pc_geetest_id"], settings.GEETEST["pc_geetest_key"]) status = gt.pre_process(user_id) # request.session[gt.GT_STATUS_SESSION_KEY] = status # request.session["user_id"] = user_id response_str = gt.get_response_str() return Response(response_str) def post(self,request): """二次验证""" gt = gt = GeetestLib(settings.GEETEST["pc_geetest_id"], settings.GEETEST["pc_geetest_key"]) challenge = request.data.get(gt.FN_CHALLENGE, '') validate = request.data.get(gt.FN_VALIDATE, '') seccode = request.data.get(gt.FN_SECCODE, '') # status = request.session[gt.GT_STATUS_SESSION_KEY] # user_id = request.session["user_id"] # if status: # result = gt.success_validate(challenge, validate, seccode, user_id) # else: result = gt.failback_validate(challenge, validate, seccode) result = {"status": "success"} if result else {"status": "fail"} # if result: # result = {"status": "success"} # else: # result = {"status": "fail"} return Response(result)
settings/dev下写入
GEETEST={ "pc_geetest_id" : "08faa04afc548993b1c79336d1671cc8", "pc_geetest_key" : "42e17e57bb181dfbe02d6faaeb157cf7" }
路由注册:
from django.urls import path,re_path from rest_framework_jwt.views import obtain_jwt_token from . import views urlpatterns = [ # 调用drf-jwt模型提供的视图功能, path("login/", obtain_jwt_token ), path("captcha/", views.CaptchaAPIView.as_view() ), ]
把下载gt3的验证码模块包中的gt.js放置到前端项目中
// 导入gt极验
static/js/gt.js
在src/index.html
<script src="/static/js/gt.js"></script>
Login.vue代码
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<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">密码登录</span> <span @click="login_type=1">短信登录</span> </div> <div class="inp" v-if="login_type==0"> <input v-model="username" type="text" placeholder="用户名 / 手机号码" class="user"> <input v-model="password" type="password" name="" class="pwd" placeholder="密码"> <div id="geetest1"></div> <div class="rember"> <p> <input type="checkbox" class="no" v-model="remember"/> <span>记住密码</span> </p> <p>忘记密码</p> </div> <button class="login_btn" @click="get_captcha">登录</button> <p class="go_login" >没有账号 <span>立即注册</span></p> </div> <div class="inp" v-show="login_type==1"> <input v-model = "username" type="text" placeholder="手机号码" class="user"> <input v-model = "password" type="text" class="pwd" placeholder="短信验证码"> <button id="get_code">获取验证码</button> <button class="login_btn">登录</button> <p class="go_login" >没有账号 <span>立即注册</span></p> </div> </div> </div> </div> </template> <script> export default { name: 'Login', data(){ return { login_type: 0, username:"", password:"", remember:false, } }, methods:{ loginHandler(){ // 登录处理 this.$axios.post(`${this.$settings.Host}/user/login/`,{ username: this.username, password: this.password, }).then(response=>{ console.log(response.data.token); // 判断用户是否勾选了"记住密码" if(this.remember){ // 记住密码 sessionStorage.clear(); localStorage.user_token = response.data.token; localStorage.user_id = response.data.id; localStorage.user_name = response.data.username; }else{ // 不记住密码 localStorage.clear(); sessionStorage.user_token = response.data.token; sessionStorage.user_id = response.data.id; sessionStorage.user_name = response.data.username; } let self = this; this.$alert("欢迎回来!","路飞学城",{ callback(){ // 页面跳转 // this.$router.push("/"); // 使用vue跳转页面到指定地址 self.$router.go(-1); // 使用vue跳转页面返回上一页 } }) }).catch(error=>{ if(error.response.status == 400){ // 登录失败! this.$message.error("登录失败!账号或密码错误!"); }else{ // 其他未知,告知管理员 this.$message.error("登录过程中遇到了未知错误!请联系站点管理员或客服人员!"); }; }) }, get_captcha(){ // 验证用户是否已经填写了账号和密码 if( this.username.length<1 || this.password.length<1){ this.$message.error("对不起,账号或密码不能为空!"); return ; } // 获取验证码 this.$axios.get(`${this.$settings.Host}/user/captcha/`).then(response=>{ let data = JSON.parse(response.data); // 使用initGeetest接口 // 参数1:配置参数 // 参数2:回调,回调的第一个参数验证码对象,之后可以使用它做appendTo之类的事件 initGeetest({ gt: data.gt, challenge: data.challenge, product: "popup", // 产品形式,包括:float,embed,popup。注意只对PC版验证码有效 offline: !data.success // 表示用户后台检测极验服务器是否宕机,一般不需要关注 // 更多配置参数请参见:http://www.geetest.com/install/sections/idx-client-sdk.html#config }, this.handlerPopup); }).catch(error=>{ // 请求失败 this.$message.error("请求验证码错误!请联系网站客服工作人员!"); }) }, handlerPopup(captchaObj){ // 验证码的二次验证码 // 成功的回调 let self = this; captchaObj.onSuccess(function () { var validate = captchaObj.getValidate(); 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.loginHandler(); }else{ // 验证码校验失败 self.$message.error("对不起,验证码校验失败!"); } }); }); // 先删除原来的内容 document.getElementById("geetest1").innerHTML = ""; // 将验证码加到id为geetest1的元素里 captchaObj.appendTo("#geetest1"); } }, }; </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 span:nth-of-type(1){ 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>
前端显示注册页面并调整首页头部和登陆页面的注册按钮的链接。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<template> <div class="box"> <img src="/static/image/Loginbg.3377d0c.jpg" alt=""> <div class="register"> <div class="register_box"> <div class="register-title">注册路飞学城</div> <div class="inp"> <input v-model = "mobile" type="text" placeholder="手机号码" class="user"> <div id="geetest"></div> <input v-model = "sms" type="text" placeholder="输入验证码" class="user"> <button class="register_btn" >注册</button> <p class="go_login" >已有账号 <router-link to="/login">直接登录</router-link></p> </div> </div> </div> </div> </template> <script> export default { name: 'Register', data(){ return { sms:"", mobile:"", validateResult:false, } }, created(){ }, methods:{}, }; </script> <style scoped> .box{ width: 100%; height: 100%; position: relative; overflow: hidden; } .box img{ width: 100%; min-height: 100%; } .box .register { position: absolute; width: 500px; height: 400px; top: 0; left: 0; margin: auto; right: 0; bottom: 0; top: -338px; } .register .register-title{ width: 100%; font-size: 24px; text-align: center; padding-top: 30px; padding-bottom: 30px; color: #4a4a4a; letter-spacing: .39px; } .register-title img{ width: 190px; height: auto; } .register-title p{ font-family: PingFangSC-Regular; font-size: 18px; color: #fff; letter-spacing: .29px; padding-top: 10px; padding-bottom: 50px; } .register_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; } .register_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; } .register_box .title span:nth-of-type(1){ color: #4a4a4a; border-bottom: 2px solid #84cc39; } .inp{ width: 350px; margin: 0 auto; } .inp input{ border: 0; outline: 0; width: 100%; height: 45px; border-radius: 4px; border: 1px solid #d9d9d9; text-indent: 20px; font-size: 14px; background: #fff !important; } .inp input.user{ margin-bottom: 16px; } .inp .rember{ display: flex; justify-content: space-between; align-items: center; position: relative; margin-top: 10px; } .inp .rember p:first-of-type{ font-size: 12px; color: #4a4a4a; letter-spacing: .19px; margin-left: 22px; display: -ms-flexbox; display: flex; -ms-flex-align: center; align-items: center; /*position: relative;*/ } .inp .rember p:nth-of-type(2){ font-size: 14px; color: #9b9b9b; letter-spacing: .19px; cursor: pointer; } .inp .rember input{ outline: 0; width: 30px; height: 45px; border-radius: 4px; border: 1px solid #d9d9d9; text-indent: 20px; font-size: 14px; background: #fff !important; } .inp .rember p span{ display: inline-block; font-size: 12px; width: 100px; /*position: absolute;*/ /*left: 20px;*/ } #geetest{ margin-top: 20px; } .register_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>
前端注册路由router/index.js,代码:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
import Register from "../components/Register" // 配置路由列表 export default new Router({ mode:"history", routes:[ // 路由列表 ... { name:"Register", path: "/register", component:Register, } ] })
修改首页头部的连接:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
# Header.vue <span class="header-register"><router-link to="/register">注册</router-link></span> #Login.vue <p class="go_login" >没有账号 <router-link to="/register">立即注册</router-link></p>
接下来,我们把注册过程中一些注册信息(例如:短信验证码)和session缓存到redis数据库中。
Redis是一款内存缓存数据库,和mysql一样,采用了CS架构[客户端和服务端],Redis也是高性能的数据库,经常用于解决一些缓存、高并发存储数据、大数据的业务场景。
Redis和Mysql不一样,MySQL是关系型数据库,内部有数据库、数据表和字段的概念。而Redis是一种非关系型数据库。
redis介绍
安装django-redis。
pip install django-redis