文章目錄
1,前後端分離
1.1 什麼是前後端分離
前端: 即客戶端,負責渲染用戶顯示界面【如web的js動態渲染頁面, 安卓, IOS,pc客戶端等】
後端:即服務器端,負責接收http請求,處理數據
API:Application Programming Interface 是一些預先定義的函數,或指軟件系統不同組成部分銜接的約定
前後端分離 完整請求過程
1,前端通過http請求後端API
2,後端以json形式返回前端數據
3,前端生成用戶顯示界面【如html , ios , android】
判斷前後端分離得核心標準: 誰生成顯示頁面
1,後端生成【前後端未分離】 ex: flask->render_template django -> HttpResponse(html)
2, 前端生成【前後端分離】
1.2 優點
1,各司其職
前端:視覺層面,兼容性,前端性能優化
後端:併發,可用性,性能
2,解耦,前端和後端均易於擴展
3,後端靈活搭配各類前端 - 如安卓等
4,提高用戶體驗
5,前端+後端可完全並行開發,加快開發效率
1.3 分離常見問題
問題 | 答案 |
---|---|
如何解決http無狀態? | 採用token(詳情見下方章節) |
如果前端爲JS,如何解決跨域問題? | 採用CORS(詳情見下方章節) |
如何解決csrf問題 | 採用token |
Single Page web Application 是否會影響Search Engine Optimization效果 | 會,前後端分離後,往往頁面不存在靜態文字【例如新聞的詳細內容】 |
”老闆,這個邏輯到底是讓前端做還是後端做啊?“ | 底線原則: 數據校驗需要前後端都做 |
”老闆,前端工作壓力太大了啊“ | 團隊協作不能只是嘴上說說 |
動靜分離和前後端分離是一個意思麼? | 動靜分離指的是 css/js/img這類靜態資源跟服務器拆開部署,典型方案-靜態資源交由CDN廠商處理 |
1.4 實現方式
1,Django/Flask 後端只返回json
2, 前端 -> ex: js向服務器發出ajax請求,獲取數據,拿到數據後動態生成html
3, 前端服務和後端服務 分開部署
查看站點信息---->site:域名
如 : site:www.jd.com
2,token - 令牌
學前須知:
1,base64
'防君子不防小人’
方法 | 作用 | 參數 | 返回值 |
---|---|---|---|
b64encode | 將輸入的參數轉化爲base64規則的串 | 預加密的明文,類型爲bytes;例:b‘guoxiaonao’ | base64對應編碼的密文,類型爲bytes;例:b’Z3VveGlhb25hbw==’ |
b64decode | 將base64串 解密回 明文 | base64密文,類型爲bytes;例:b’Z3VveGlhb25hbw==’ | 參數對應的明文,類型爲bytes;例:b’guoxiaonao’ |
urlsafe_b64encode | 作用同b64encode,但是會將 ‘+‘替換成 ‘-’,將’/‘替換成’_’ | 同b64encode | 同b64encode |
urlsafe_b64decode | 作用同b64decode | 同b64decode | 同b64decode |
代碼演示:
import base64
#base64加密
s = b'guoxiaonao'
b_s = base64.b64encode(s)
#b_s打印結果爲 b'Z3VveGlhb25hbw=='
#base64解密
ss = base64.b64decode(b_s)
#ss打印結果爲 b'guoxiaonao'
2,SHA-256 安全散列算法的一種(hash)
hash三大特點:
1)定長輸出
2)不可逆
3) 雪崩
import hashlib
s = hashlib.sha256() #創建sha256對象
s.update(b'xxxx') #添加欲hash的內容,類型爲 bytes
s.digest() #獲取最終結果
>>> import hashlib
>>> m=hashlib.md5()
>>> m.update(b'123')
>>> m.hexdigest()
3,HMAC-SHA256 是一種通過特別計算方式之後產生的消息認證碼,使用散列算法
同時結合一個加密密鑰
。
- 它可以用來
保證數據的完整性
,同時可以用來作某個消息的身份驗證
import hmac
#生成hmac對象
#第一個參數爲加密的key,bytes類型(鹽),
#第二個參數爲欲加密的串,bytes類型
#第三個參數爲hmac的算法,指定爲SHA256
h = hmac.new(key, str, digestmod='SHA256 ')
h.digest() #獲取最終結果
h.hexdigest()
4,RSA256 非對稱加密
1,加密: 公鑰加密,私鑰解密
2,簽名: 私鑰簽名, 公鑰驗籤
JWT - json-web-token
1,三大組成
1,header
格式爲字典-元數據格式如下
{'alg':'HS256', 'typ':'JWT'}
#alg代表要使用的 算法
#typ表明該token的類別 - 此處必須爲 大寫的 JWT
該部分數據需要轉成json串並用base64 加密
2,payload
格式爲字典-此部分分爲公有聲明和私有聲明
-
公共聲明:JWT提供了內置關鍵字用於描述常見的問題
此部分均爲可選項,用戶根據自己需求 按需添加key,常見公共聲明如下:
{'exp':xxx, # Expiration Time 此token的過期時間的時間戳 'iss':xxx,# (Issuer) Claim 指明此token的簽發者 'iat':xxx, # (Issued At) Claim 指明此創建時間的時間戳 'aud':xxx, # (Audience) Claim 指明此token簽發面向羣體 }
-
私有聲明:用戶可根據自己業務需求,添加自定義的key,例如如下:
{'username': 'guoxiaonao'}
公共聲明和私有聲明均在同一個字典中;轉成json串並用base64加密
3,signature 簽名
簽名規則如下:
根據header中的alg確定 具體算法,以下用 HS256爲例
HS256(自定義的key , base64後的header + b’.’ + base64後的payload)
解釋:用自定義的key, 對base64後的header + b’.’ + base64後的payload進行hmac計算
2,jwt結果格式
base64(header) + b’.’ + base64(payload) + b’.’ + base64(sign)
最終結果token
如下: b’eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6Imd1b3hpYW9uYW8iLCJpc3MiOiJnZ2cifQ.Zzg1u55DCBqPRGf9z3-NAn4kbA-MJN83SxyLFfc5mmM’
3,校驗jwt規則
1,解析header, 確認alg
2,簽名校驗 - 根據傳過來的header和payload按 alg指明的算法進行簽名,將簽名結果和傳過來的sign進行對比,若對比一致,則校驗通過
3,獲取payload自定義內容
import copy, json, base64, hmac, time
class MyJWT():
"""自制JWK"""
def __init__(self):
pass
@staticmethod
def b64encode(j_s):
# 替換b64生成的佔位'='
return base64.urlsafe_b64encode(j_s).replace(b'=', b'')
@staticmethod
def b64decode(b64_s):
rem = len(b64_s) % 4
if rem:
b64_s += b'=' * (4 - rem)
return base64.urlsafe_b64decode(b64_s)
@staticmethod
def encode(payload, key, exp=300):
# init header
header = {'alg': 'HS256', 'typ': 'JWT'}
# 將header 轉成json串 sort_keys=True(有序) 去掉空格
header_json = json.dumps(header, sort_keys=True, separators=(',', ':'))
# b64 - json 串
header_bs = MyJWT.b64encode(header_json.encode())
# init payload 參數可能爲{'username':'xioaming',}
payload = copy.deepcopy(payload) # 避免污染變量.使用深拷貝
# 失效時間
payload['exp'] = time.time() + exp
# 轉成json
payload_json = json.dumps(payload, sort_keys=True, separators=(',', ':'))
# b64 - json 串
payload_bs = MyJWT.b64encode(payload_json.encode())
if isinstance(key, str):
key = key.encode()
# signature 簽名
_str = header_bs + b'.' + payload_bs
h = hmac.new(key, _str, digestmod='SHA256')
h_bs = MyJWT.b64encode(h.digest())
return _str + b'.' + h_bs
@staticmethod
def decode(key, token):
# 1.對比兩次HMAC結果
# 2.payload部分有exp的話,要校驗exp
# 3.最終返回 payload
header_bs, payload_bs, sign = token.split(b'.')
if isinstance(key, str):
key = key.encode()
# 重新計算hmac的值
h = hmac.new(key, header_bs + b'.' + payload_bs, digestmod='SHA256')
# 比較兩次sign
if sign != MyJWT.b64encode(h.digest()):
# hmac 異常
raise
# 獲取payload內容
payload_json = MyJWT.b64decode(payload_bs)
payload = json.loads(payload_json)
# exp 校驗
exp = payload['exp']
now = time.time()
if exp < now:
# 此token 過期
raise
return payload
if __name__ == '__main__':
payload = {
'username': 'xiaoming',
}
res = MyJWT.encode(payload, key='abc', exp=300)
print(res)
print(MyJWT.decode('abc', res))
# 測試過時
# time.sleep(2)
# print(MyJWT.decode('abc',res))
4,pyjwt
1,安裝 pip3 install pyjwt
方法 | 參數說明 | 返回值 |
---|---|---|
encode(payload, key, algorithm) | payload: jwt三大組成中的payload,需要組成字典,按需添加公有聲明和私有聲明 例如: {‘username’: ‘guoxiaonao’, ‘exp’: 1562475112} 參數類型: dict |
token串 返回類型:bytes |
key : 自定義的加密key 參數類型:str |
||
algorithm: 需要使用的加密算法[HS256, RSA256等] 參數類型:str |
||
decode(token,key,algorithm,) | token: token串 參數類型: bytes/str |
payload明文 返回類型:dict |
key : 自定義的加密key ,需要跟encode中的key保持一致 參數類型:str |
||
algorithm: 同encode | ||
issuer: 發佈者,若encode payload中添加 ‘iss’ 字段,則可針對該字段校驗 參數類型:str |
若iss校驗失敗,則拋出jwt.InvalidIssuerError | |
audience:簽發的受衆羣體,若encode payload中添加’aud’字段,則可針對該字段校驗 參數類型:str |
若aud校驗失敗,則拋出jwt.InvalidAudienceError |
PS: 若encode得時候 payload中添加了exp字段; 則exp字段得值需爲 當前時間戳+此token得有效期時間, 例如希望token 300秒後過期 {‘exp’: time.time() + 300}; 在執行decode時,若檢查到exp字段,且token過期,則拋出jwt.ExpiredSignatureError
import hashlib
import json
import time
import jwt
from django.http import JsonResponse
from django.shortcuts import render
from user.models import UserProfile
from tools.logging import TOKEN_KEY
# Create your views here.
# 將token 獨立出來
# 登錄驗證
def tokens(request):
# 登錄
if request.method!='POST':
result={'code':10200,'error':'Please send POST`s request'}
return JsonResponse(result)
json_str=request.body
if not json_str:
result = {'code':10201,'error':'Please give me data'}
return JsonResponse(result)
json_obj=json.loads(json_str)
username=json_obj.get('username')
password=json_obj.get('password')
if not username:
result={'code':10202,'error':'Please give me username'}
return JsonResponse(result)
if not password:
result={'code':10203,'error':'Please give me password'}
return JsonResponse(result)
user=UserProfile.objects.filter(username=username)
if not user:
result={'code':10204,'error':'The username or password is wrong'}
return JsonResponse(result)
user=user[0]
# 對比密碼
p_m=hashlib.md5()
p_m.update(password.encode())
if p_m.hexdigest() != user.password:
result={'code':10205,'error':'The username or password is wrong'}
return JsonResponse(result)
# 生成token
token=make_token(username)
return JsonResponse({'code': 200, 'username':username,'data': {'token': token.decode()}})
def make_token(username,expire=24*60*60):
# 生成token
key=TOKEN_KEY
now_t=time.time()
payload={'username':username,'exp':now_t+expire}
token=jwt.encode(payload,key,algorithm='HS256')
return token
import jwt
from django.http import JsonResponse
from user.models import UserProfile
TOKEN_KEY = 'asd'
# 登錄驗證
def logging_check(*methods):
def _logging_check(func):
def wrapper(request, *args, **kwargs):
token = request.META.get('HTTP_AUTHORIZATION')
if not methods:
# 不檢查
return func(request, *args, **kwargs)
else:
# @logging_check('PUT','POST')
if request.method not in methods:
return func(request, *args, **kwargs)
# 檢查token
# 1. 檢查有沒有token
if not token:
result = {'code': 10206, 'error': 'Please login'}
return JsonResponse(result)
try:
res = jwt.decode(token, TOKEN_KEY)
except Exception as e:
print(e)
result = {'code': 10207, 'error': 'Please login !'}
return JsonResponse(result)
username = res['username']
try:
user = UserProfile.objects.get(username=username)
except Exception as e:
print('--get error--')
print(e)
result = {'code': 10208, 'error': 'Please login !!'}
return JsonResponse(result)
# 將user 作爲request的屬性帶回視圖函數
request.user = user
return func(request, *args, **kwargs)
return wrapper
return _logging_check
def get_user_by_request(request):
# 嘗試性拿user
token=request.META.get('HTTP_AUTHORIZATION')
if not token:
# 遊客
return None
try:
res=jwt.decode(token,TOKEN_KEY)
except Exception as e:
print(e)
return None
username=res['username']
try:
user=UserProfile.objects.get(username=username)
except Exception as e:
print(e)
return None
return user
3, CORS - Cross-origin resource sharing - 跨域資源共享
1,什麼是CORS
允許瀏覽器向跨源(協議 + 域名 + 端口)服務器,發出XMLHttpRequest請求,從而克服了AJAX只能同源使用的限制
2,特點
1,瀏覽器自動完成(在請求頭中加入特殊頭 或 發送特殊請求)
2,服務器需要支持(響應頭中需要有特殊頭)
3,簡單請求(Simple requests)和預檢請求(Preflighted requests)
滿足以下全部條件的請求爲 簡單請求
1,請求方法如下:
GET or HEAD or POST
2,請求頭僅包含如下:
Accept
Accept-Language
Content-Language
Content-Type
3,Content-Type 僅支持如下三種:
application/x-www-form-urlencoded >> 普通表單
multipart/form-data >> 文件上傳的表單
text/plain
不滿足以上任意一點的請求都是 預檢請求
4,簡單請求發送流程
1,請求
請求頭中 攜帶 Origin,該字段表明自己來自哪個域
2,響應
如果請求頭中的Origin在服務器接受範圍內, 則返回如下頭
響應頭 | 作用 | 備註 |
---|---|---|
Access-Control-Allow-Origin | 服務器接受得域 | |
Access-Control-Allow-Credentials | 是否接受Cooike | 可選 |
Access-Control-Expose-Headers | 默認情況下,xhr只能拿到如下響應頭:Cache-Control,Content-Language,Content-Type,Expires,Last-Modified;如果有需要獲取其他頭,需在此指定 |
可選 |
如果服務器不接受此域,則響應頭中不包含 Access-Control-Allow-Origin
5,預檢請求發送流程
1,OPTION 請求發起,攜帶如下請求頭
請求頭 | 作用 | 備註 |
---|---|---|
Origin | 表明此請求來自哪個域 | 必選 |
Access-Control-Request-Method | 此次請求使用方法 | 必選 |
Access-Control-Request-Headers | 此次請求使用的頭 | 必選 |
2,OPTION 接受響應階段,攜帶如下響應頭
響應頭 | 作用 | 備註 |
---|---|---|
Access-Control-Allow-Origin | 同簡單請求 - 服務器接受得域 | 必選 |
Access-Control-Allow-Methods | 告訴瀏覽器,服務器接受得跨域請求方法 | 必選 |
Access-Control-Allow-Headers | 返回所有支持的頭部,當request有 ‘Access-Control-Request-Headers’時,該響應頭必然回覆 |
必選 |
Access-Control-Allow-Credentials | 同簡單請求 | 可選 |
Access-Control-Max-Age | OPTION請求緩存時間,單位s | 可選 |
3,主請求階段
請求頭 | 作用 | 備註 |
---|---|---|
Origin | 表明此請求來自哪個域 |
4,主請求響應階段
響應頭 | 作用 | 備註 |
---|---|---|
Access-Control-Allow-Origin | 當前服務器接受得域 |
6,Django支持
django-cors-headers官網 https://pypi.org/project/django-cors-headers/
直接pip 將把django升級到2.0以上,強烈建議用離線安裝方式
$ tar -zxvf django-cors-headers-3.0.2.tar.gz
$ cd django-cors-headers-3.0.2/
$ sudo python3 setup.py install
$ pip3 freeze | grep 'cors'
配置流程
1,INSTALLED_APPS 中添加 corsheaders
2,MIDDLEWARE 中添加 corsheaders.middleware.CorsMiddleware
位置儘量靠前,官方建議 ‘django.middleware.common.CommonMiddleware’ 上方
3,CORS_ORIGIN_ALLOW_ALL 布爾值 如果爲True 白名單不啓用
4,CORS_ORIGIN_WHITELIST =[ # 白名單
"https://example.com"
]
5, CORS_ALLOW_METHODS = ( #支持的請求方法
'DELETE',
'GET',
'OPTIONS',
'PATCH',
'POST',
'PUT',
)
6, CORS_ALLOW_HEADERS = ( # 支持的頭
'accept-encoding',
'authorization', # 校驗信息-token放在此處
'content-type',
'dnt',
'origin',
'user-agent',
'x-csrftoken', # x- 不屬於官方頭
'x-requested-with',
)
7, CORS_PREFLIGHT_MAX_AGE 默認 86400s(一天)
8, CORS_EXPOSE_HEADERS []
9, CORS_ALLOW_CREDENTIALS 布爾值, 默認False # 是否使用跨域cookie
4,RESTful -Representational State Transfer
什麼是RESTful
- 一種指導思想
1,資源 (Resources)
網絡上的一個實體,或者說是網絡上的一個具體信息,並且每個資源都有一個獨一無二得URI與之對應;獲取資源-直接訪問URI即可
2,表現層(Representation)
如何去表現資源 - 即資源得表現形式;如HTML , xml , JPG , json等
3,狀態轉化(State Transfer)
訪問一個URI即發生了一次 客戶端和服務端得交互;此次交互將會涉及到數據和狀態得變化
客戶端需要通過某些方式觸發具體得變化 - HTTP method 如 GET, POST,PUT,PATCH,DELETE 等
RESTful的特徵
1,每一個URI代表一種資源
2,客戶端和服務器端之前傳遞着資源的某種表現
3,客戶端通過HTTP的幾個動作 對 資源進行操作 - 發生‘狀態轉化’
如何設計符合RESTful 特徵的API
1,協議 - http/https
2,域名:
域名中體現出api字樣,如
https://api.example.com
or
https://example.org/api/
3, 版本:
https://api.example.com/v1/
4,路徑 -
路徑中避免使用動詞,資源用名詞表示,案例如下
https://api.example.com/v1/users
https://api.example.com/v1/animals
5,HTTP動詞語義
-
GET(SELECT):從服務器取出資源(一項或多項)。
-
POST(CREATE):在服務器新建一個資源。
-
PUT(UPDATE):在服務器更新資源(客戶端提供改變後的完整資源)。
-
PATCH(UPDATE):在服務器更新資源(客戶端提供改變的屬性)。
-
DELETE(DELETE):從服務器刪除資源。
具體案例如下:
GET /zoos:列出所有動物園 POST /zoos:新建一個動物園 GET /zoos/ID:獲取某個指定動物園的信息 PUT /zoos/ID:更新某個指定動物園的信息(提供該動物園的全部信息) PATCH /zoos/ID:更新某個指定動物園的信息(提供該動物園的部分信息) DELETE /zoos/ID:刪除某個動物園 GET /zoos/ID/animals:列出某個指定動物園的所有動物 DELETE /zoos/ID/animals/ID:刪除某個指定動物園的指定動物
6,巧用查詢字符串
?limit=10:指定返回記錄的數量 ?offset=10:指定返回記錄的開始位置。 ?page=2&per_page=100:指定第幾頁,以及每頁的記錄數。 ?sortby=name&order=asc:指定返回結果按照哪個屬性排序,以及排序順序。 ?type_id=1:指定篩選條件
7,狀態碼
1,用HTTP響應碼錶達 此次請求結果,例如
200 OK - [GET]:服務器成功返回用戶請求的數據 201 CREATED - [POST/PUT/PATCH]:用戶新建或修改數據成功。 202 Accepted - [*]:表示一個請求已經進入後臺排隊(異步任務) 204 NO CONTENT - [DELETE]:用戶刪除數據成功。 400 INVALID REQUEST - [POST/PUT/PATCH]:用戶發出的請求有錯誤,服務器沒有進行新建或修改數據的操作,該操作是冪等的。 401 Unauthorized - [*]:表示用戶沒有權限(令牌、用戶名、密碼錯誤)。 403 Forbidden - [*] 表示用戶得到授權(與401錯誤相對),但是訪問是被禁止的。 404 NOT FOUND - [*]:用戶發出的請求針對的是不存在的記錄,服務器沒有進行操作,該操作是冪等的。 406 Not Acceptable - [GET]:用戶請求的格式不可得(比如用戶請求JSON格式,但是隻有XML格式)。 410 Gone -[GET]:用戶請求的資源被永久刪除,且不會再得到的。 422 Unprocesable entity - [POST/PUT/PATCH] 當創建一個對象時,發生一個驗證錯誤。 500 INTERNAL SERVER ERROR - [*]:服務器發生錯誤
2, 自定義內部code 進行響應(json碼)
如 返回結構如下 {‘code’:200, ‘data’: {}, ‘error’: xxx}
8,返回結果
根據HTTP 動作的不同,返回結果的結構也有所不同
GET /users:返回資源對象的列表(數組) GET /users/guoxiaonao:返回單個資源對象 POST /users:返回新生成的資源對象 PUT /users/guoxiaonao:返回完整的資源對象 PATCH /users/guoxiaonao:返回完整的資源對象 DELETE /users/guoxiaonao:返回一個空文檔