路飞项目
作者:互联网
3. 环境搭建
3.1 外部依赖
- 注册支付宝的开发者账号[https://open.alipay.com],注册一下账号就可以了,剩下的以后再说
- 注册容联云短信接口平台的账号[https://www.yuntongxun.com/?ly=baidu-pz-p&qd=cpc&cp=ppc&xl=null&kw=10360228]
- 注册保利威视频服务平台的账号[暂时别注册,因为有个7天免费测试期,如果到时候过期了就没法用了,网址:http://www.polyv.net/?f=baiduPZ&utm_term=%E4%BF%9D%E5%88%A9%E5%A8%81]
- 注册gitee[码云]的账号
- 注册阿里云账号,如果可以购买一个服务器和域名, 或者第一次使用的可以申请一个免费外网服务器
- 如果有条件的,可以申请一个域名进行备案[ICP备案和公安部备案],如果没有的话, 可以注册natapp[内网穿透]
3.2 依赖包安装
pip3 install django -i https://pypi.douban.com/simple/ # 注意:在虚拟环境中安装第三方包的时候,不要使用sudo,因为sudo是以管理员身份来安装的,会将安装的东西安装到全局中去,而不是虚拟环境中,并且在linux系统下不要出现中文路径
pip3 install djangorestframework -i https://pypi.douban.com/simple/
pip3 install PymySQL -i https://pypi.douban.com/simple/
pip3 install Pillow -i https://pypi.douban.com/simple/
pip3 install django-redis -i https://pypi.douban.com/simple/
4. 搭建项目
4.1 创建项目
可以使用pycharm-django直接创建django项目
也可以在终端使用命令创建
django-admin startproject luffyapi
4.2 调整目录
打开项目以后,调整目录结构,因为公司使用的结构和平常django是不太一样的
luffy/
├── docs/ # 项目相关资料保存目录
├── luffycity/ # 前端项目目录
├── luffyapi/ # 后端项目目录
├── logs/ # 项目运行时/开发时的代码保存
├── manage.py
├── luffyapi/ # 项目主应用,开发时的代码保存
│ ├── apps/ # 开发者的代码保存目录,以模块[子应用]为目录保存(包)
│ ├── libs/ # 第三方类库的保存目录[第三方组件,模块](包)
│ ├── settings/ #(包)
│ ├── dev.py # 项目开发时的本地配置
│ ├── prod.py # 项目上线时的运行配置
│ ├── test.py # 测试人员使用的配置(咱们不需要)
│ ├── urls.py # 总路由(包)
│ ├── utils/ # 多个模块[子应用]的公共函数类库[自己开发的组件]
└── scripts/ # 保存项目运营时的脚本文件
在编辑开发项目时,必须制定项目目录才能运行,例如,开发后端项目,则必须选择的目录是luffyapi
上面的目录结构图,使用ubuntu的命令tree输出的
如果没有安装tree,可以使用 sudo apt install tree
注意: 创建文件夹的时候,是创建包(憨init.py文件的)还是创建单纯的文件夹,看目录里面放什么,如果放的是py文件相关的代码,最好创建包,如果不是,那就创建单纯的文件夹
4.3 分不同环境进行项目配置
开发者本地的环境,目录,数据库密码和线上的服务器都会不一样,所以我们的配置文件可以针对不同的系统分成多份。
- 在项目主应用下,创建一个settings的配置文件存储目录
- 根据线上线下两种情况分别创建两个配置文件dev.py和prod.py
- 把原来的项目主应用的settings.py配置内容符合两份到dev.py和prod.py里面
- 把原来的settings.py配置文件修改文件名或者删除
接下里在manage.py根据不同的情况导入对应的配置文件
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'luffyapi.settings.dev')
4.4 创建代码版本
cd进入到自己希望存储代码的目录路径,并创建本地仓库.git(pycharm直接打开终端就是项目根目录了,无需cd)
新创建的本地仓库.git是空仓库
git init
git add . 或者文件名 # .代表所有文件
git status # 查看当前项目的版本状态
git commit -m '描述信息' # 可以写版本信息
git push 远程仓库名称 dev(分支名称) # 往远程仓库提交代码
git branch dev # 创建本地分支dev
git checkout dev # 切换到本地分支代码
4.5 配置用户名和邮箱(码云账号邮箱)
可以先注册码云
git config --global user.name '账号'
git config --global user.email '邮箱'
4.6 在gitee平台上创建仓库
公司一般都有自己的代码仓库,一般都是自己搭建,也有使用第三方提供的代码管理平台
常用的代码管理平台: github,gitee(码云),codepen
若果公司自己搭建的代码管理平台:gitlab框架
4.6.1创建一个公有仓库
创建仓库后的界面
仓库地址要选择HTTPS
接下来,我们就可以把本地新建好的项目提交到gitee码云上了
# .表示当前目录下所有的文件或目录提交到上传队列[上传队列也叫"暂存区"]
git add .
# 把本地上传队列的代码提交到本地仓库
git commit -m "项目描述"
# 给本地的git版本控制软件设置项目的远程仓库地址
git remote add origin https://gitee.com/cloud_chaoy/qunyyasha.git
# 提交代码给远程仓库
git push -u origin master
扩展:
git status 可以查看当前项目的代码版本状态
git reflog 可以查看代码版本日志[简单格式]
git log 可以查看代码版本日志[详细格式]
git branch -D 分支名称
删除分支时,必须切换到别的分支上才能进行删除
上面虽然成功移交了代码版本,但是一些不需要的文件也被提交上去了,所以我们针对一些不要的文件,可以选择从代码版本中删除,并且使用.gitignore把这些垃圾文件过滤掉
git rm 文件 # 删除单个文件
git rm -rf 目录 # 递归删除目录
# 以下操作建议通过终端来完成,不要使用pycharm提供,否则删除.idea还会继续生成。
git rm -rf .idea
git rm db.sqlite3
# 注意,上面的操作只是从项目的源代码中删除,但是git是不知情的,所以我们需要同步。
git add .
git commit -m "删除不必要的文件或目录"
git push -u origin master
使用.gitignore
把一些垃圾文件过滤掉
vim .gitignore
index.html
.gitignore
./lyapi/idea
./lyapi/idea/*
./git
./lyapi/db.sqlite3
4.6.2 克隆项目到本地
注意:
克隆只用在当我们进入一家新公司的时候,参与人家已经在做的项目,人家已经有仓库了,但我们新加入到项目中,这时我们就可以执行 git clone 直接复制别人的仓库代码
如果当前目录下出现git仓库同名目录时,会克隆失败
4.7 日志配置
django官方文档
在settings/dev.py文件中追加如下配置:
# 日志配置
LOGGING = {
'version': 1, #使用的python内置的logging模块,那么python可能会对它进行升级,所以需要写一个版本号,目前就是1版本
'disable_existing_loggers': False, #是否去掉目前项目中其他地方中以及使用的日志功能,但是将来我们可能会引入第三方的模块,里面可能内置了日志功能,所以尽量不要关闭。
'formatters': { #日志记录格式
'verbose': { #levelname等级,asctime记录时间,module表示日志发生的文件名称,lineno行号,message错误信息
'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': { #在debug=True下产生的一些日志信息,要不要记录日志,需要的话就在handlers中加上这个过滤器,不需要就不加
'()': 'django.utils.log.RequireDebugTrue',
},
'require_debug_false': { #和上面相反
'()': 'django.utils.log.RequireDebugFalse',
},
},
'handlers': { #日志处理方式,日志实例
'console': { #在控制台输出时的实例
'level': 'DEBUG', #日志等级;debug是最低等级,那么只要比它高等级的信息都会被记录
'filters': ['require_debug_true'], #在debug=True下才会打印在控制台
'class': 'logging.StreamHandler', #使用的python的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',
'encoding': 'utf-8', # 设置默认编码,否则打印出来汉字乱码
},
},
# 日志对象
'loggers': {
'django': { #和django结合起来使用,将django中之前的日志输出内容的时候,按照我们的日志配置进行输出,
'handlers': ['console', 'file'], #将来项目上线,把console去掉
'propagate': True, #冒泡:是否将日志信息记录冒泡给其他的日志处理系统,工作中都是True,不然django这个日志系统捕获到日志信息之后,其他模块中可能也有日志记录功能的模块,就获取不到这个日志信息了
},
}
}
4.8 异常处理
新建utils/execptions.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.py配置文件中添加
REST_FRAMEWORK = {
# 异常处理
'EXCEPTION_HANDLER': 'luffyapi.utils.exceptions.custom_exception_handler',
}
4.9 创建数据库
create database luffy default charset=utf8mb4; -- utf8也会导致有些极少的中文出现乱码的问题,mysql5.5之后官方才进行处理,出来了utf8mb4,这个是真正的utf8,能够容纳所有的中文,其实一般情况下utf8就够用了。
为当前目录创建数据库用户(这个用户只能看到这个数据库)
create user chao identified by '123';
grant all privileges on luffy.* to 'chao'@'%';
flush privileges;
mysql -u chao -p123
select user(); #chao
4.9.1 配置数据库连接
在settings/dev.py文件中配置
DATABASES = {
"default": {
"ENGINE": "django.db.backends.mysql",
"HOST": "127.0.0.1",
"PORT": 3306,
"USER": "chao",
"PASSWORD": "123",
"NAME": "luffy",
}
}
在项目主模块的__init__.py
中导入pymysql
import pymysql
pymysql.install_as_MySQLdb()
5. 前端项目初始化
cd到路飞项目下创建一个luffcity前端项目
vue init webpack luffycity
在src目录下创建settings.js站点开发配置文件:
export default {
Host:"http://www.luffyapi.com:8000", // 后台接口
}
sudo vim /etc/hosts/
127.0.0.1 localhost
127.0.1.1 ubuntu
# The following lines are desirable for IPv6 capable hosts
::1 ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
127.0.0.1 www.luffycc.com
127.0.0.1 www.luffyapi.com
然后到后端luffyapi中,设置manage.py
runserver www.luffyapi.com:8000
前端luffycity中,main.js
// 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 './router'
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/>'
})
Edit Configurations 添加 npm 设置 dev
引入elementUI
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import axios from 'axios'
Vue.use(ElementUI);
复制组件和图片资源
src里的router配置一下,把没用的hellword删掉,加mode:'history'去掉路径里的#
config/index.js里面的 host改一下 : www.luffycc.com
在static/css/style.css 里写全局css样式
在main.js中引入一下
import '../static/css/style.css'
6. cors跨域
在settings/dev.py里面
# 设置哪些客户端可以通过地址访问到后端
ALLOWED_HOSTS = ['www.luffyapi.com','www.luffycc.com']
# 自己的客户端网址也要设置,将来要访问到服务端
现在,前端与后端分出不同的域名,我们需要为后端添加跨域访问的支持否则前端无法使用axios请求后端提供的api数据,可以使用CORS来解决后端对跨域访问的支持
使用django-cors-headers扩展
Response(headers={"Access-Control-Allow-Origin":'客户端地址'})
文档:https://github.com/ottoyiu/django-cors-headers/
安装
pip3 install django-cors-headers
添加应用
INSTALLED_APPS = (
...
'corsheaders',
...
)
中间件设置(必须写在第一个位置)
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware', #放在中间件的最上面,就是给响应头加上了一个响应头跨域
...
]
需要添加白名单,确定一下哪些客户端可以跨区
# CORS组的配置信息
CORS_ORIGIN_WHITELIST = (
#'www.luffycity.cn:8080', #如果这样写不行的话,就加上协议(http://www.luffycity.cn:8080,因为不同的corsheaders版本可能有不同的要求)
'http://www.luffycc.com:8080', # 一定要加逗号啊
)
CORS_ALLOW_CREDENTIALS = False # 是否允许ajax跨域请求时携带cookie,False表示不用,我们后面也用不到cookie,所以关掉它就可以了,以防有人通过cookie来搞我们的网站
前端引入axios插件
npm i axios -S --registry https://registry.npm.taobao.org
在main.js中引用axios
import axios from 'axios'; // 从node_modules目录中导入包
// 客户端配置是否允许ajax发送请求时附带cookie,false表示不允许
axios.defaults.withCredentials = false;
Vue.prototype.$axios = axios; // 把对象挂载vue中
如果你拷贝前端vue-cli项目到指定目录下,运行有问题,报一些不知名的错误,那么就删除node_modules
文件夹,然后在项目根目录下执行npm install
,重新按照package.json文件夹中的包进行node_modules里面包的下载。
都设置好后 项目启动没有问题
cd 到项目目录
git add .
git commit -m 'v1 初始化项目'
git log
git push origin master # 推到远程仓库上
7. 轮播图功能实现
7.1 安装依赖模块和配置
后端
图片处理模块
pip3 install pillow
上传文件相关配置
settings.py,由于我们需要在后台上传轮播图图片,所以需要在django中配置一下上传文件的相关配置,有了它以后,就不需要我们自己写上传文件和保存文件的操作了
# 访问静态文件的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/"
总路由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}),
]
7.2 注册home子应用
因为当前功能是drf的第一个功能,所以我们先创建一个子应用home,创建在luffyapi/apps目录下
python3 ../../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使我们可以直接import导入时使用到的路径,所以我们直接将我们的apps路径加到默认搜索路径里面去,那么django就能直接找到apps下面的应用了
sys.path.insert(0,os.path.join(BASE_DIR,"apps"))
INSTALLED_APPS = [
# 注意,加上drf框架的注册
'rest_framework',
# 子应用
'home',
]
注意,pycharm会路径错误的提示。可以鼠标右键设置apps为 mark dir.... as source root,不推荐,因为这是pycharm提供的。
7.3 新建开发分支进行独立开发
接下来,我们完成的功能[轮播图]这些,建议采用开发分支来完成,所以我们可以通过以下命令,复刻一份代码[也就是新建一个分支]出来进行独立开发.这样的话,就不会影响到线上的主干代码!!!
# 新建一个分支
git branch 分支名称
# 查看所有分支
git branch
# 切换分支[-b表示新建分支的同时并切换到新分支]
git checkout -b 分支名称
# 删除分支
git branch -d 分支名称
接下来,我们可以创建一个dev开发分支并在开发分支下干活!
git branch dev
git checkout dev
7.4 创建轮播图的模型
home/models.py
from django.db import models
# Create your models here.
class Banner(models.Model):
"""轮播广告图模型"""
# 模型字段
title = models.CharField(max_length=500, verbose_name="广告标题")
link = models.CharField(max_length=500, verbose_name="广告链接")
# upload_to 设置上传文件的保存子目录,将来上传来的文件会存到我们的media下面的banner文件夹下,这里存的是图片地址。
image_url = models.ImageField(upload_to="banner", null=True, blank=True, max_length=255, verbose_name="广告图片")
remark = models.TextField(verbose_name="备注信息")
is_show = models.BooleanField(default=False, verbose_name="是否显示") #将来轮播图肯定会更新,到底显示哪些
orders = models.IntegerField(default=1, verbose_name="排序")
is_deleted = models.BooleanField(default=False, verbose_name="是否删除")
# 表信息声明
class Meta:
db_table = "ly_banner"
verbose_name = "轮播广告"
verbose_name_plural = verbose_name
# 自定义方法[自定义字段或者自定义工具方法]
def __str__(self):
return self.title
数据迁移指令
python manage.py makemigrations
python manage.py migrate
7.4.1 序列化器
home/serializers.py(自己创建一个)
from rest_framework import serializers
from . import models
class BannerModelSerializer(serializers.ModelSerializer):
""" 轮播广告的序列化器 """
class Meta:
model = models.Banner
fields = ['id','image_url','link']
7.4.2 视图代码
views.py
from django.shortcuts import render
from rest_framework.generics import ListAPIView
# Create your views here.
from . import models
from luffyapi.settings import contains
from .serializers import BannerModelSerializer,NavModelSerializer
class BannerView(ListAPIView):
queryset = models.Banner.objects.filter(is_deleted=False,is_show=True)[0:contains.BANNER_LENGTH] #没有必要获取所有图片数据,因为有些可能是删除了的或者不显示的
# 切片获取数据的时候,我们可以将切片长度设置成常熟默认配置项,用来控制前端的页面展示效果
serializer_class = BannerModelSerializer
在settings下新建一个contains.py 的文件存放我们所有的一些常量信息配置
# 首页展示的轮播图广告数量
BANNER_LENGTH = 3
# 顶部导航的数量
HEADER_NAV_LENGTH = 5
# 脚部导航的数量
FOOTER_NAV_LENGTH = 7
7.4.3 路由代码
home/urls.py
from django.urls import path
from . import views
urlpatterns = [
path(r'banner/',views.BannerView.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") ),
]
8. Xadmin
我们还需要有一个后台提供数据管理操作,安装xadmin,他的功能要比django默认的admin的功能更强大一点
pip3 install https://codeload.github.com/sshwsfc/xadmin/zip/django2 -i https://pypi.douban.com/simple/
在配置文件中注册如下应用
INSTALLED_APPS = [
...
'xadmin',
'crispy_forms',
'reversion',
...
]
# 修改使用中文界面
LANGUAGE_CODE = 'zh-Hans'
# 修改时区
TIME_ZONE = 'Asia/Shanghai'
xadmin有建立自己的数据库模型类,需要进行数据库迁移
python manage.py makemigrations
python manage.py migrate
8.1在总路由中添加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
8.2 给xadmin设置基本站点配置信息
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)
8.4 注册轮播图模型到xadmin中
在当前子应用中创建adminx.py
import xadmin
from xadmin import views
from . import models
class BannerXAdmin(object):
list_display = ['id','title','link','image_url']
search_fields = ['id','title']
ordering = ['-id']
xadmin.site.register(models.Banner, BannerXAdmin)
8.5 修改后端xadmin中子应用名称
home/apps.py
class HomeConfig(AppConfig):
name = 'home'
verbose_name = '我的首页'
在home这app下面的__init__.py
中设置
default_app_config = "home.apps.HomeConfig"
手动在xadmin中把,轮播图图片信息添加进去
8.6 客户端获取后端数据
Banner.vue代码
<template>
<el-carousel indicator-position="outside" height="400px">
<el-carousel-item v-for="(value,index) in banner_list" :key="value.id">
<!-- <router-link :to="value.link">-->
<a :href="value.link">
<img :src="value.image_url" alt="" style="width: 100%;height: 400px;">
<!-- <img src="@/assets/banner1.png" alt="">-->
<!-- </router-link>-->
</a>
</el-carousel-item>
</el-carousel>
</template>
<script>
export default {
name: "Banner",
data(){
return {
banner_list:[
]
}
},
methods:{
get_banner_data(){
this.$axios.get(`${this.$settings.Host}/home/banner`)
.then((res)=>{
console.log(res);
this.banner_list = res.data
})
.catch((error)=>{
})
}
},
created(){
this.get_banner_data();
},
}
</script>
<style scoped>
</style>
9. 导航功能实现
9.1 创建模型
引入一个公共模型(抽象模型,不会在数据迁移的时候为它创建表)
from django.db import models
# Create your models here.
from django.db import models
class BaseModel(models.Model):
"""公共模型"""
is_show = models.BooleanField(default=False, verbose_name="是否显示")
orders = models.IntegerField(default=1, verbose_name="排序")
is_deleted = models.BooleanField(default=False, verbose_name="是否删除")
created_time = models.DateTimeField(auto_now_add=True, verbose_name="添加时间")
updated_time = models.DateTimeField(auto_now=True, verbose_name="修改时间")
#更新:update方法不能自动更新auto_now的时间,save()方法保存能够自动修改更新时间
class Meta:
# 设置当前模型为抽象模型,在数据迁移的时候django就不会为它单独创建一张表
abstract = True
# Create your models here.
class Banner(models.Model):
"""轮播广告图模型"""
# 模型字段
title = models.CharField(max_length=500, verbose_name="广告标题")
link = models.CharField(max_length=500, verbose_name="广告链接")
# upload_to 设置上传文件的保存子目录,将来上传来的文件会存到我们的media下面的banner文件夹下,这里存的是图片地址。
image_url = models.ImageField(upload_to="banner", null=True, blank=True, max_length=255, verbose_name="广告图片")
remark = models.TextField(verbose_name="备注信息")
is_show = models.BooleanField(default=False, verbose_name="是否显示") #将来轮播图肯定会更新,到底显示哪些
orders = models.IntegerField(default=1, verbose_name="排序")
is_deleted = models.BooleanField(default=False, verbose_name="是否删除")
# 表信息声明
class Meta:
db_table = "ly_banner"
verbose_name = "轮播广告"
verbose_name_plural = verbose_name
# 自定义方法[自定义字段或者自定义工具方法]
def __str__(self):
return self.title
class Nav(BaseModel):
"""导航菜单模型"""
POSITION_OPTION = (
(1, "顶部导航"),
(2, "脚部导航"),
)
title = models.CharField(max_length=500, verbose_name="导航标题")
link = models.CharField(max_length=500, verbose_name="导航链接")
position = models.IntegerField(choices=POSITION_OPTION, default=1, verbose_name="导航位置")
is_site = models.BooleanField(default=False, verbose_name="是否是站外地址")
class Meta:
db_table = 'luffy_nav'
verbose_name = '导航菜单'
verbose_name_plural = verbose_name
# 自定义方法[自定义字段或者自定义工具方法]
def __str__(self):
return self.title
数据迁移指令
python manage.py makemigrations
python manage.py migrate
9.2 序列化器
home/serializers.py
class NavModelSerializer(serializers.ModelSerializer):
""" 导航栏序列化器 """
class Meta:
model = models.Nav
fields = ['id','title','link','position','is_site']
9.3 视图
home/views.py
from django.shortcuts import render
from rest_framework.generics import ListAPIView
# Create your views here.
from . import models
from luffyapi.settings import contains
from .serializers import BannerModelSerializer,NavModelSerializer
class BannerView(ListAPIView):
queryset = models.Banner.objects.filter(is_deleted=False,is_show=True)[0:contains.BANNER_LENGTH]
serializer_class = BannerModelSerializer
# 获取顶部导航栏需要的数据
class NavView(ListAPIView):
queryset = models.Nav.objects.filter(is_deleted=False,is_show=True,position=1)[0:contains.HEADER_NAV_LENGTH]
serializer_class = NavModelSerializer
# 获取底部导航栏需要的数据
class BottomNavView(ListAPIView):
queryset = models.Nav.objects.filter(is_deleted=False,is_show=True,position=2)[0:contains.FOOTER_NAV_LENGTH]
serializer_class = NavModelSerializer
常量配置
settings/contains.py
# 首页展示的轮播广告数量
BANNER_LENGTH = 3
# 顶部导航的数量
HEADER_NAV_LENGTH = 5
# 脚部导航的数量
FOOTER_NAV_LENGTH = 7
9.4 路由
home/urls.py
from django.urls import path
from . import views
urlpatterns = [
path(r'banner/',views.BannerView.as_view()),
path(r'nav/',views.NavView.as_view()),
path(r'nav/bottom/', views.BottomNavView.as_view())
]
总路由
from django.contrib import admin
from django.urls import path,include,re_path
from django.conf import settings
from django.views.static import serve
import xadmin
xadmin.autodiscover()
# version模块自动注册需要版本控制的 Model
from xadmin.plugins import xversion
xversion.register_models()
urlpatterns = [
path(r'xadmin/', xadmin.site.urls),
re_path(r'media/(?P<path>.*)', serve, {"document_root": settings.MEDIA_ROOT}),
path(r'home/', include('home.urls')),
path(r'users/', include('users.urls')),
]
9.5 xadmin中注册导航栏模型
home/adminx.py
class NavXAdmin(object):
list_display = ['id','title','link','position','is_site']
xadmin.site.register(models.Nav,NavXAdmin)
9.6 前端获取后端数据
Header.vue
<template>
<div class="total-header">
<div class="header">
<el-container>
<el-header height="80px" class="header-cont">
<el-row>
<el-col class="logo" :span="3">
<a href="/">
<img src="@/assets/head-logo.svg" alt="">
</a>
</el-col>
<el-col class="nav" :span="10">
<el-row>
<!-- <el-col :span="3"> <router-link to="/course/" class="active">免费课</router-link> </el-col>-->
<!-- <el-col :span="3"> <router-link to="/">轻课</router-link> </el-col>-->
<!-- <el-col :span="3"> <router-link to="/">学位课</router-link> </el-col>-->
<!-- <el-col :span="3"> <router-link to="/">题库</router-link> </el-col>-->
<!-- <el-col :span="3"> <router-link to="/">教育</router-link> </el-col>-->
<el-col :span="3" v-for="(value,index) in nav_list" :key="value.id">
<router-link v-if="!value.is_site" :to="value.link" :class="{active:count===index}" @click="count=index">{{value.title}}</router-link>
<a v-else="" :href="value.link" :class="{active:count===index}">{{value.title}}</a>
</el-col>
</el-row>
</el-col>
<el-col :span="11" class="header-right-box">
<div class="search">
<input type="text" id="Input" placeholder="请输入想搜索的课程" style="" @blur="inputShowHandler" ref="Input" v-show="!s_status">
<ul @click="ulShowHandler" v-show="s_status" class="search-ul">
<span>Python</span>
<span>Linux</span>
</ul>
<p>
<img class="icon" src="@/assets/sousuo1.png" alt="" v-show="s_status">
<img class="icon" src="@/assets/sousuo2.png" alt="" v-show="!s_status">
<img class="new" src="@/assets/new.png" alt="">
</p>
</div>
<div class="register" v-show="!token">
<router-link to="/user/login"><button class="signin">登录</button></router-link>
|
<!-- <a target="_blank" href="">-->
<router-link to="/"><button class="signup">注册</button></router-link>
<!-- </a>-->
</div>
<div class="shop-car" v-show="token">
<router-link to="/">
<b>6</b>
<img src="@/assets/shopcart.png" alt="">
<span>购物车 </span>
</router-link>
</div>
<div class="nav-right-box" v-show="token">
<div class="nav-right">
<router-link to="/">
<div class="nav-study">我的教室</div>
</router-link>
<div class="nav-img" @mouseover="personInfoList" @mouseout="personInfoOut">
<img src="@/assets/touxiang.png" alt="" style="border: 1px solid rgb(243, 243, 243);">
<!-- hover -- mouseenter+mouseout-->
<ul class="home-my-account" v-show="list_status">
<li>
我的账户
<img src="https://hcdn1.luffycity.com/static/frontend/activity/back_1568185800.821227.svg" alt="">
</li>
<li>
我的订单
<img src="https://hcdn1.luffycity.com/static/frontend/activity/back_1568185800.821227.svg" alt="">
</li>
<li>
贝里小卖铺
<img src="https://hcdn1.luffycity.com/static/frontend/activity/back_1568185800.821227.svg" alt="">
</li>
<li>
我的优惠券
<img src="https://hcdn1.luffycity.com/static/frontend/activity/back_1568185800.821227.svg" alt="">
</li>
<li>
<span>
我的消息
<b>(26)</b>
</span>
<img src="https://hcdn1.luffycity.com/static/frontend/activity/back_1568185800.821227.svg" alt="">
</li>
<li @click="logout">
退出
<img src="https://hcdn1.luffycity.com/static/frontend/activity/back_1568185800.821227.svg" alt="">
</li>
</ul>
</div>
</div>
</div>
</el-col>
</el-row>
</el-header>
</el-container>
</div>
</div>
</template>
<script>
export default {
name: "Header",
data(){
return {
// 设置一个登录状态的标记,因为登录注册部分在登录之后会发生变化,false未登录转台
token:false,
s_status:true,
list_status:false, //用来控制个人中心下拉菜单的动态显示,false不显示
nav_list:[],
count:0,
}
},
methods:{
get_nav_data(){
this.$axios.get(`${this.$settings.Host}/home/nav`)
.then((res)=>{
console.log(res)
this.nav_list = res.data
})
.catch((error)=>{
})
},
ulShowHandler(){
this.s_status = false;
console.log(this.$refs.Input);
// this.$refs.Input.focus();
this.$nextTick(()=>{ //延迟回调方法,Vue中DOM更新是异步的,也就是说让Vue去显示我们的input标签的操作是异步的,如果我们直接执行this.$refs.Input.focus();是不行的,因为异步的去显示input标签的操作可能还没有完成,所有我们需要等它完成之后在进行DOM的操作,需要借助延迟回调对DOM进行操作,这是等这次操作对应的所有Vue中DOM的更新完成之后,在进行nextTick的操作。
this.$refs.Input.focus();
})
},
inputShowHandler(){
console.log('xxxxx')
this.s_status = true;
},
personInfoList(){
this.list_status = true;
},
personInfoOut(){
this.list_status = false;
},
check_login(){
this.token = localStorage.token || sessionStorage.token;
},
// 退出登录
logout(){
sessionStorage.removeItem('token');
sessionStorage.removeItem('username');
sessionStorage.removeItem('id');
localStorage.removeItem('token');
localStorage.removeItem('username');
localStorage.removeItem('id');
this.check_login();
},
},
created(){
this.get_nav_data();
this.check_login();
},
}
</script>
<style scoped>
.header-cont .nav .active{
color: #f5a623;
font-weight: 500;
border-bottom: 2px solid #f5a623;
}
.total-header{
min-width: 1200px;
z-index: 100;
box-shadow: 0 4px 8px 0 hsla(0,0%,59%,.1);
}
.header{
width: 1200px;
margin: 0 auto;
}
.header .el-header{
padding: 0;
}
.logo{
height: 80px;
/*line-height: 80px;*/
/*text-align: center;*/
display: flex; /* css3里面的弹性布局,高度设定好之后,设置这个属性就能让里面的内容居中 */
align-items: center;
}
.nav .el-row .el-col{
height: 80px;
line-height: 80px;
text-align: center;
}
.nav a{
font-size: 15px;
font-weight: 400;
cursor: pointer;
color: #4a4a4a;
text-decoration: none;
}
.nav .el-row .el-col a:hover{
border-bottom: 2px solid #f5a623
}
.header-cont{
position: relative;
}
.search input{
width: 185px;
height: 26px;
font-size: 14px;
color: #4a4a4a;
border: none;
border-bottom: 1px solid #ffc210;
outline: none;
}
.search ul{
width: 185px;
height: 26px;
display: flex;
align-items: center;
padding: 0;
padding-bottom: 3px;
border-bottom: 1px solid hsla(0,0%,59%,.25);
cursor: text;
margin: 0;
font-family: Helvetica Neue,Helvetica,Microsoft YaHei,Arial,sans-serif;
}
.search .search-ul,.search #Input{
padding-top:10px;
}
.search ul span {
color: #545c63;
font-size: 12px;
padding: 3px 12px;
background: #eeeeef;
cursor: pointer;
margin-right: 3px;
border-radius: 11px;
}
.hide{
display: none;
}
.search{
height: auto;
display: flex;
}
.search p{
position: relative;
margin-right: 20px;
margin-left: 4px;
}
.search p .icon{
width: 16px;
height: 16px;
cursor: pointer;
}
.search p .new{
width: 18px;
height: 10px;
position: absolute;
left: 15px;
top: 0;
}
.register{
height: 36px;
display: flex;
align-items: center;
line-height: 36px;
}
.register .signin,.register .signup{
font-size: 14px;
color: #5e5e5e;
white-space: nowrap;
}
.register button{
outline: none;
cursor: pointer;
border: none;
background: transparent;
}
.register a{
color: #000;
outline: none;
}
.header-right-box{
height: 100%;
display: flex;
align-items: center;
font-size: 15px;
color: #4a4a4a;
position: absolute;
right: 0;
top: 0;
}
.shop-car{
width: 99px;
height: 28px;
border-radius: 15px;
margin-right: 20px;
background: #f7f7f7;
display: flex;
align-items: center;
justify-content: center;
position: relative;
cursor: pointer;
}
.shop-car b{
position: absolute;
left: 28px;
top: -1px;
width: 18px;
height: 16px;
color: #fff;
font-size: 12px;
font-weight: 350;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
background: #ff0826;
overflow: hidden;
transform: scale(.8);
}
.shop-car img{
width: 20px;
height: 20px;
margin-right: 7px;
}
.nav-right-box{
position: relative;
}
.nav-right-box .nav-right{
float: right;
display: flex;
height: 100%;
line-height: 60px;
position: relative;
}
.nav-right .nav-study{
font-size: 15px;
font-weight: 300;
color: #5e5e5e;
margin-right: 20px;
cursor: pointer;
}
.nav-right .nav-study:hover{
color:#000;
}
.nav-img img{
width: 26px;
height: 26px;
border-radius: 50%;
display: inline-block;
cursor: pointer;
}
.home-my-account{
position: absolute;
right: 0;
top: 60px;
z-index: 101;
width: 190px;
height: auto;
background: #fff;
border-radius: 4px;
box-shadow: 0 4px 8px 0 #d0d0d0;
}
li{
list-style: none;
}
.home-my-account li{
height: 40px;
font-size: 14px;
font-weight: 300;
color: #5e5e5e;
padding-left: 20px;
padding-right: 20px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
}
.home-my-account li img{
cursor: pointer;
width: 5px;
height: 10px;
}
.home-my-account li span{
height: 40px;
display: flex;
align-items: center;
}
.home-my-account li span b{
font-weight: 300;
margin-top: -2px;
}
</style>
10. 用户的登录认证
10.1 前端显示登陆页面
Login.vue
<template>
<div class="login box">
<img src="../../static/img/Loginbg.3377d0c.jpg" alt="">
<div class="login">
<div class="login-title">
<img src="../../static/img/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" v-model="remember"/>
<span>记住密码</span>
</p>
<p>忘记密码</p>
</div>
<button class="login_btn" @click="loginHandle">登录</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:'',
}
},
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;
top: 0;
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-family: PingFangSC-Regular;
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{
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;
}
.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>
10.2 绑定登陆页面路由
main.js
import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/components/Home'
import Login from '@/components/Login'
Vue.use(Router)
export default new Router({
mode : 'history' ,
routes: [
...
{
path: '/user/login',
name: '',
component: Login,
},
],
})
调整Home.vue中头部子组件Vheader.vue的登陆按钮的链接地址
Header.vue
<router-link to="/user/login">登录</router-link>
10.3 后端实现登陆认证
Django默认已经提供了认证系统Auth模块,我们认证的时候,会使用auth模块里面给我们提供的表,认证系统包含:
- 用户管理
- 权限
- 用户组
- 密码哈希系统
- 用户登录或内容显示的表单和视图
- 一个可插拔的后台系统admin
Django默认用户的认证机制依赖Session机制,我们在项目中将引入JWT认证机制,将用户的身份凭据存放在Token中,然后对接Django的认证系统实现:
- 用户的数据模型
- 用户密码的加密与验证
- 用户的权限系统
10.4 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
创建用户模块的子应用
python manage.py startapp users
在settings.py文件中注册子应用
INSTALLED_APPS = [
...
'users',
]
10.5 创建自定义的用户模型类
Django认证系统中提供的用户模型类及方法很方便,我们可以使用这个模型,但是字段有些无法满足项目需求,如本项目中需要保存用户的手机号,则需要给模型类添加额外的字段
Django提供了django.contrib.auth.models.AbstractUser
用户抽象模型类允许我们继承并扩展字段来使用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):
phone = models.CharField(max_length=16,null=True,blank=True)
wechat = models.CharField(max_length=16,null=True,blank=True)
class Meta:
db_table = 'luffy_user'
verbose_name = '用户表'
verbose_name_plural = verbose_name
我们自定义的用户模型类还不能直接被Django的认证系统所识别,需要在配置文件中告知Django认证系统使用我们自定义的模型类
配置文件中设置settings/dev.py
#注册自定义用户模型,格式:“应用名.模型类名”
AUTH_USER_MODEL = 'users.User'
AUTH_USER_MODEL
参数的设置以.
来分隔,表示应用名.模型类名
注意: Django建议我们对于AUTH_USER_MODEL参数的设置一定要在第一次数据库迁移之前就设置好,否则后续使用可能出现位置错误
执行数据库迁移
python manage.py makemigrations
python manage.py migrate
如果在第一次数据迁移之后,才设置AUTH_USER_MODEL自定义用户模型,则会报错,解决方案如下
1. 先把现有的数据库导出备份 Dump with 'mysqldump' , 然后清掉数据库中所有的数据表
2. 把开发者穿件的所有子应用下面的migrations目录下除了__init__.py以外的所有迁移文件,只要涉及到用户的,一律删除,并将django-migrations表中的数据全部删除
3. 把django.contrib.admin.migrations目录下除了__init__.py以外的所有迁移文件,全部删除。
4. 把django.contrib.auth.migrations目录下除了__init__.py以外的所有迁移文件,全部删除。
5. 把reversion.migrations目录下除了__init__.py以外的所有迁移文件,全部删除。这个不在django目录里面,在site-packages里面,是xadmin安装的时候带的,它会记录用户信息,也需要删除
6. 把xadmin.migrations目录下除了__init__.py以外的所有迁移文件,全部删除。
7. 删除我们当前数据库中的所有表
8. 接下来,执行数据迁移(makemigrations和migrate),回顾第0步中的数据,将数据导入就可以了,以后如果要修改用户相关数据,不需要重复本次操作,直接数据迁移即可。
11. Django REST framework JWT
在用户注册或登录后,我们想记录用户的登录状态,或者为用户创建身份认证的凭证,我们不再使用session认证机制,而使用Json Web Token认证机制
很多公司开发的一些移动端可能不支持cookie,并且我们通过cookie和session做接口认证的话,效率其实并不是很高,我们的接口可能提供给给多个客户端,session数据保存在服务端,那么就需要每次调用session数据进行校验,比较耗时,所以引入了token认证
Json Web token(JWT),是为了在网络应用环境间传递声明执行的一种基于JSON的开放标准(RFC 7519),该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景,JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其他业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密
11.1 JWT的构成
JWT就是一段字符串,由三段信息构成,将这三段信息文本用.
连接在一起就构成了jwt字符串
如:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
header
jwt的头部承载两部分信息:
- 声明类型: jwt
- 声明加密的算法: 通常直接使用
HMAC SHA256
完整的头部就像这样的Json数据
{
'typ': 'JWT',
'alg': 'HS256'
}
然后将头部进行base64.b64encode()
加密(该加密是可以对称解密的),构成了第一部分
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
python3中base64加密解密
import base64
str1 = 'admin'
str2 = str1.encode()
b1 = base64.b64encode(str2) #数据越多,加密后的字符串越长
b2 = base64.b64decode(b1) #admin
各个语言中都有base64加密解密的功能,所以我们jwt为了安全,需要配合第三段加密
payload
载荷就是存放有效信息的地方
- 标准中注册的声明
- 公共的声明
- 私有的声明
标准中注册的声明 (建议但不强制使用) :
-
iss: jwt签发者
-
sub: jwt所面向的用户
-
aud: 接收jwt的一方
-
exp: jwt的过期时间,这个过期时间必须要大于签发时间
-
nbf: 定义在什么时间之前,该jwt都是不可用的.
-
iat: jwt的签发时间
-
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
以上是JWT 规定的7个官方字段,供选用
公共声明:公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息,但不建议添加敏感信息,因为该部分在客户端可解密
私有的声明:私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息
定义一个payload,json格式的数据
{
"sub": "1234567890",
"exp": "3422335555", #时间戳形式
"name": "John Doe",
"admin": true
}
然后将其进行base64.b64encode()
加密,得到JWT的第二部分
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
signature
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'); //xxxx // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
将这三部分用.
连接成一个完整的字符串,构成了最终的JWT
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证的,所以,他就是你服务端的私钥,在任何场景都不应该流露出去,一旦客户端得知这个secret,就意味着客户端可以自我签发jwt了
jwt的优点:
1. 实现分布式的单点登录非常方便
2. 数据实际保存在客户端,可以分担服务器的存储压力
3. JWT不仅可用于认证,还可用于信息交换,善用JWT有助于减少服务器请求数据库的次数,jwt的构成非常简单,字节占用很小,所以它非常便于传输
jwt的缺点:
1. 数据保存在客户端,服务端只认jwt,不识别客户端
2. jwt可以设置过期时间,但是因为数据保存在了客户端,所以对于过期时间不好调整。 secret_key轻易不要改,一改所有客户端都要重新登录
11.2 安装配置JWT
安装
pip install djangorestframework-jwt -i https://mirrors.aliyun.com/pypi/simple/
配置(github网址:https://github.com/jpadilla/django-rest-framework-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),
}
JWT_EXPIRATION_DELTA
指明token的有效期
我们django创建项目的时候,在settings配置文件中直接就生成了一个serect+key,我们可以直接使用它作为我们jwt的serect_key,其实django rest framework-jwt 默认配置中就使用它。
手动生成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即可
11.3 后端实现登录认证接口
Django REST framework JWT提供了登录获取token的视图,可以直接使用
在子应用路由urls.py中
from rest_framework_jwt.views import obtain_jwt_token
urlpatterns = [
path(r'login/', obtain_jwt_token),
]
在主路由中,引入当前子应用的路由文件
urlpatterns = [
...
path('user/', include("users.urls")),
# include 的值必须是 模块名.urls 格式,字符串中间只能出现一个圆点
]
接下来,我们可以通过postman来测试下功能,但是jwt是通过username和password来进行登录认证处理的,所以我们要给真实数据,jwt会去我们配置的user表中去查询用户数据的,验证通过会返回一个token值。
11.4 前端实现登录功能
在登陆组件中找到登陆按钮,绑定点击事件
<button class="login_btn" @click="loginhander">登录</button>
在methods中请求后端
<script>
export default {
name: 'Login',
data(){
return {
login_type: 0,
username:"",
password:"",
remember:'',
}
},
methods:{
loginHandle(){
this.$axios.post(`${this.$settings.Host}/users/login/`,{
username : this.username,
password : this.password
}).then((res)=>{
console.log(res)
}).catch((error)=>{
console.log(error)
});
})
}
},
};
</script>
11.4.1 前端保存jwt
jwt可以保存在cookie中,也可以保存在浏览器的本地存储里,我们一般保存在浏览器本地存储里
浏览器的本地存储提供了sessionStorage和localStorage两种,从属于window对象:
- sessionStorage浏览器关闭即失效
- localStorage长期有效
使用方法:
sessionStorage.变量名 = 变量值 // 保存数据
sessionStorage.setItem("变量名","变量值") // 保存数据
sessionStorage.变量名 // 读取数据
sessionStorage.getItem("变量名") // 读取数据
sessionStorage.removeItem("变量名") // 清除单个数据
sessionStorage.clear() // 清除所有sessionStorage保存的数据
localStorage.变量名 = 变量值 // 保存数据
localStorage.setItem("变量名","变量值") // 保存数据
localStorage.变量名 // 读取数据
localStorage.getItem("变量名") // 读取数据
localStorage.removeItem("变量名") // 清除单个数据
localStorage.clear() // 清除所有sessionStorage保存的数据
登录组件代码Login.vue
methods:{
loginHandle(){
this.$axios.post(`${this.$settings.Host}/users/login/`,{
username : this.username,
password : this.password
}).then((res)=>{
console.log(res)
console.log(this.remember)
if (this.remember){
localStorage.token = res.data.token;
localStorage.username = res.data.username;
localStorage.id = res.data.id;
sessionStorage.removeItem('token');
sessionStorage.removeItem('username');
sessionStorage.removeItem('id');
}else{
sessionStorage.token = res.data.token;
sessionStorage.username = res.data.username;
sessionStorage.id = res.data.id;
localStorage.removeItem('token');
localStorage.removeItem('username');
localStorage.removeItem('id');
}
this.$router.push('/');
默认的返回值仅有token,我们还需在返回值中添加username和id,方便在客户端页面中显示当前登录用户
通过修改该视图的返回值可以完成
在user/utils.py中
def jwt_response_payload_handler(token, user=None, request=None):
"""
自定义jwt认证成功返回数据
"""
return {
'token': token,
'id': user.id,
'username': user.username
}
修改settings.py
# JWT
JWT_AUTH = {
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1),
'JWT_RESPONSE_PAYLOAD_HANDLER': 'users.utils.jwt_response_payload_handler',
}
标签:git,name,项目,models,py,django,路飞,import 来源: https://www.cnblogs.com/yunchao-520/p/13898718.html