一 基础分析

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';

查看如下

多人博客开发项目-后端_第1张图片

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/'

多人博客开发项目-后端_第2张图片

启动查看如下

多人博客开发项目-后端_第3张图片

多人博客开发项目-后端_第4张图片

二 注册接口实现

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文件

如下

多人博客开发项目-后端_第5张图片

#!/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方式提交数据

多人博客开发项目-后端_第6张图片

JSON方式提交数据如下

多人博客开发项目-后端_第7张图片

日志如下

[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中创建如下代码,其中邮箱必须唯一

多人博客开发项目-后端_第8张图片


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  ''.format(self.name,self.id)
    __str__=__repr__

2 迁移Migration

python  manage.py  makemigrations

多人博客开发项目-后端

3 执行迁移生成数据库的表

python  manage.py  migrate

多人博客开发项目-后端

结果如下

多人博客开发项目-后端_第9张图片

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 ------------------



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 ------------------



[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')

请求如下

多人博客开发项目-后端_第10张图片

请求结果如下

{'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() #创建一个实例,但实例中没有任何内容

请求如下

多人博客开发项目-后端_第11张图片

响应数据如下

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() #创建一个实例,但实例中没有任何内容

请求数据如下

多人博客开发项目-后端_第12张图片

log日志中返回数据和数据库数据如下

多人博客开发项目-后端_第13张图片

再次请求结果如下

多人博客开发项目-后端_第14张图片

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

多人博客开发项目-后端_第15张图片

左边是加密过的东西,无法识别,其使用的是base64编码,等号去掉,分为三部分,以点号断开

第一部分 HEADER:是什么类型,加密算法是啥

第二部分 PAYLOAD: 数据部分

第三部分 VERIFY SIGNATURE: 加密得到签名,这个签名是不可逆的,其中还包含一个密码,而在Pycharm中就有这样一个密码,如下

多人博客开发项目-后端_第16张图片

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)

结果如下

多人博客开发项目-后端_第17张图片

从耗时看出,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() #创建一个实例,但实例中没有任何内容

返回结果如下

多人博客开发项目-后端_第18张图片

三 登录接口实现

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')

结果如下

多人博客开发项目-后端_第19张图片

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')

多人博客开发项目-后端_第20张图片

请求参数如下

多人博客开发项目-后端_第21张图片

多人博客开发项目-后端_第22张图片

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 "".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 "".format(self.id, self.post)

    __str__ = __repr__

2 迁移配置

 python  manage.py makemigrations

3 生效配置

python  manage.py migrate

查看结果

多人博客开发项目-后端_第23张图片

4 添加至界面如下

/blog/post/admin.py中增加如下配置

from django.contrib import admin
from .models import Content, Post

admin.site.register(Content)
admin.site.register(Post)

查看如下

多人博客开发项目-后端_第24张图片

5 添加测试数据如下

1 post 数据如下

多人博客开发项目-后端_第25张图片

2 content数据添加如下

多人博客开发项目-后端_第26张图片

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的结果

多人博客开发项目-后端_第27张图片

添加了token的结果

多人博客开发项目-后端_第28张图片

多人博客开发项目-后端_第29张图片

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')

结果如下

多人博客开发项目-后端_第30张图片

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

结果如下

多人博客开发项目-后端_第31张图片

多人博客开发项目-后端_第32张图片

至此,后端功能基本开发完成