前後端分離 | token令牌 | JWT | CORS跨域資源共享 | RESTful

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:返回一個空文檔
    
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章