多人博客開發項目-後端

一 基礎分析

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,
        }
    })

結果如下

多人博客開發項目-後端

多人博客開發項目-後端

至此,後端功能基本開發完成

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章