多人使用的博客系统,此处采用BS架构实现
博客系统,需用户管理,博文管理用户管理:用户注册,增删改查用户
博文管理:增删改查博文
需要数据库,本次使用Mysql5.7.17,innodb存储引擎,前端使用react框架,后端使用django框架
需要支持多用户登录,各自可以管理自己的博文(增删改查),管理是不公开的,但是博文是不需要登录就可以公开浏览的。
创建库并指定字符集及相关用户和权限
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‘;
查看如下
基本的应用创建本节不再累赘,如需详细情况请看前一章节
项目创建完毕目录如下
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/‘
启动查看如下
提供用户注册成立
提供用户登录处理
提供路由配置
用户注册接口实现
接受用户通过POST方法提交的注册信息,提交的数据是JSON格式数据
检查email 是否已经存在于数据库中,如果存在则返回错误状态码,如4xx,若不存在,则将用户提交的数据存入表中。
整个过程都采用AJAX异步过程,用户移交JSON数据,服务端获取数据返回处理,返回JSON。
URL:/user/reg
METHOD: POST
前端的时间只能通过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树中渲染,并显示出来。
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
在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__
python manage.py makemigrations
python manage.py migrate
结果如下
#!/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
由于上述返回为二进制数据,因此需要使用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
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
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
mkdir /var/log/blog/
邮箱检测
邮箱检测需要查询user表,需要使用User类的filter方法
email=email,前面是字段名,后面是变量名,查询后返回结果,如果查询有结果,则说明该email已经存在,返回400到前端。
用户存储信息
创建User 类实例,属性存储数据,最后调用save方法,Django默认是在save(),delete() 的时候提交事务数据,如果提交抛出任何异常,则需要捕获其异常
异常处理
出现获取输入框提交信息异常,就返回异常
查询邮箱存在,返回异常
save方法保存数据,有异常,则向外抛出,捕获异常
注意一点,django的异常类继承自HttpEResponse类,所以不能raise,只能return
前端通过状态验证码判断是否成功
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日志中返回数据和数据库数据如下
再次请求结果如下
HTTP协议是无状态协议,为了解决它产生了cookie和session技术
传统的session-cookie机制
浏览器发起第一次请求到服务器,服务器发现浏览器没有提供session id,就认为这是第一次请求。会返回一个新的session id 给浏览器端,浏览器只要不关闭,这个session id就会随着每一次请求重新发送给服务器端,服务器检查找到这个sessionid ,若查到,则就认为是同一个会话,若没有查到,则认为就是一个新的请求
session是会话级别的,可以在这个会话session中创建很多数据,链接或断开session清除,包括session id
这个session 机制还得有过期的机制,一段时间内如果没有发起请求,认为用户已经断开,就清除session,浏览器端也会清除相应的cookie信息
这种情况下服务器端保存着大量的session信息,很消耗服务器的内存字段,而且如果多服务器部署,还需要考虑session共享问题,如使用redis和memchached等解决方案。
既然服务器端就是需要一个ID来表示身份,那么不适用session也可以创建一个ID返回给客户端,但是,需要保证客户端不可篡改。
服务端生成一个标识,并使用某种算法对标识签名
服务端受到客户端发来的标识,需要检查签名
这种方案的缺点是,加密,解密需要消耗CPU计算机资源,无法让浏览器自己主动检查过期的数据以清除。这种技术成为JWT(JSON WEB TOKEN)
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中就有这样一个密码,如下
#!/usr/bin/poython3.6
#conding:utf-8
import jwt
import datetime
import base64
key=‘test‘
payload={‘name‘:‘demo‘,‘email‘:‘188@123.com‘,‘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)))
结果如下
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)进行返回
#!/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‘:‘188@123.com‘,‘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再次计算即便,然后和签名进行比较,一定对不上签名。
使用邮箱+ 密码方式登录
邮箱要求唯一就行了,但密码如何存储
早期,密码都是通过名为存储的
后来,使用了MD5存储,但是,目前也不安全,
MD5 是不可逆的,是非对称算法
但MD5是可以反查出来的,穷举的时间也不是很长MD5,MD5计算速度很快
加相同的前缀和后缀,则若穷举出两个密码。则也可以推断处理所有密码,加盐,使用hash(password+salt)的结果存储进入数据库中,就算拿到处理密码反查,也没用,但如果是固定加盐,则还是容易被找出规律,或者从源码中泄露,随机加盐,每次盐都变,就增加了破解的难度
暴力破解,什么密码都不能保证不被暴力破解,例如穷举,所以要使用慢hash算法,如bcrypt,就会让每一次计算都很慢,都是秒级别的,这样会导致穷举时间过长,在密码破解中,CPU是不能更换的,及不能实现分布式密码破解。
pip install bcrypt
#!/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
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() #创建一个实例,但实例中没有任何内容
返回结果如下
提供用户注册处理
提供用户登录处理
提供用户路由配置
接受用户通过POST提交的登录信息,提交的数据是JSON格式的数据
{
"email":"122@123",
"password":"demo"
}
从user 表中找到email 匹配的一条记录,验证密码是否正确
验证通过说明是合法用户登录,显示欢迎界面
验证失败返回错误码,如4xx整个过程采用AJAX异步过程,用户提交JSON数据,服务端获取数据后处理,返回JSON对象
API 地址
URL : /user/login
METHOD: POST
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)
]
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‘)
结果如下
如何获取浏览器提交的token信息?
1 使用header中的Authorization
通过这个header增加token信息
通过header 发送数据,所有方法可以是Post,Get
2 自定义header
JWT 来发送token
我们选择第二种方式认证
基本上所有业务都需要认证用户的信息
在这里比较时间戳,如果过期,则就直接抛出401未认证,客户端受到后就该直接跳转至登录页面
如果没有提交user id,就直接重新登录,若用户查到了,填充user
request -> 时间戳比较 -> user id 比较,向后执行
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机制,且用户信息自己的表来进行相关的管理,因此认证是通过自己的方式实现的
官方定义,在django的request和response处理过程中,由框架提供的hook钩子
中间键技术在1.10之后发生了变化
官方参考文档
https://docs.djangoproject.com/en/2.2/topics/http/middleware/
其相当于全局拦截器,能够拦截进来的和出去的数据
在需要认证的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‘)
请求参数如下
pyjwt 支持设置过期,在decode的时候,如果过期,则直接抛出异常,需要在payload中增加clamin exp,exp 要求是一个整数int的时间戳。
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‘)
功能 | 函数名 | Request 方法 | 路径 |
---|---|---|---|
发布 (增) | pub | post | /pub |
看文章(查) | get | get | /(\d+) |
列表(分页) | getall | get | / |
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‘))
]
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)
]
在 /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__
python manage.py makemigrations
python manage.py migrate
查看结果
/blog/post/admin.py中增加如下配置
from django.contrib import admin
from .models import Content, Post
admin.site.register(Content)
admin.site.register(Post)
查看如下
用户从浏览器端提交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的结果
根据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‘)
结果如下
发起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,
}
})
结果如下
至此,后端功能基本开发完成
原文:https://blog.51cto.com/11233559/2445866