一 基础分析
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 ''.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 ------------------
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')
请求如下
请求结果如下
{'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 "".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
查看结果
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,
}
})
结果如下
至此,后端功能基本开发完成