一 基礎分析
1 分析
多人使用的博客系統,此處採用BS架構實現
博客系統,需用戶管理,博文管理用戶管理:用戶註冊,增刪改查用戶
博文管理:增刪改查博文
需要數據庫,本次使用Mysql5.7.17,innodb存儲引擎,前端使用react框架,後端使用django框架
需要支持多用戶登錄,各自可以管理自己的博文(增刪改查),管理是不公開的,但是博文是不需要登錄就可以公開瀏覽的。
2 環境準備
1 mysql 5.6.42
創建庫並指定字符集及相關用戶和權限
create database if not exists blog CHARACTER set utf8mb4 COLLATE utf8mb4_general_ci;
grant all on blog.* to blog@localhost identified by 'blog@123Admin';
flush privileges;
上述因爲本項目後端和數據庫在一個設備上,因此可使用localhost訪問,若非一個設備,則需要將第二條的localhost修改爲'%'及
grant all on blog.* to blog@'%' identified by 'blog';
查看如下
2 django後端項目準備
基本的應用創建本節不再累贅,如需詳細情況請看前一章節
項目創建完畢目錄如下
settings.py
"""
Django settings for blog project.
Generated by 'django-admin startproject' using Django 2.0.
For more information on this file, see
https://docs.djangoproject.com/en/2.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/2.0/ref/settings/
"""
import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '-5n#!qq=8=49k@iikd@c46r%=iq=nu97-5#f@4d4&^x+0=s^9f'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = ['*']
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'user',
'post',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
# 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'blog.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'blog.wsgi.application'
# Database
# https://docs.djangoproject.com/en/2.0/ref/settings/#databases
# DATABASES = {
# 'default': {
# 'ENGINE': 'django.db.backends.sqlite3',
# 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
# }
# }
DATABASES = {
'default':{
'ENGINE' :'django.db.backends.mysql',
'NAME':'blog',
'USER':'blog',
'PASSWORD':'blog@123Admin',
'HOST':'localhost',
'PORT':'3306',
}
}
# Password validation
# https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/2.0/topics/i18n/
LANGUAGE_CODE = 'zh-Hans'
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
USE_L10N = True
USE_TZ = False
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.0/howto/static-files/
STATIC_URL = '/static/'
啓動查看如下
二 註冊接口實現
1 用戶功能設計與實現
提供用戶註冊成立
提供用戶登錄處理
提供路由配置
用戶註冊接口實現
接受用戶通過POST方法提交的註冊信息,提交的數據是JSON格式數據
檢查email 是否已經存在於數據庫中,如果存在則返回錯誤狀態碼,如4xx,若不存在,則將用戶提交的數據存入表中。
整個過程都採用AJAX異步過程,用戶移交JSON數據,服務端獲取數據返回處理,返回JSON。
URL:/user/reg
METHOD: POST
2 請求與分析
前端的時間只能通過CSS,JS和HTML 來完成,但後端的實現可以使用多種語言共同完成
請求流程
瀏覽器------nginx/LVS(處理靜態和動態分離及反向代理請求) ------ python解決動態問題(java,php等)---- react 處理項目靜態頁面問題。
兩個python項目之間的通信通過簡單的HTTP協議暴露URL 即可完成其之間的訪問問題。
nginx 後端代理的nginx處理靜態頁面是唯一的一條路。
用戶請求先到nginx前端,後端又兩個服務,一個是nginx靜態頁面,另一個是python。通過靜態的nginx來訪問API來進行處理,其可以使用內部IP地址加端口號進行訪問,而不需要使用外部訪問處理;
django向後訪問DB,將數據整理好後返回給nginx靜態,通過react框架形成相關的JS,通過AJAX 回調在DOM樹中渲染,並顯示出來。
3 基本URL路由配置
1 在 blog/urls中設置url映射配置多級路由
from django.contrib import admin
from django.conf.urls import url,include # 此處引入include模塊主要用於和下層模塊之間通信處理
urlpatterns = [
url(r'admin/', admin.site.urls),
url(r'^user/',include('user.urls')) # 此處的user.urls表示是user應用下的urls文件引用
]
include 函數參數寫 應用.路由模塊,該函數就會動態導入指定的包的模塊,從模塊中讀取urlpatterns,返回三元組
url 函數第二參數如果不是可調用對象,如果是元祖或列表,則會從路徑找中出去已匹配的部分,將剩餘部分與應用中的路由模塊的urlpatterns 進行匹配
在user應用中創建urls.py文件
如下
#!/usr/bin/poython3.6
#conding:utf-8
from django.conf.urls import url
from django.http import HttpResponse,HttpRequest,JsonResponse
def reg(request:HttpRequest): #此處臨時配置用於測試能否正常顯示
return HttpResponse(b'user.reg')
urlpatterns = [
url(r'reg$',reg) # 此處reg表示的是reg函數。其可以是函數,對象和類,
]
測試
此處是form-data方式提交數據
JSON方式提交數據如下
日誌如下
[20/Oct/2019 10:40:22] "POST /user/reg HTTP/1.1" 200 8
[20/Oct/2019 10:42:01] "POST /user/reg HTTP/1.1" 200 8
4 數據庫user 表類創建
1 創建類
在user/models.py中創建如下代碼,其中郵箱必須唯一
from django.db import models
class User(models.Model):
class Meta:
db_table='user'
id=models.AutoField(primary_key=True)
name=models.CharField(max_length=48,null=False)
email=models.CharField(max_length=64,unique=True,null=False)
password=models.CharField(max_length=128,null=False)
createdate=models.DateTimeField(auto_now=True) # 只在創建時更新時間
def __repr__(self):
return '<user name:{} id:{}>'.format(self.name,self.id)
__str__=__repr__
2 遷移Migration
python manage.py makemigrations
3 執行遷移生成數據庫的表
python manage.py migrate
結果如下
5 視圖函數配置
1 在user/views.py中編寫視圖函數reg
#!/usr/bin/poython3.6
#conding:utf-8
from django.conf.urls import url
from user.views import reg #此處通過導入的方式將views中的函數導出到此處
urlpatterns = [
url(r'reg$',reg) # 此處reg表示的是reg函數。其可以是函數,對象和類,
]
user/views.py
from django.http import HttpResponse,HttpRequest,JsonResponse
def reg(request:HttpRequest): #此處臨時配置用於測試能否正常顯示
print ('request','------------------')
print (type(request))
print (request.POST)
print (request.GET)
print(request.body)
return HttpResponse(b'user.reg')
JSON 請求結果如下
Quit the server with CONTROL-C.
request ------------------
<class 'django.core.handlers.wsgi.WSGIRequest'>
<QueryDict: {}>
<QueryDict: {}>
b'{\n\t"name":"mysql"\n}'
[20/Oct/2019 10:47:02] "POST /user/reg HTTP/1.1" 200 8
此處返回的是一個二進制的json數據
from-data提交顯示結果如下,此中方式處理必須去掉request.body
request ------------------
<class 'django.core.handlers.wsgi.WSGIRequest'>
<QueryDict: {'hello': ['word']}>
<QueryDict: {}>
[20/Oct/2019 10:52:12] "POST /user/reg HTTP/1.1" 200 8
2 JSON 數據處理
由於上述返回爲二進制數據,因此需要使用JSON對其進行相關的處理操作
修改代碼如下
from django.http import HttpResponse,HttpRequest,JsonResponse
import json
def reg(request:HttpRequest): #此處臨時配置用於測試能否正常顯示
print (json.loads(request.body.decode())) # 此處必須是JSON提交方式
return HttpResponse(b'user.reg')
請求如下
請求結果如下
{'name': 'mysql'}
[20/Oct/2019 10:55:19] "POST /user/reg HTTP/1.1" 200 8
3 處理轉換過程異常
from django.http import HttpResponse,HttpRequest,JsonResponse
import json
def reg(request:HttpRequest): #此處臨時配置用於測試能否正常顯示
try:
payloads=json.loads(request.body.decode()) # 此處必須是JSON提交方式
print(payloads)
return HttpResponse('user.reg')
except Exception as e:
print (e)
return HttpResponse() #創建一個實例,但實例中沒有任何內容
結果如下
{'name': 'mysql'}
[20/Oct/2019 11:04:58] "POST /user/reg HTTP/1.1" 200 8
4 simplejson 處理數據
simplejson 比標準庫方便好用,功能強大
pip install simplejson
瀏覽器端端提交的數據放在了請求對象的body中,需要使用simplejson解析,解析方式和json相同,但simplejson更方便 。
from django.http import HttpResponse,HttpRequest,JsonResponse
import simplejson
def reg(request:HttpRequest): #此處臨時配置用於測試能否正常顯示
try:
payloads=simplejson.loads(request.body) # 此處必須是JSON提交方式
print(payloads['name']) # 獲取其中的數據
return HttpResponse('user.reg')
except Exception as e:
print (e)
return HttpResponse() #創建一個實例,但實例中沒有任何內容
請求如下
響應數據如下
mysql
[20/Oct/2019 11:20:52] "POST /user/reg HTTP/1.1" 200 8
5 項目註冊用戶配置
1 創建項目註冊目錄日誌
mkdir /var/log/blog/
2 分析如下
郵箱檢測
郵箱檢測需要查詢user表,需要使用User類的filter方法
email=email,前面是字段名,後面是變量名,查詢後返回結果,如果查詢有結果,則說明該email已經存在,返回400到前端。
用戶存儲信息
創建User 類實例,屬性存儲數據,最後調用save方法,Django默認是在save(),delete() 的時候提交事務數據,如果提交拋出任何異常,則需要捕獲其異常
異常處理
出現獲取輸入框提交信息異常,就返回異常
查詢郵箱存在,返回異常
save方法保存數據,有異常,則向外拋出,捕獲異常
注意一點,django的異常類繼承自HttpEResponse類,所以不能raise,只能return
前端通過狀態驗證碼判斷是否成功
3 編寫相關代碼如下
from django.http import HttpResponse,HttpRequest,JsonResponse,HttpResponseBadRequest
import simplejson
import logging
from .models import User
FORMAT="%(asctime)s %(threadName)s %(thread)d %(message)s"
logging.basicConfig(format=FORMAT,level=logging.INFO,filename='/var/log/blog/reg.log')
def reg(request:HttpRequest): #此處臨時配置用於測試能否正常顯示
print (request.POST)
print (request.body)
payloads=simplejson.loads(request.body)
try:
email=payloads['email']
query=User.objects.filter(email=email) # 此處是驗證郵箱是否存在,若存在,則直接返回
if query:
return HttpResponseBadRequest('email:{} exits'.format(email)) # 此處返回一個實例,此處return 後下面的將不會被執行
name=payloads['name']
password=payloads['password']
logging.info('註冊用戶{}'.format(name)) # 此處寫入註冊用戶基本信息
# 實例化寫入數據庫
user=User() # 實例化對象
user.email=email
user.password=password
user.name=name
try:
user.save() # commit 提交數據
return JsonResponse({'userid':user.id}) # 如果提交正常。則返回此情況
except:
raise # 若異常則直接返回
except Exception as e:
logging.infe(e)
return HttpResponse() #創建一個實例,但實例中沒有任何內容
請求數據如下
log日誌中返回數據和數據庫數據如下
再次請求結果如下
6 註冊接口完善
1 認證
HTTP協議是無狀態協議,爲了解決它產生了cookie和session技術
傳統的session-cookie機制
瀏覽器發起第一次請求到服務器,服務器發現瀏覽器沒有提供session id,就認爲這是第一次請求。會返回一個新的session id 給瀏覽器端,瀏覽器只要不關閉,這個session id就會隨着每一次請求重新發送給服務器端,服務器檢查找到這個sessionid ,若查到,則就認爲是同一個會話,若沒有查到,則認爲就是一個新的請求
session是會話級別的,可以在這個會話session中創建很多數據,鏈接或斷開session清除,包括session id
這個session 機制還得有過期的機制,一段時間內如果沒有發起請求,認爲用戶已經斷開,就清除session,瀏覽器端也會清除相應的cookie信息
這種情況下服務器端保存着大量的session信息,很消耗服務器的內存字段,而且如果多服務器部署,還需要考慮session共享問題,如使用redis和memchached等解決方案。
2 無session解決方案 JWT
1 概述
既然服務器端就是需要一個ID來表示身份,那麼不適用session也可以創建一個ID返回給客戶端,但是,需要保證客戶端不可篡改。
服務端生成一個標識,並使用某種算法對標識簽名
服務端受到客戶端發來的標識,需要檢查簽名
這種方案的缺點是,加密,解密需要消耗CPU計算機資源,無法讓瀏覽器自己主動檢查過期的數據以清除。這種技術成爲JWT(JSON WEB TOKEN)
2 JWT
JWT(json web token) 是一種採用json方式安裝傳輸信息的方式
PYJWT 是python對JW的實現,
文檔
https://pyjwt.readthedocs.io/en/latest/
包
https://pypi.org/project/PyJWT/
安裝
pip install pyjwt
左邊是加密過的東西,無法識別,其使用的是base64編碼,等號去掉,分爲三部分,以點號斷開
第一部分 HEADER:是什麼類型,加密算法是啥
第二部分 PAYLOAD: 數據部分
第三部分 VERIFY SIGNATURE: 加密得到簽名,這個簽名是不可逆的,其中還包含一個密碼,而在Pycharm中就有這樣一個密碼,如下
3 測試JWT
#!/usr/bin/poython3.6
#conding:utf-8
import jwt
import datetime
import base64
key='test'
payload={'name':'demo','email':'[email protected]','password':'demo','ts':int(datetime.datetime.now().timestamp())}
pwd=jwt.encode(payload,key,'HS256')
HEADER,PAYLOAD,VERIFY=pwd.split(b'.')
def fix(src):
rem=len(src)%4 # 取餘數
return src+b'='*rem # 使用等號填充
print (base64.urlsafe_b64decode(fix(HEADER)))
print (base64.urlsafe_b64decode(fix(PAYLOAD)))
print (base64.urlsafe_b64decode(fix(VERIFY)))
結果如下
4 JWT 中encode源代碼
def encode(self, payload, key, algorithm='HS256', headers=None,
json_encoder=None):
# Check that we get a mapping
if not isinstance(payload, Mapping):
raise TypeError('Expecting a mapping object, as JWT only supports '
'JSON objects as payloads.')
# Payload
for time_claim in ['exp', 'iat', 'nbf']:
# Convert datetime to a intDate value in known time-format claims
if isinstance(payload.get(time_claim), datetime):
payload[time_claim] = timegm(payload[time_claim].utctimetuple())
json_payload = json.dumps(
payload,
separators=(',', ':'),
cls=json_encoder
).encode('utf-8')
return super(PyJWT, self).encode(
json_payload, key, algorithm, headers, json_encoder
)
其中會對payload進行json.dumps進行序列化,並使用utf8的編碼方式
父類中的相關方法
def encode(self, payload, key, algorithm='HS256', headers=None,
json_encoder=None):
segments = []
if algorithm is None:
algorithm = 'none'
if algorithm not in self._valid_algs:
pass
# Header
header = {'typ': self.header_typ, 'alg': algorithm}
if headers:
header.update(headers)
json_header = json.dumps(
header,
separators=(',', ':'),
cls=json_encoder
).encode('utf-8')
segments.append(base64url_encode(json_header))
segments.append(base64url_encode(payload))
# Segments
signing_input = b'.'.join(segments)
try:
alg_obj = self._algorithms[algorithm]
key = alg_obj.prepare_key(key)
signature = alg_obj.sign(signing_input, key)
except KeyError:
raise NotImplementedError('Algorithm not supported')
segments.append(base64url_encode(signature))
return b'.'.join(segments)
支持的算法
def get_default_algorithms():
"""
Returns the algorithms that are implemented by the library.
"""
default_algorithms = {
'none': NoneAlgorithm(),
'HS256': HMACAlgorithm(HMACAlgorithm.SHA256),
'HS384': HMACAlgorithm(HMACAlgorithm.SHA384),
'HS512': HMACAlgorithm(HMACAlgorithm.SHA512)
}
if has_crypto:
default_algorithms.update({
'RS256': RSAAlgorithm(RSAAlgorithm.SHA256),
'RS384': RSAAlgorithm(RSAAlgorithm.SHA384),
'RS512': RSAAlgorithm(RSAAlgorithm.SHA512),
'ES256': ECAlgorithm(ECAlgorithm.SHA256),
'ES384': ECAlgorithm(ECAlgorithm.SHA384),
'ES512': ECAlgorithm(ECAlgorithm.SHA512),
'PS256': RSAPSSAlgorithm(RSAPSSAlgorithm.SHA256),
'PS384': RSAPSSAlgorithm(RSAPSSAlgorithm.SHA384),
'PS512': RSAPSSAlgorithm(RSAPSSAlgorithm.SHA512)
})
return default_algorithms
header也會被強制轉換爲二進制形式。
其中是將頭部和payload均加入segments列表中,並通過二進制的b'.'.join進行包裝,進而將其和key一起通過alg_obj.sign(signing_input, key)方法進行處理後得到的signature加入到之前的segments再次通過b'.'.join(segments)進行返回
5 根據相應的JWT算法,重新生成簽名
#!/usr/bin/poython3.6
#conding:utf-8
import jwt
import datetime
from jwt.algorithms import get_default_algorithms
import base64
key='test'
payload={'name':'demo','email':'[email protected]','password':'demo','ts':int(datetime.datetime.now().timestamp())}
pwd=jwt.encode(payload,key,'HS256')
header,payload,sig=pwd.split(b'.')
al_obj=get_default_algorithms()['HS256'] # 拿到對應算法,因爲上面的是一個函數
newkey=al_obj.prepare_key(key) # 獲取到加密後的key
print(newkey)
# 獲取算法信息和對應的payload信息
sig_input,_,_=pwd.rpartition(b'.') # 獲取到對應的算法信息和payload信息,
#此處的整體輸出結果如下
#(b'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiZGVtbyIsInRzIjoxNTcxNTYwNjI2LCJwYXNzd29yZCI6ImRlbW8iLCJlbWFpbCI6IjE4OEAxMjMuY29tIn0', b'.', b'XtY5v8wB0YCsX6ZDwKAMzaPwpbYbPPhTt-vgx4StB74')
print(sig_input)
crypat=al_obj.sign(sig_input,newkey) # 獲取新的簽名
print (base64.urlsafe_b64encode(crypat)) # 使用base64進行處理 如下
print (sig) # 原始的加密後的sig簽名內容
結果如下
簽名的獲取過程
1 通過方法get_default_algorithms 獲取對應算法的相關實例
2 通過實例的prepare_key(key) 生成新的key,newkey,及就是進行了二進制處理
3 通過sign將新的key和對應的算法進行處理,即可生成新的簽名。
由此可知,JWT 生成的token分爲三部分
1 header,有數據類型,加密算法組成
2 payload, 負責數據傳輸,一般放入python對象即可,會被JSON序列化
3 signature,簽名部分,是前面的2部分數據分別base64編碼後使用點號鏈接後,加密算法使用key計算好一的一個結果,再被bse64編碼,得到簽名。
所有的數據都是明文傳輸的,只是做了base64,如果是敏感信息,請不要使用jwt
數據簽名的目的不是爲了隱藏數據,而是保證數據不被篡改,如果數據被篡改了,發回到服務器端,服務器使用自己的key再次計算即便,然後和簽名進行比較,一定對不上簽名。
3 密碼問題
使用郵箱+ 密碼方式登錄
郵箱要求唯一就行了,但密碼如何存儲
早期,密碼都是通過名爲存儲的
後來,使用了MD5存儲,但是,目前也不安全,
MD5 是不可逆的,是非對稱算法
但MD5是可以反查出來的,窮舉的時間也不是很長MD5,MD5計算速度很快
加相同的前綴和後綴,則若窮舉出兩個密碼。則也可以推斷處理所有密碼,加鹽,使用hash(password+salt)的結果存儲進入數據庫中,就算拿到處理密碼反查,也沒用,但如果是固定加鹽,則還是容易被找出規律,或者從源碼中泄露,隨機加鹽,每次鹽都變,就增加了破解的難度
暴力破解,什麼密碼都不能保證不被暴力破解,例如窮舉,所以要使用慢hash算法,如bcrypt,就會讓每一次計算都很慢,都是秒級別的,這樣會導致窮舉時間過長,在密碼破解中,CPU是不能更換的,及不能實現分佈式密碼破解。
4 bcrypt
1 安裝
pip install bcrypt
2 測試代碼
#!/usr/bin/poython3.6
#conding:utf-8
import bcrypt
import datetime
password=b'123456'
# 不同的鹽返回結果是不同的
print (1, bcrypt.gensalt())
print (2,bcrypt.gensalt())
# 獲取到相同的鹽,則計算結果相同
salt=bcrypt.gensalt()
print ('same salt')
x=bcrypt.hashpw(password,salt)
print (3,x)
x=bcrypt.hashpw(password,salt)
print (4,x)
# 不同的鹽結果不同
print('---------- different salt -----------')
x=bcrypt.hashpw(password,bcrypt.gensalt())
print (5,x)
x=bcrypt.hashpw(password,bcrypt.gensalt())
print (6,x)
# 校驗
print(7,bcrypt.checkpw(password,x),len(x)) # 此處返回校驗結果
print(8,bcrypt.checkpw(password+b' ',x),len(x)) # 此處增加了一個空格,則導致校驗不通過
# 計算時長
start=datetime.datetime.now()
y=bcrypt.hashpw(password,bcrypt.gensalt())
delta=(datetime.datetime.now()-start).total_seconds()
print (9,delta)
# 校驗時長
start=datetime.datetime.now()
z=bcrypt.checkpw(password,x)
delta=(datetime.datetime.now()-start).total_seconds()
print (10,delta,z)
結果如下
從耗時看出,bcrypt加密,驗證非常耗時,因此其若使用窮舉,則非常耗時,而且攻破一個密碼,由於鹽不一樣,還得窮舉另外一個
鹽
b'$2b$12$F18k/9ChWWu8BUYjC2iIMO'
加密後結果 b'$2b$12$F18k/9ChWWu8BUYjC2iIMOj0Ny0GdwC.X/.2bFAAy25GgRzcpmqsy'
其中$ 是分割符
$2b$ 加密算法
12表示2^12 key expansion rounds
這是鹽 b'F18k/9ChWWu8BUYjC2iIMO',22 個字符,Base64 編碼
這裏的密文b'F18k/9ChWWu8BUYjC2iIMOj0Ny0GdwC.X/.2bFAAy25GgRzcpmqs',31個字符,Base64
5 註冊接口完善
from django.http import HttpResponse,HttpRequest,JsonResponse,HttpResponseBadRequest
import simplejson
import logging
from .models import User
import jwt
import bcrypt
from blog.settings import SECRET_KEY # 獲取django中自帶的密碼
import datetime
FORMAT="%(asctime)s %(threadName)s %(thread)d %(message)s"
logging.basicConfig(format=FORMAT,level=logging.INFO,filename='/var/log/blog/reg.log')
def get_token(user_id): # 此處的token是通過userid和時間組成的名稱,通過django默認的key來實現加密處理
return (jwt.encode({'user_id':user_id, # 此處是獲取到的token,告訴是那個用戶
'timestamp':int(datetime.datetime.now().timestamp()), # 增加時間戳
},SECRET_KEY,'HS256')).decode()
def reg(request:HttpRequest): #此處臨時配置用於測試能否正常顯示
print (request.POST)
print (request.body)
payloads=simplejson.loads(request.body)
try:
email=payloads['email']
query=User.objects.filter(email=email) # 此處是驗證郵箱是否存在,若存在,則直接返回
if query:
return HttpResponseBadRequest('email:{} exits'.format(email)) # 此處返回一個實例,此處return 後下面的將不會被執行
name=payloads['name']
password=payloads['password']
logging.info('註冊用戶{}'.format(name)) # 此處寫入註冊用戶基本信息
# 實例化寫入數據庫
user=User() # 實例化對象
user.email=email
user.password=bcrypt.hashpw(password.encode(),bcrypt.gensalt()).decode() # 密碼默認是字符串格式,而bcrypt默認需要進行相關處理
#之後返回
user.name=name
try:
user.save() # commit 提交數據
return JsonResponse({'token':get_token(user.id)}) # 如果提交正常。則返回此情況
except:
raise # 若異常則直接返回
except Exception as e:
logging.info(e)
return HttpResponse() #創建一個實例,但實例中沒有任何內容
返回結果如下
三 登錄接口實現
1 用戶功能設計與實現
1 功能設計
提供用戶註冊處理
提供用戶登錄處理
提供用戶路由配置
2 用戶登錄接口設計
接受用戶通過POST提交的登錄信息,提交的數據是JSON格式的數據
{
"email":"122@123",
"password":"demo"
}
從user 表中找到email 匹配的一條記錄,驗證密碼是否正確
驗證通過說明是合法用戶登錄,顯示歡迎界面
驗證失敗返回錯誤碼,如4xx整個過程採用AJAX異步過程,用戶提交JSON數據,服務端獲取數據後處理,返回JSON對象
API 地址
URL : /user/login
METHOD: POST
2 路由配置
1 添加二級路由
user/urls.py 中配置如下
#!/usr/bin/poython3.6
#conding:utf-8
from django.conf.urls import url
from user.views import reg,login
urlpatterns = [
url(r'reg$',reg), # 此處reg表示的是reg函數。其可以是函數,對象和類,
url(r'login$',login)
]
3 基礎登錄代碼
def login(request:HttpRequest):
payload=simplejson.loads(request.body)
try:
email=payload['email']
query=User.objects.filter(email=email).get()
print(query.id)
if not query:
return HttpResponseBadRequest(b'email not exist')
if bcrypt.checkpw(payload['password'].encode(),query.password.encode()): #判斷密碼合法性
# 驗證通過
token=get_token(query.id)
print('token',token)
res=JsonResponse({
'user':{
'user_id':query.id,
'name':query.name,
'email':query.email,
},'token':token
})
return res
else:
return HttpResponseBadRequest(b'password is not correct')
except Exception as e:
logging.info(e)
return HttpResponseBadRequest(b'The request parameter is not valid')
結果如下
4 處理驗證是否登錄問題
如何獲取瀏覽器提交的token信息?
1 使用header中的Authorization
通過這個header增加token信息
通過header 發送數據,所有方法可以是Post,Get
2 自定義header
JWT 來發送token
我們選擇第二種方式認證
基本上所有業務都需要認證用戶的信息
在這裏比較時間戳,如果過期,則就直接拋出401未認證,客戶端受到後就該直接跳轉至登錄頁面
如果沒有提交user id,就直接重新登錄,若用戶查到了,填充user
request -> 時間戳比較 -> user id 比較,向後執行
5 django的認證方式
django.contrib.auth 中提供了許多認證方式,這裏主要介紹三種
1 authenticate(**credentials)
提供了用戶認證,及驗證用戶名及密碼是否正確user=authentical(username='1234',password='1234')
2 login(HttpRequest,user,backend=None)
該函數接受一個HttpRequest對象,及一個驗證了的User對象
此函數使用django的session框架給某個已認證的用戶附加上session id 等信息
3 logout(request)
註銷用戶
該函數接受一個HttpRequest對象,無返回值
當調用該函數時,當前請求的session信息會被全部清除
該用戶即使沒有登錄,使用該函數也不會報錯還提供了一個裝飾器來判斷是否登錄django.contrib.auth.decoratores.login_required
本項目實現了無session機制,且用戶信息自己的表來進行相關的管理,因此認證是通過自己的方式實現的
6 中間鍵技術 Middeware
1 概述
官方定義,在django的request和response處理過程中,由框架提供的hook鉤子
中間鍵技術在1.10之後發生了變化
官方參考文檔
https://docs.djangoproject.com/en/2.2/topics/http/middleware/
其相當於全局攔截器,能夠攔截進來的和出去的數據
2 裝飾器
在需要認證的view函數上增強功能,寫一個裝飾器,誰需要認證,就在這個view函數上應用這個裝飾器
from django.http import HttpResponse,HttpRequest,JsonResponse,HttpResponseBadRequest
import simplejson
import logging
from .models import User
import jwt
import bcrypt
from blog.settings import SECRET_KEY # 獲取django中自帶的密碼
import datetime
FORMAT="%(asctime)s %(threadName)s %(thread)d %(message)s"
logging.basicConfig(format=FORMAT,level=logging.INFO,filename='/var/log/blog/reg.log')
def get_token(user_id): # 此處的token是通過userid和時間組成的名稱,通過django默認的key來實現加密處理
return (jwt.encode({'user_id':user_id, # 此處是獲取到的token,告訴是那個用戶
'timestamp':int(datetime.datetime.now().timestamp()), # 增加時間戳
},SECRET_KEY,'HS256')).decode()
AUTH_EXPIRE=8*60*60 #此處是定義超時時間
def authenticate(view):
def __wapper(request:HttpRequest):
print (request.META)
payload=request.META.get('HTTP_JWT') # 此處會加上HTTP前綴,並自動進行大寫處理
print('request',request.body)
if not payload: # 此處若爲None,則表示沒拿到,則認證失敗
return HttpResponseBadRequest(b'authenticate failed')
try:
payload=jwt.decode(payload,SECRET_KEY,algorithms=['HS256'])
print('返回數據',payload)
except:
return HttpResponse(status=401)
current=datetime.datetime.now().timestamp()
print(current,payload.get('timestamp',0))
if (current-payload.get('timestamp',0)) > AUTH_EXPIRE:
return HttpResponse(status=401)
try:
user_id=payload.get('user_id',-1) # 獲取user_id
user=User.objects.filter(pk=user_id).get()
print ('user',user_id)
except Exception as e:
print(e)
return HttpResponse(status=401)
ret=view(request)
return ret
return __wapper
def reg(request:HttpRequest): #此處臨時配置用於測試能否正常顯示
print (request.POST)
print (request.body)
payloads = simplejson.loads(request.body)
try:
email=payloads['email']
query=User.objects.filter(email=email) # 此處是驗證郵箱是否存在,若存在,則直接返回
if query:
return HttpResponseBadRequest('email:{} exits'.format(email)) # 此處返回一個實例,此處return 後下面的將不會被執行
name=payloads['name']
password=payloads['password']
logging.info('註冊用戶{}'.format(name)) # 此處寫入註冊用戶基本信息
# 實例化寫入數據庫
user=User() # 實例化對象
user.email=email
user.password=bcrypt.hashpw(password.encode(),bcrypt.gensalt()).decode() # 密碼默認是字符串格式,而bcrypt默認需要進行相關處理
#之後返回
user.name=name
try:
user.save() # commit 提交數據
return JsonResponse({'token':get_token(user.id)}) # 如果提交正常。則返回此情況
except:
raise # 若異常則直接返回
except Exception as e:
logging.info(e)
return HttpResponseBadRequest(b'email not exits') #創建一個實例,但實例中沒有任何內容
@authenticate
def login(request:HttpRequest):
payload=simplejson.loads(request.body)
try:
print('login------------',payload)
email=payload['email']
query=User.objects.filter(email=email).get()
print(query.id)
if not query:
return HttpResponseBadRequest(b'email not exist')
if bcrypt.checkpw(payload['password'].encode(),query.password.encode()): #判斷密碼合法性
# 驗證通過
token=get_token(query.id)
print('token',token)
res=JsonResponse({
'user':{
'user_id':query.id,
'name':query.name,
'email':query.email,
},'token':token
})
return res
else:
return HttpResponseBadRequest(b'password is not correct')
except Exception as e:
logging.info(e)
return HttpResponseBadRequest(b'The request parameter is not valid')
請求參數如下
7 JWT 過期問題
1 概述
pyjwt 支持設置過期,在decode的時候,如果過期,則直接拋出異常,需要在payload中增加clamin exp,exp 要求是一個整數int的時間戳。
2 相關代碼如下
from django.shortcuts import render
from django.http import HttpRequest, HttpResponse, HttpResponseBadRequest, JsonResponse
import simplejson
from .models import User
from testdj.settings import SECRET_KEY
import jwt
import datetime
import bcrypt
# 定義時間
EXP_TIMNE = 10 * 3600 * 8
def get_token(user_id):
return jwt.encode(payload={'user_id': user_id, 'exp': int(datetime.datetime.now().timestamp())+EXP_TIMNE}
, key=SECRET_KEY, algorithm='HS256').decode()
def authontoken(view):
def __wapper(request: HttpRequest):
token = request.META.get('HTTP_JWT')
if token:
try:
payload = jwt.decode(token, SECRET_KEY, algorithm='HS256') # 此處便有處理機制來處理過期
user = User.objects.filter(pk=payload['user_id']).get() # 獲取user_id,若存在,則表明此token是當前用戶的token
request.user_id = user.id# 此處獲取user_id,用於後期直接處理
print('token 合法校驗通過')
except Exception as e:
print(e)
return HttpResponseBadRequest(b'token auth failed')
else:
print('未登錄過,請登錄')
return view(request)
return __wapper
def reg(request: HttpRequest):
try:
payload = simplejson.loads(request.body)
email = payload['email']
print(email)
query = User.objects.filter(email=email) # 獲取郵箱信息
if query: # 若郵箱存在
return HttpResponseBadRequest(b'email exist')
user = User()
name = payload['name']
passowrd = payload['password'].encode()
print(email, name, passowrd)
user.name = name
user.password = bcrypt.hashpw(passowrd, bcrypt.gensalt()).decode() # 獲取加密後的password信息
user.email = email
try:
user.save()
return JsonResponse({'userinfo': {
'USER_ID': user.id,
'name': user.name,
'email': user.email,
}, 'token': get_token(user.id)})
except Exception as e:
print(e)
return HttpResponseBadRequest(b'data insert failed')
except Exception as e:
print(e)
return HttpResponseBadRequest(b'paraments type not legal')
@authontoken
def login(request: HttpRequest):
try:
payload = simplejson.loads(request.body) # 郵箱和密碼,並且能夠獲取token,需要先判斷郵箱是否存在,若不存在,則直接報錯
email = payload['email']
print(email, '-------------------------------')
user = User.objects.filter(email=email).get()
if not user.id:
return HttpResponseBadRequest("email :{} not exist".format(email).encode())
password = payload['password']
if bcrypt.checkpw(password.encode(), user.password.encode()):
return JsonResponse({
"userinfo": {
"user_id": user.id,
"user_name": user.name,
"user_email": user.email,
},
"token": get_token(user.id)
})
else:
return HttpResponseBadRequest(b'password failed')
except Exception as e:
print(e)
return HttpResponseBadRequest(b'email failed')
四 博文接口實現
1 功能分析
功能 | 函數名 | Request 方法 | 路徑 |
---|---|---|---|
發佈 (增) | pub | post | /pub |
看文章(查) | get | get | /(\d+) |
列表(分頁) | getall | get | / |
2 路由配置
1 添加一級路由
blog/urls.py配置
from django.contrib import admin
from django.conf.urls import url,include # 此處引入include模塊主要用於和下層模塊之間通信處理
urlpatterns = [
url(r'admin/', admin.site.urls),
url(r'^user/',include('user.urls')), # 此處的user.urls表示是user應用下的urls文件引用
url(r'^post/',include('post.urls'))
]
2 添加二級路由
post/urls.py
#!/usr/bin/poython3.6
#conding:utf-8
from django.conf.urls import url
from post.views import get,getall,pub
urlpatterns=[
url(r'pub',pub),
url(r'^$',getall),
url(r'(\d+)',get)
]
2 添加博客功能實現
1 創建數據庫類
在 /blog/post/models.py中創建如下配置
from django.db import models
from testapp.models import User
class Post(models.Model):
class Meta:
db_table = 'post'
id = models.AutoField(primary_key=True) # 主鍵自增
title = models.CharField(max_length=256, null=False) # 文章標題定義
pubdata = models.DateTimeField(auto_now=True) # 自動處理時間更新
author = models.ForeignKey(User, on_delete=False) # 定義外鍵
def __repr__(self):
return "<Post id:{} title:{}>".format(self.id, self.title)
__str_ = __repr__
class Content(models.Model): # 此處若不添加id,則系統會自動添加自增id,用於相關操作
class Meta:
db_table = 'content'
post = models.OneToOneField(Post, to_field='id', on_delete=False) # 一對一,此處會有一個外鍵引用post_id
content = models.TextField(null=False)
def __repr__(self):
return "<Content {} {}>".format(self.id, self.post)
__str__ = __repr__
2 遷移配置
python manage.py makemigrations
3 生效配置
python manage.py migrate
查看結果
4 添加至界面如下
/blog/post/admin.py中增加如下配置
from django.contrib import admin
from .models import Content, Post
admin.site.register(Content)
admin.site.register(Post)
查看如下
5 添加測試數據如下
1 post 數據如下
2 content數據添加如下
3 上傳接口實現
用戶從瀏覽器端提交json數據,包含title,content
提交需要認證用戶,從請求的header中驗證jwt
from django.http import HttpResponseBadRequest, HttpRequest, HttpResponse, JsonResponse
from .models import Post, Content
import math
from user.views import authontoken
import simplejson
@authontoken # 此處需要先進行認證。認證通過後方可進行相關操作,其會獲取到一個user_id,通過是否存在user_id來進行處理
def pub(request: HttpRequest):
try:
payload = simplejson.loads(request.body)
title = payload['title']
author = request.user_id
post = Post()
post.title = title
post.author_id = author
try:
post.save()
cont = Content()
content = payload['content']
cont.content = content
cont.post_id = post.id
try:
cont.save()
return JsonResponse({"user_id": post.id})
except Exception as e:
print(e)
return HttpResponseBadRequest(b'con insert into failed')
except Exception as e:
print(e)
HttpResponseBadRequest(b'post data insert failed')
except Exception as e:
print(e)
return HttpResponseBadRequest(b'request param not auth')
結果如下
未添加token的結果
添加了token的結果
4 博文操作之get 接口實現
根據post_id 查看博文並返回
此處是查看,不需要認證,相關代碼如下
def get(request: HttpRequest, id): # 此處用於獲取之前配置的分組匹配的內容
print('文章ID', id)
try:
query = Post.objects.filter(pk=id).get()
if not query:
return HttpResponseBadRequest(b'article not exist')
return JsonResponse({
"post": {
"post_title": query.title,
"author_id": query.author.id,
"post_conent": query.content.content, # 通過此方式可獲取關聯的數據庫的數據
"post_user": query.author.email,
'date': query.pubdata,
'post_name': query.author.name,
}
})
except Exception as e:
print(e)
return HttpResponseBadRequest(b'article 00 not exist')
結果如下
5 getall接口實現
發起get請求,通過查詢字符串http://url/post/?page=1&size=10 進行查詢處理,獲取相關分頁數據和相關基本數據
代碼如下
def getall(request: HttpRequest):
try:
page = int(request.GET.get('page', 1)) # 此處可獲取相關數據的值,page和size
page = page if page > 0 else 1
except:
page = 1
try:
size = int(request.GET.get('size', 20))
size = size if size > 0 and size < 11 else 10
except:
size = 10
start = (page - 1) * size # 起始數據列表值
postsall = Post.objects.all()
posts = Post.objects.all()[::-1][start:page * size]
# 總數據,當前頁,總頁數
count = postsall.count()
# 總頁數
pages = math.ceil(count / size)
# 當前頁
page = page
# 當前頁數量
return JsonResponse({
"posts": [
{
"post_id": post.id,
"post_title": post.title,
"post_name": post.author.name,
} for post in posts
],
"pattern": {
"count": count,
"pages": pages,
"page": page,
"size": size,
}
})
優化代碼,將page和size 使用同一個函數處理如下
def getall(request: HttpRequest):
size=validate(request.GET,'size',int,20,lambda x,y : x if x>0 and x<20 else y)
page=validate(request.GET,'page',int,1,lambda x,y : x if x>0 else y)
start = (page - 1) * size # 起始數據列表值
print(size, page)
postsall = Post.objects.all()
posts = Post.objects.all()[::-1][start:page * size]
# 總數據,當前頁,總頁數
count = postsall.count()
# 總頁數
pages = math.ceil(count / size)
# 當前頁
page = page
# 當前頁數量
return JsonResponse({
"posts": [
{
"post_id": post.id,
"post_title": post.title,
"post_name": post.author.name,
} for post in posts
],
"pattern": {
"count": count,
"pages": pages,
"page": page,
"size": size,
}
})
結果如下
至此,後端功能基本開發完成