路飛專案
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',
}