1、Python3 博客后端开发

作者: Brinnatt 分类: python 道 发布时间: 2023-06-18 14:46

1.1、分析

多人使用的博客系统。采用 BS 架构实现。博客系统,需要用户管理、博文管理。

  • 用户管理:注册、增删改查用户。

  • 博文管理:增删改查博文

需要数据库,本次使用 Mysql 8.0,InnoDB 引擎。需要支持多用户登录,各自可以管理自己的博文(增删改查),管理是不公开的,但是博文是不需要登录就可以公开浏览的。

先不要思考过多的功能,先完成最小的核心需求代码。

1.2、数据库设计

创建数据库:

CREATE DATABASE IF NOT EXISTS blog;

需要用户表、文章表。

用户表 user:

  • id:唯一标识。
  • name:用户姓名,描述性字段。
  • email:电子邮箱,注册用信息,应该唯一,用作登录名。
  • password:存储密码。注意,不能存储明文,一般采用单向加密,例如MD5。
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(48) NOT NULL,
`email` varchar(64) NOT NULL,
`password` varchar(128) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

文章 post 要素:

  • id:唯一标识。
  • title:标题,描述字段。
  • author:博文作者要求必须是注册用户,那这里就是用户 userid 了,这是外键。
  • postdate:发布日期,日期类型
  • content:文章内容,博文内容可能很长,一般来说不会小于256个字符的。

一对多关系:一篇博文属于一个作者,一个作者有多篇博文。

content 字段的问题:

  1. 博客选取什么字段类型?
  2. 多大合适?
  3. 博文中图片如何处理?
  4. 适合和其它字段放在同一张表吗?

思考:

  1. 字段类型

    博文一般很长,不可能只有几百个字符,需要大文本字段。MySQL 中,选择 TEXT 类型,而不是 char 或者 varchar 类型。

  2. 大小

    text 类型是 65535 个字符,也不太够用,选择 longtext,有 2^32-1个字符长度,应该来说,足够使用了。

  3. 图片存储

    博文就像 HTML 一样,图片是通过路径信息将图片嵌入在内容中的,所以保存的内容还是字符串。

    图片来源有 2 种:

    • 外链,通过 URL 链接访问,本站不用存储该图片,但容易引起盗链问题。

    • 本站存储,需要提供博文的文本编辑器,提供图片上传到网站存储的功能,并生成图片 URL,这个 URL 嵌入博客正文中。不会有盗链的问题,但要解决众多图片存储问题、水印问题、临时图片清理、在线压缩问题等等。

    本次博客项目不实现图片功能。

  4. 字段考虑

    content 字段存储大文本,一般不和数据频繁查询的字段放在一张表中,需要拆到另一张表中。

CREATE TABLE `post` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT,
    `title` varchar(256) NOT NULL,
    `author_id` int(11) NOT NULL,
    `postdate` datetime NOT NULL,
    PRIMARY KEY (`id`),
    KEY `author_id` (`author_id`),
    CONSTRAINT `fk_post_user` FOREIGN KEY (`author_id`) REFERENCES `user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `content` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT,
    `content` longtext NOT NULL,
    PRIMARY KEY (`id`),
    CONSTRAINT `fk_content_post` FOREIGN KEY (`id`) REFERENCES `post` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

注:这里的 SQL 脚本不要用来生成表,使用 ORM 工具来创建表,用来检查实体类构建是否正确。

用户完成的功能有:登录、注册、登出。user 表基本满足。

博客:用户发文、文章列表、文章详情。post、content表基本满足。

1.3、项目构建

在 Pycharm 中新建一个项目:

python3_blog_createproj

使用虚拟环境,Python 使用 3.10 版本:

python3_blog_createproj1

注意:项目创建后,要确认一下 python 的虚拟环境是否符合自己的预期,如下所示。如果不是,可以在 settings 中修改。

python3_blog_createproj2

本次项目使用 Django 开发后台,下面就开始 Django 之旅。

1.4、Django

1.4.1、概述

Django 采用 MVC 架构设计,是开源的 WEB 快速开发框架。

优点:

  • 能够快速开发,如 Auth, Cache, 模板。

  • MVC 设计模式,结构清晰。

  • 实用的管理后台。

  • 自带 ORM, Template, Form, Auth 核心。

  • 简洁的 url 设计。

  • 周边插件丰富。

缺点:

  • 重,因为东西大而全。

所以 Django 的设计目标就是一款大而全,便于企业快速开发项目的框架,因此企业应用较广。

1.4.2、Django 版本

Django Version Python Version
2.2 3.5, 3.6, 3.7, 3.8 (added in 2.2.8), 3.9 (added in 2.2.17)
3.1 3.6, 3.7, 3.8, 3.9 (added in 3.1.3)
3.2 3.6, 3.7, 3.8, 3.9, 3.10 (added in 3.2.9)
4.0 3.8, 3.9, 3.10
4.1 3.8, 3.9, 3.10, 3.11 (added in 4.1.3)

1.4.3、安装 Django

Python 使用 3.10

Django 的下载地址 https://www.djangoproject.com/download/

$ pip install django==4.0

本次使用 Django 4.0 版本,请在虚拟环境中安装。

安装完 Django,在虚拟环境路径中会提供一个 django-admin 的命令。Django 旧版本是在 Lib/site-packages/django/bin 目录下提供 django-admin.py 脚本,新版本是在 Scripts/ 下直接提供 django-admin 二进制文件。

(venv) PS D:\JetBrains\BrinnattProjects\blog> django-admin    

Type 'django-admin help <subcommand>' for help on a specific subcommand. 

Available subcommands:                                                   

[django]                                                                 
    check                                                                
    compilemessages                                                      
    createcachetable                                                     
    dbshell                                                              
    diffsettings                                                         
    dumpdata                                                             
    flush
    inspectdb
    loaddata
    makemessages
    makemigrations
    migrate
    runserver
    sendtestemail
    shell
    showmigrations
    sqlflush
    sqlmigrate
    sqlsequencereset
    squashmigrations
    startapp
    startproject
    test
    testserver

注意:本文如若未特殊声明,所有的命令操作都在项目根目录下。

1.4.4、创建 django 项目

$ django-admin startproject blog .

上句命令就在当前项目根目录中构建了 Django 项目的初始文件。 代表项目根目录。

blog/
├── asgi.py
├── __init__.py
├── settings.py
├── urls.py
└── wsgi.py

重要文件说明:

  • manage.py:管理本项目的命令行工具。应用创建、数据库迁移、等都使用它完成。

  • blog/settings.py:本项目的配置文件。数据库参数等。

  • blog/urls.py:URL 路径映射配置。默认情况下,只配置 /admin 的路由。

  • blog/wsgi:定义 WSGI 接口信息。一般无须改动。

  • blog/asgi.py:定义 ASGI 接口信息。相对于传统的 WSGI(Web Server Gateway Interface),ASGI 提供了更高级别的异步处理能力,使得 Django 可以更好地支持异步代码、WebSockets、长轮询等实时通信功能。

1.4.5、数据库配置

使用数据库,需要修改默认的数据库配置。

在主项目的 settings.py 中有关于 DATABASES 的配置。默认使用 sqlite,修改为 mysql。

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'blog',
        'USER': 'brinnatt',
        'PASSWORD': 'brinnatt',
        'HOST': '192.168.136.128',
        'PORT': '3306',
    }
}

HOST 数据库主机。缺省是空字符串,代表 localhost。如果是 / 开头表示使用 Unix Socket 连接。

PORT 端口,USER 用户名,PASSWORD 密码,NAME 库名。

OPTIONS 选项,字典,参考 MySQL 文档。

数据库引擎 ENGINE,内建的引擎有:

  • 'django.db.backends.postgresql'
  • 'django.db.backends.mysql'
  • 'django.db.backends.sqlite3'
  • 'django.db.backends.oracle'

1.4.6、MySQL 数据库驱动

https://docs.djangoproject.com/en/4.2/ref/databases/

Django支持 MySQL 8+

Django 官方推荐使用本地驱动 mysqlclient 1.4.3+

$ pip install mysqlclient
Collecting mysqlclient
  Downloading mysqlclient-2.1.1-cp310-cp310-win_amd64.whl (178 kB)
     ---------------------------------------- 178.4/178.4 kB 182.6 kB/s eta 0:00:00
Installing collected packages: mysqlclient
Successfully installed mysqlclient-2.1.1

安装 mysqlclient v1.3.x 版本,会遇到下面的问题,可参考下面的方法:

windows下安装错误 error: Microsoft Visual C++ 14.0 is required.解决方法
1、下载Visual C++ Redistributable Packages 2015、2017安装,但是即使安装后,确实看到了V14库,也不保证安装mysqlclient就成功
2、直接安装编译好的wheel文件
mysqlclient-1.3.13-cp35-cp35m-win_amd64.whl ,python 3.5使用
mysqlclient-1.3.13-cp36-cp36m-win_amd64.whl ,python 3.6使用
$ pip install mysqlclient-1.3.13-cp35-cp35m-win_amd64.whl
直接下载对应的版本:https://archive.linux.duke.edu/pypi/simple/mysqlclient/

1.4.7、创建应用

创建用户应用 $ python manage.py startapp user

该应用完成以下功能:

  • 用户注册

  • 用户登录

创建应用后,项目根目录下产生一个 user 目录,有如下文件:

  • admin.py:管理站点模型的声明文件。

  • models.py:模型层 Model 类定义。

  • views.py:定义 URL 响应函数。

  • apps.py:应用的信息定义文件。

1.4.8、注册应用

在 settings.py 中,增加 user 应用。目的是为了后台管理 admin 使用,或迁移 migrate 使用。

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'user',
]

1.4.9、模型 Model

字段类 说明
AutoField 自增的整数字段。
如果不指定,django 会为模型类自动增加主键字段。
BooleanField 布尔值字段,True 和 False
对应表单控件 CheckboxInput
NullBooleanField 比 BooleanField 多一个 null 值
CharField 字符串,max_length 设定字符长度
对应表单控件 TextInput
TextField 大文本字段,一般超过 4000 个字符使用
对应表单控件 Textarea
IntegerField 整数字段
BigIntegerField 更大整数字段,8 字节
DecimalField 使用 Python 的 Decimal 实例表示十进制浮点数。max_digits 总位数,decimal_places 小数点后的位数
FloatField Python 的 Float 实例表示的浮点数
DateField 使用 Python 的 datetime.date 实例表示的日期
auto_now=False 每次修改对象自动设置为当前时间。
auto_now_add=False 对象第一次创建时自动设置为当前时间。auto_now_add、auto_now、default 互斥。
对应控件为 TextInput,关联了一个 Js 编写的日历控件。
TimeField 使用 Python 的 datetime.time 实例表示的时间,参数同上。
DateTimeField 使用 Python 的 datetime.datetime 实例表示的时间,参数同上。
FileField 一个上传文件的字段。
ImageField 继承了FileField的所有属性和方法,但是对上传的文件进行校验,确保是一个有效的图片。

字段选项:

说明
db_column 表中字段的名称。如果未指定,则使用属性名。
primary_key 是否主键
unique 是否是唯一键
default 缺省值。这个缺省值不是数据库字段的缺省值,而是新对象产生的时候被填入的缺省值
null 表的字段是否可为 null,默认为 False。
blank Django 表单验证中,是否可以不填写,默认为 False。
db_index 字段是否有索引。

关系类型字段类:

说明
ForeignKey 外键,表示一对多
ForeignKey('production.Manufacturer')
自关联 ForeignKey('self')
ManyToManyField 表示多对多
OneToOneField 表示一对一

一对多时,自动创建会增加 _id 后缀。

从一访问多,使用 对象.小写模型类_set

从一访问一,使用 对象.小写模型类

访问 id 对象.属性_id

1.4.10、创建 User 的 Model 类

基类 models.Model

表名不指定,默认使用 <appname>_<model_name>。使用 Meta 类修改表名。

from django.db import models

# Create your models here.
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)

    def __repr__(self):
        return '<user {} {}>'.format(self.id, self.name)

    __str__ = __repr__

1.4.11、迁移 Migration

迁移:从模型定义生成数据库的表。

1、生成迁移文件

(venv) PS D:\JetBrains\BrinnattProjects\blog> python manage.py makemigrations
Migrations for 'user':
  user\migrations\0001_initial.py
    - Create model User

生成如下文件
user
├─ migrations
    ├─ 0001_initial.py
    └─ __init__.py

修改过 Model 类,还需要调用 makemigrations,然后 migrate,迁移文件的序号会增加。

注意:迁移的应用必须在 settings.py 的 INSTALLED_APPS 中注册。

# 0001_initial.py 文件内容如下
# Generated by Django 4.0 on 2023-05-23 12:09

from django.db import migrations, models

class Migration(migrations.Migration):

    initial = True

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name='User',
            fields=[
                ('id', models.AutoField(primary_key=True, serialize=False)),
                ('name', models.CharField(max_length=48)),
                ('email', models.CharField(max_length=64, unique=True)),
                ('password', models.CharField(max_length=128)),
            ],
            options={
                'db_table': 'user',
            },
        ),
    ]

2、执行迁移生成数据库的表

$ python manage.py migrate

执行了迁移,还同时生成了 admin 管理用的表。

查看数据库,user 表创建好了,字段设置完全正确。

1.4.12、Django 后台管理

1、创建管理员

管理员用户名 brinnatt,密码 brinnatt。

(venv) PS D:\JetBrains\BrinnattProjects\blog> python manage.py createsuperuser
Username (leave blank to use 'brinnatt'): 
Email address: brinnatt@gmail.com
Password:
Password (again):
The password is too similar to the username.
Bypass password validation and create user anyway? [y/N]: y
Superuser created successfully.

2、本地化

settings.py 中设置语言、时区,语言名称可以查看 django\contrib\admin\locale 目录。

LANGUAGE_CODE = 'zh-Hans' #'en-us'
USE_TZ = True
TIME_ZONE = 'Asia/Shanghai' #'UTC'

3、启动 WEB Server

python manage.py runserver

默认启动 8000 端口。

python3_blog_createproj3

4、登录后台管理

后台登录地址 http://127.0.0.1:8000/admin

python3_blog_createproj4

5、注册应用模块

在 user 应用的 admin.py 添加:

from django.contrib import admin
from .models import User

# Register your models here.
admin.site.register(User)  # 注册

python3_blog_createproj5

user 就可以在后台进行增删改了。

1.4.13、路由

编写 WSGI 框架项目中,路由功能就是实现 URL 模式匹配和处理函数之间的映射。对于 Django 也是如此。

路由配置要在项目的 urls.py 中配置,也可以多级配置,在每一个应用中,建立一个 urls.py 文件配置路由映射。

urls.py 内容如下:

from django.contrib import admin
from django.urls import path, re_path
from django.http import HttpRequest, HttpResponse

def index(request: HttpRequest):
    """视图函数:请求进来返回响应"""
    return HttpResponse(b'welcome to www.brinnatt.com')

urlpatterns = [
    path('admin/', admin.site.urls),
    re_path(r'^$', index),
    re_path(r'^index$', index),
]

path 函数和 re_path 函数请参见 https://docs.djangoproject.com/en/4.2/ref/urls/

re_path(r'^index/$', index)

http://127.0.0.1:8000/index/ 可以访问

http://127.0.0.1:8000/index 可以访问,但会补一个 /

re_path(r'^index$', index)

http://127.0.0.1:8000/index 可以访问

http://127.0.0.1:8000/index/ 不可以访问

请求信息测试和 JSON 响应:

from django.http import HttpRequest, HttpResponse, JsonResponse

def index(request: HttpRequest):
    """视图函数:请求进来返回响应"""
    d = {}
    d['method'] = request.method
    d['path'] = request.path
    d['path_info'] = request.path_info
    d['GETparams'] = request.GET
    return JsonResponse(d)

在项目中首页多数使用 HTML 显示,为了加载速度快,一般多使用静态页面。如果首页内容多,还有部分数据需要变化,变化部分使用 AJAX 技术从后台获取。

本次,为了使用模板技术,首页采用 Django 的模板技术实现。

1.5、Django 模板

如果使用 react 实现前端页面,其实 Django 就没有必须使用模板,它其实就是一个后台服务程序,接收请求,响应数据。接口设计就可以是纯粹的 Restful 风格。

模板的目的就是为了可视化,将数据按照一定布局格式输出,而不是为了数据处理,所以一般不会有复杂的处理逻
辑。

模板的引入实现了业务逻辑和显示格式的分离,这样,在开发中,就可以分工协作,页面开发完成页面布局设计,后台开发完成数据处理逻辑。

Python 的模板引擎默认使用 Django template language (DTL) 构建。

1.5.1、模板配置

在 settings.py 中,设置模板项目的路径:

from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR.joinpath('templates')],
        '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',
            ],
        },
    },
]

DIRS 列表,定义模板文件的搜索路径顺序。

APP_DIRS 是否在每个已经安装的应用中查找模板。应用自己目录下有 templates 目录,例如 django/contrib/admin/templates。如果应用需要可分离、可重用,建议把模板放到应用目录下。

BASE_DIR 是项目根目录,BASE_DIR.joinpath('templates') 就是在 manage.py 这一层建立一个目录 templates。这个路径就是以后默认找模板的地方。

1.5.2、模板渲染

1、加载模板

模板是一个文件,需要从磁盘读取并加载。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>www.brinnatt.com</title>
</head>
<body>
我是模板,数据是{{content}}
</body>
</html>

将模板 index.html 放入到 templates 目录下。使用浏览器访问首页,可以正常显示。

2、渲染

模板需要使用数据来填充渲染,生成 HTML 文件内容。

from django.template import loader

def index(request: HttpRequest):
    """视图函数:请求进来返回响应"""
    template = loader.get_template('index.html')  # 加载器模块搜索模板并加载它
    return HttpResponse(template.render({'content': 'www.brinnatt.com'}))

使用 render 快捷渲染函数

render(request, template_name, context=None)

  • 返回 HttpResponse 对象。

  • template_name 模板名称。

  • context 数据字典。

from django.shortcuts import render

def index(request: HttpRequest):
    """视图函数:请求进来返回响应"""
    return render(request, 'index.html', {'content': 'python.brinnatt.com'})

1.5.3、DTL 语法

变量,标签,注释,过滤器。

1.5.3.1、变量

语法 {{ variable }}
变量名由字母、数字、下划线、点号组成。

点号使用的时候,例如 foo.bar,遵循以下顺序:

  1. 字典查找,例如 foo["bar"],把 foo 当做字典,bar 当做 key。
  2. 属性或方法的查找,例如 foo.bar,把 foo 当做对象,bar 当作属性或方法。
  3. 数字索引查找,例如 foo[bar],把 foo 当做列表一样,使用索引访问。

如果变量未能找到,则缺省插入空字符串 ''

在模板中调用方法,不能加小括号,自然也不能传递参数。

{ my_dict.keys } 这样是对的,不能写成 { my_dict.keys() }

1.5.3.2、模板标签

if/else 标签:

{% if condition %}
    ... display
{% endif %}

或者:

{% if condition1 %}
    ... display 1
{% elif condition2 %}
    ... display 2
{% else %}
    ... display 3
{% endif %}

条件也支持 and、or、not。

注意,因为这些标签是断开的,所以不能像 Python 一样使用缩进就可以表示出来,必须有个结束标签,例如 endif、endfor。

for 标签:

<ul>
{% for athlete in athlete_list %}
    <li>{{ athlete.name }}</li>
{% endfor %}
</ul>

{% for person in person_list %}
    <li> {{ person.name }} </li>
{% endfor %}
变量 说明
forloop.counter 当前循环从 1 开始的计数
forloop.counter0 当前循环从 0 开始的计数
forloop.revcounter 从循环的末尾开始倒计数到 1
forloop.revcounter0 从循环的末尾开始到计数到 0
forloop.first 第一次进入循环
forloop.last 最后一次进入循环
forloop.parentloop 循环嵌套时,内层当前循环的外层循环

给标签增加一个 reversed 使得该列表被反向迭代:

{% for athlete in athlete_list reversed %}
...
{% empty %}
... 如果被迭代的列表是空的或者不存在,执行empty
{% endfor %}

可以嵌套使用 % for % 标签:

{% for athlete in athlete_list %}
    <h1>{{ athlete.name }}</h1>
    <ul>
    {% for sport in athlete.sports_played %}
        <li>{{ sport }}</li>
    {% endfor %}
    </ul>
{% endfor %}

testfor.html 模板:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>测试 for 循环</title>
</head>
<body>
字典是dict(zip('abced', range(1,6)))
<ul>
    {% for k,v in dct.items %}
        <li>{{forloop.counter}} {{k}} {{v}}</li>
    {% endfor %}
</ul>
<hr>

<ul>
    {% for k,v in dct.items %}
        <li>{{forloop.counter0}} {{k}} {{v}}</li>
    {% endfor %}
</ul>
<hr>

<ul>
    {% for k,v in dct.items %}
    {{ forloop.first }}
    {{ forloop.last }}
        <li>{{forloop.revcounter0}} {{k}} {{v}}</li>
    {% endfor %}
</ul>
<hr>

<ul>
    {% for k,v in dct.items %}
        <li>{{forloop.revcounter}} {{k}} {{v}}</li>
    {% endfor %}
</ul>
<hr>

</body>
</html>

把 index 函数改一下:

from django.shortcuts import render

def index(request: HttpRequest):
    """视图函数:请求进来返回响应"""
    return render(request, 'testfor.html', {"dct": dict(zip('abced', range(1, 6)))})

ifequal/ifnotequal 标签:

% ifequal % 标签比较两个值,当他们相等时,显示 % ifequal %% endifequal % 之间的全部值。

下面的例子比较两个模板变量 user 和 currentuser:

{% ifequal user currentuser %}
    <h1>Welcome!</h1>
{% endifequal %}

% if % 类似,% ifequal % 支持可选的 % else % 标签:

{% ifequal section 'sitenews' %}
    <h1>Site News</h1>
{% else %}
    <h1>No News Here</h1>
{% endifequal %}

1.5.3.3、注释标签

单行注释 # #

多行注释 % comment %} ... {% endcomment %

{# 这是一个注释 #}

{% comment %}
这是多行注释
{% endcomment %}

1.5.3.4、过滤器

模板过滤器可以在变量被显示前修改它。

语法 { 变量|过滤器 }

过滤器使用管道字符 |,例如 { name|lower }{ name } 变量被过滤器 lower 处理后,大写字符转换为小写字符。

过滤管道可以被套接 ,一个过滤器管道的输出又可以作为下一个管道的输入,例如 { my_list|first|upper },取列表第一个元素并将其转化为大写。

过滤器传参:

有些过滤器可以传递参数,过滤器的参数跟随冒号之后并且总是以双引号包含。

例如:{ bio|truncatewords:"30" },截取显示变量 bio 的前 30 个词。

{ my_list|join:"," },将 my_list 的所有元素使用 , 逗号连接起来。

其他过滤器:

过滤器 说明 举例
first 取列表第一个元素
last 取列表最后一个元素
yesno 变量可以是 True、False、None。
对应 yesno 的三个参数值。
True 对应第一个。
False 对应第二个。
None 对应第三个。
如果参数只有 2 个,None 等效 False 处理。
{ value | yesno:"yeah,no,maybe" }
add 加法。参数是负数就是减法。 数字加 { value |add:"100"}
列表合并 {mylist | add:newlist}
divisibleby 能否被整除 { value | divisibleby:"3"}能被3整除返回True
addslashes 在反斜杠、单引号或者双引号前面加上反斜杠 { value | addslashes }
length 返回变量的长度 % if my_list | length > 1%
default 变量等价False则使用缺省值 { value |default:"nothing" }
default_if_none 变量为None使用缺省值 { value |default_if_none:"nothing"}

练习:要求模板中列表输出多行数据,要求奇偶行颜色不同。

<ul>
{% for k,v in dct.items %}
    <li style='color:{{forloop.revcounter0|divisibleby:"2"|yesno:"red,blue"}}'>
{{forloop.revcounter}} {{k}} {{v | add:"100"}}</li>
{% endfor %}
</ul>

1.6、用户功能设计与实现

  1. 提供用户注册处理
  2. 提供用户登录处理
  3. 提供路由配置

1.6.1、用户注册接口设计

接收用户通过 Post 方法提交的注册信息,提交的数据是 JSON 格式数据。

检查 email 是否已存在于数据库表中,如果存在返回错误状态码,例如 4xx;如果不存在,将用户提交的数据存入表中。

整个过程都采用 AJAX 异步过程,用户提交 JSON 数据,服务端获取数据后处理,返回 JSON。

URL:/user/reg

METHOD:POST

1.6.2、路由配置

为了避免项目中的 urls.py 条目过多,也为了让应用自己管理路由,采用多级路由。

# blog/urls.py 中
urlpatterns = [
    path('admin/', admin.site.urls),
    re_path(r'^$', index),
    re_path(r'^index/$', index),
    re_path(r'^user/', include('user.urls'))
]

include 函数参数写 应用.路由模块,该函数就会动态导入指定的包的模块,从模块里面读取 urlpatterns,返回三元组。

url 函数第二参数如果不是可调用对象,如果是元组或列表,则会从路径中除去已匹配的部分,将剩余部分与应用中的 urls 模块的 urlpatterns 进行匹配。

# 新建user/urls.py
from django.urls import re_path
# 临时测试用reg视图函数
from django.http import HttpRequest, HttpResponse

def reg(request: HttpRequest):
    return HttpResponse(b'user.reg')

urlpatterns = [
    re_path(r'^reg$', reg)
]

浏览器中输入 http://127.0.0.1:8000/user/reg 测试一下,可以看到响应的数据。下面开始完善视图函数。

1.6.3、视图函数

在 user/views.py 中编写视图函数 reg,路由做响应的调整。

测试 JSON 数据

使用 POST 方法,提交的数据类型为 application/json,json 字符串要使用双引号。

这个数据是登录和注册用的,由客户端提交。

{
    "password":"abc",
    "name":"wayne",
    "email":"wayne@magedu.com"
}

JSON 数据处理

simplejson 比标准库方便好用,功能强大。

$ pip install simplejson

浏览器端提交的数据放在了请求对象的 body 中,需要使用 simplejson 解析,解析的方式同 json 模块,但是 simplejson 更好用。

错误处理

Django 中有很多异常类,定义在 django.http 下,这些类都继承自 HttpResponse。

# Create your views here.
# user/views.py中
from django.http import HttpRequest, HttpResponse, HttpResponseBadRequest, JsonResponse
import simplejson

def reg(request: HttpRequest):
    print(request.POST)
    print(request.body)
    payload = simplejson.loads(request.body)
    try:
        email = payload['email']
        name = payload['name']
        password = payload['password']
        print(email, name, password)
        return JsonResponse({})  # 如果正常,返回json数据
    except Exception as e:  # 有任何异常,都返回
        return HttpResponseBadRequest()  # 这里返回实例,这不是异常类

将上面代码增加邮箱检查、用户信息保存功能,就要用到 Django 的模型操作。

1.6.4、注册代码 v1

# Create your views here.
# user/views.py中
from django.http import HttpRequest, HttpResponse, HttpResponseBadRequest, JsonResponse
import simplejson
from .models import User

# 注册函数
def reg(request: HttpRequest):
    print(request.POST)
    print(request.body)
    payload = simplejson.loads(request.body)
    try:
        # 有任何异常,都返回400,如果保存数据出错,则向外抛出异常
        email = payload['email']
        query = User.objects.filter(email=email)
        print(query)
        print(type(query), query, query)  # 查看SQL语句
        if query:
            return HttpResponseBadRequest()  # 这里返回实例,这不是异常类
        name = payload['name']
        password = payload['password']
        print(email, name, password)

        user = User()
        user.email = email
        user.name = name
        user.password = password

        try:
            user.save()
            return JsonResponse({'user': user.id})  # 如果正常,返回json数据
        except:
            raise
    except Exception as e:  # 有任何异常,都返回
        print(e)
        return HttpResponseBadRequest()  # 这里返回实例,这不是异常类

邮箱检查

邮箱检查需要查 user 表,需要使用 User 类的 filter 方法。

email=email,前面是字段名 email,后面是 email 变量。查询后返回结果,如果查询有结果,则说明该 email 已经存在,返回 400 到前端。

用户信息存储

创建 User 类实例,属性存储数据,最后调用 save 方法。Django 默认是在 save()、delete() 的时候事务自动提交。

如果提交抛出任何错误,则捕获此异常做相应处理。

异常处理

  • 查询邮箱存在,返回异常。
  • save() 方法保存数据,有异常,则向外抛出,捕获返回异常。
  • 注意一点,Django 的异常类继承自 HttpResponse 类,所以不能 raise,只能 return。
  • 前端通过状态码判断是否成功。

下面我们说说模型类的操作。

1.7、模型操作

1.7.1、管理器对象

Django 会为模型类提供一个 objects 对象,它是 django.db.models.manager.Manager 类型,用于与数据库交互。

当定义模型类的时候没有指定管理器,则 Django 会为模型类提供一个 objects 的管理器。

如果在模型类中手动指定管理器,Django 就不再提供默认的 objects 的管理器了。

管理器是 Django Model 进行数据库查询操作的接口,Django Apps 的每个模型都至少拥有一个管理器。

1.7.2、Django ORM

数据的校验 validation 是在对象的 Save、update 方法上。

python3_blog_ORM

对模型对象的 CRUD,被 Django ORM 转换成相应的 SQL 语句操作不同的数据源。

1.7.3、查询

1.7.3.1、查询集

查询会返回结果的集,它是 django.db.models.query.QuerySet 类型。它是惰性求值,和 sqlalchemy 一样。结果就是查询的集。它是可迭代对象。

  1. 惰性求值

    创建查询集不会带来任何数据库的访问,直到调用数据时,才会访问数据库。在迭代、序列化、if 语句中都会立即求值。

  2. 缓存

    每一个查询集都包含一个缓存,来最小化对数据库的访问。

    新建查询集,缓存为空。首次对查询集求值时,会发生数据库查询,Django 会把查询的结果存在这个缓存中,并返回请求的结果,接下来对查询集求值将使用缓存的结果。

观察下面的 2 个例子:

1、没有使用缓存,每次都要去查库,查了 2 次库。

[user.name for user in User.objects.all()]
[user.name for user in User.objects.all()]

2、下面的语句使用缓存,因为使用同一个结果集。

qs = User.objects.all()
[user.name for user in qs]
[user.name for user in qs]

1.7.3.2、查询集切片

查询集对象可以直接使用索引下标的方式(不支持负索引),相当于 SQL 语句中的 limit 和 offset 子句。

注意使用索引返回的新的结果集,依然是惰性求值,不会立即查询。

qs = User.objects.all()[20:40]
# LIMIT 20 OFFSET 20

qs = User.objects.all()[20:30]
# LIMIT 10 OFFSET 20

1.7.3.3、过滤器

返回查询集的方法,称为过滤器,如下:

名称 说明
all() 返回结果集
filter() 过滤,返回满足条件的数据
exclude() 排除,排除满足条件的数据
order_by() 排序
values() 返回一个对象字典的列表。像 json。

filter(k1=v1).filter(k2=v2) 等价于 filter(k1=v1, k2=v2)

filter(pk=10) 这里 pk 指的就是主键,不用关心主键字段名,当然也可以使用主键名 filter(emp_no=10)

返回单个值的方法:

名称 说明
get() 仅返回单个满足条件的对象。
如果未能返回对象则抛出 DoesNotExist 异常;如果能返回多条,抛出 MultipleObjectsReturned 异常。
count() 返回当前查询的总条数。
first() 返回第一个对象。
last() 返回最后一个对象。
exist() 判断查询集中是否有数据,如果有则返回 True。
user = User.objects.filter(email=email).get() # 期待查询集只有一行,否则抛出异常

user = User.objects.get(email=email) # 返回不是查询集,而是一个User实例,否则抛出异常

user = User.objects.get(id=1) # 使用主键查询,也可以使用 pk=1

user = User.objects.first() # 使用limit 1查询,查到返回一个实例,查不到返回None

user = User.objects.filter(pk=3, email=email).first() # and 条件

1.7.3.4、字段查询表达式

字段查询表达式可以作为 filter()、exclude()、get() 的参数,实现 where 子句。

语法: 属性(字段)名称__比较运算符=值

注意:属性名和运算符之间使用双下划线。

比较运算符如下:

名称 举例 说明
exact filter(isdeleted=False)
filter(isdeleted__exact=False)
严格等于,可省略不写
contains exclude(title__contains='天') 是否包含,大小写敏感,等价于 like '%天%'
statswith
endswith
filter(title__startswith='天') 以什么开头或结尾,大小写敏感
isnull
isnotnull
filter(title__isnull=False) 是否为 null
iexact
icontains
istartswith
iendswith
i 的意思是忽略大小写
in filter(pk__in=[1,2,3,100]) 是否在指定数据范围中
gt、gte、lt、lte filter(id__gt=3)
filter(pk__lte=6 )
filter(pub_date__gt=date(2000,1,1))
大于、大于等于,小于、小于等于
year、month、day
week_day
hour、minute、
second
filter(pub_date__year=2000) 对日期类型属性处理

1.7.3.5、Q 对象

虽然 Django 提供传入条件的方式,但是不方便,它还提供了 Q 对象来解决。

Q 对象是 django.db.models.Q,可以使用 &(and)|(or) 操作符来组成逻辑表达式。~ 表示 not。

from django.db.models import Q
User.objects.filter(Q(pk__lt=6)) # 不如直接写User.objects.filter(pk<6)

User.objects.filter(pk__gt=6).filter(pk_lt=10) # 与
User.objects.filter(Q(pk_gt=6) & Q(pk_lt=10)) # 与
User.objects.filter(Q(pk=6) | Q(pk=10)) # 或
User.objects.filter(~Q(pk__lt<6)) # 非

可使用 & | 和 Q 对象来构造复杂的逻辑表达式。

过滤器函数可以使用一个或多个 Q 对象。

如果混用关键字参数和 Q 对象,那么 Q 对象必须位于关键字参数的前面。所有参数都将 and 在一起。

1.8、注册接口设计完善

1.8.1、认证

HTTP 协议是无状态协议,为了解决它产生了 cookie 和 session 技术。

传统的 session-cookie 机制:

  1. 浏览器发起第一次请求到服务器,服务器发现浏览器没有提供 session id,就认为这是第一次请求,会返回一个新的 session id 给浏览器端。

  2. 浏览器只要不关闭,这个 session id 就会随着每一次请求重新发给服务器端。

  3. 服务器端查找这个 session id,如果查到,就认为是同一个会话。如果没有查到,就认为是新的请求。

session 是会话级的,可以在这个会话 session 中创建很多数据,连接断开 session 清除,包括 session id。

这个 session id 还得有过期的机制,一段时间如果没有发起请求,认为用户已经断开,就清除 session。浏览器端也会清除响应的 cookie 信息。

服务器端保存着大量 session 信息,很消耗服务器内存,而且如果多服务器部署,还要考虑 session 共享的问题,比如 redis、memcached 等方案。

无 session 方案:

  1. 既然服务端就是需要一个 ID 来表示身份,那么不使用 session 也可以创建一个 ID 返回给客户端。但是,要保证客户端不可篡改。

  2. 服务端生成一个标识,并使用某种算法对标识签名。

  3. 服务端收到客户端发来的标识,需要检查签名。

这种方案的缺点是,加密、解密需要消耗 CPU 计算资源,无法让浏览器自己主动检查过期的数据以清除。

这种技术称作 JWT(Json WEB Token)。

1.8.2、JWT

JWT(Json WEB Token)是一种采用 Json 加解密传输信息的方式。这里使用 PyJWT,它是 Python 对 JWT 的实现。

安装包:https://pypi.org/project/PyJWT/

查文档:https://pyjwt.readthedocs.io/en/stable/

$ pip install pyjwt

jwt 原理:

import jwt
import base64
from jwt import algorithms
import json

key = 'secret'
token = jwt.encode({'payload': 'bingo147'}, key, 'HS256')
print(11, '-->', token)
print(12, '-->', jwt.decode(token, key, algorithms=['HS256']))

header, payload, signature = token.split('.')
print(21, '-->', header)
print(22, '-->', payload)
print(23, '-->', signature)

def polish(s: str):
    # 为base64编码补齐等号
    rem = len(s) % 4
    return s + '=' * rem

print(31, '-->', 'header=', base64.urlsafe_b64decode(polish(header)))
print(32, '-->', 'payload=', base64.urlsafe_b64decode(polish(payload)))
print(33, '-->', 'signature=', base64.urlsafe_b64decode(polish(signature)))

# 根据jwt算法,重新生成签名
# 1.获取算法对象
alg = algorithms.get_default_algorithms()['HS256']
pkey = alg.prepare_key(key)  # key 为 secret

# 2.获取前两部分 header.payload
signing_input, _, _ = token.rpartition('.')
print(41, '-->', signing_input)

# 3.使用key签名
signature = alg.sign(signing_input.encode(), pkey)
print('-' * 80)
print(42, '-->', signature)
print(43, '-->', base64.urlsafe_b64encode(signature))

print(51, '-->', base64.urlsafe_b64encode(json.dumps({'payload': 'abc123'}).encode()))

由此,可知 jwt 生成的 token 分为三部分:

1、header,由数据类型、加密算法构成。

2、payload,负载就是要传输的数据,一般来说放入 python 对象即可,会被 json 序列化的。

3、signature,签名部分。是前面 2 部分数据分别 base64 编码后使用点号连接后,加密算法使用 key 计算好一个结果,再被 base64 编码,得到签名。

所有数据都是明文传输的,只是做了 base64,如果是敏感信息,请不要使用 jwt。

数据签名的目的不是为了隐藏数据,而是保证数据不被篡改。如果数据篡改了,发回到服务器端,服务器使用自己的 key 再计算一遍,然后进行签名比对,一定对不上签名。

1.8.3、密码

使用邮箱 + 密码方式登录。

邮箱要求唯一就行了,但是,密码如何存储?早期,都是明文的存储方式。

后来,使用 MD5 存储,但是,目前也不安全,网上有很多 MD5 的网站,可以使用反查方式找到密码。

加盐,使用 hash(password + salt) 的结果存入数据库中,就算拿到数据库的密码反查,也没有用了。如果是固定加盐,还是容易被找到规律,或者从源码中泄露。随机加盐,每一次盐都改变,就增加了破解的难度。

暴力破解,什么密码都不能保证不被暴力破解,例如穷举。所以要使用慢 hash 算法,例如 bcrypt,就会让每一次计算都很慢,都是秒级的,这样穷举的时间就会很长,为了破解一个密码,在当前 CPU 或者 GPU 的计算能力下可能需要几十年以上。

$ pip install bcrypt
import bcrypt
import datetime

password = b'123456'
# 每次拿到的盐都不一样
print(1, '-->', bcrypt.gensalt())
print(2, '-->', bcrypt.gensalt())

# 拿到的盐相同,计算得到的密文相同
print('=========same salt ==========')
salt = bcrypt.gensalt()
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(10, '-->', 'duration={}'.format(delta))

# 检验时长
start = datetime.datetime.now()
y1 = bcrypt.checkpw(password, x)
delta = (datetime.datetime.now() - start).total_seconds()
print(11, '-->', y1, 'duration={}'.format(delta))
start = datetime.datetime.now()
y2 = bcrypt.checkpw(b'1', x)
delta = (datetime.datetime.now() - start).total_seconds()
print(12, '-->', y2, 'duration={}'.format(delta))

从耗时看出,bcrypt 加密、验证非常耗时,所以如果穷举,非常耗时。而且碰巧攻破一个密码,由于盐不一样,还得穷举另一个。

salt=b'$2b$12$jwBD7mg9stvIPydF2bqoPO'
b'$2b$12$jwBD7mg9stvIPydF2bqoPOodPwWYVvdmZb5uWWuWvlf9iHqNlKSQO'

$是分隔符
$2b$,加密算法
12,表示2^12 key expansion rounds
这是盐b'jwBD7mg9stvIPydF2bqoPO',22个字符,Base64
这是密文b'odPwWYVvdmZb5uWWuWvlf9iHqNlKSQO',31个字符,Base64

1.8.4、注册代码 V2

全局变量:项目的 settings.py 文件实际上就是全局变量的配置文件。SECRET_KEY 是一个强密码。

from django.conf import settings
import os

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'blog.settings')

print(settings.SECRET_KEY)

使用 jwt 和 bcrypt,修改注册代码:

# Create your views here.
# user/views.py中
import bcrypt
from django.http import HttpRequest, HttpResponseBadRequest, JsonResponse
import simplejson
from .models import User
from django.conf import settings
import jwt
import datetime

def gen_token(user_id):
    # 生成token
    return jwt.encode(
        {
            'user_id': user_id,
            'timestamp': int(datetime.datetime.now().timestamp())  # 要取整
        }, settings.SECRET_KEY, 'HS256')  # 返回的是字符串

# 注册函数
def reg(request: HttpRequest):
    print(request.POST)
    print(request.body)
    payload = simplejson.loads(request.body)
    try:
        # 有任何异常,都返回400,如果保存数据出错,则向外抛出异常
        email = payload['email']
        query = User.objects.filter(email=email)
        print(query)
        print(type(query), query, query)  # 查看SQL语句
        if query:
            return HttpResponseBadRequest("Come on, you have registered")  # 这里返回实例,这不是异常类
        name = payload['name']
        password = bcrypt.hashpw(payload['password'].encode(), bcrypt.gensalt())
        print(email, name, password)

        user = User()
        user.email = email
        user.name = name
        user.password = password.decode()

        try:
            user.save()
            return JsonResponse({'token': gen_token(user.id)})  # 如果正常,返回json数据
        except:
            raise
    except Exception as e:  # 有任何异常,都返回
        print(e)
        return HttpResponseBadRequest()  # 这里返回实例,这不是异常类

1.9、用户登录接口设计

接收用户通过 POST 方法提交的登录信息,提交的数据是 JSON 格式数据。

{
    "password":"brinnatt",
    "email":"brinnatt@gmail.com"
}

从 user 表中 email 找出匹配的一条记录,验证密码是否正确。

验证通过说明是合法用户登录,显示欢迎页面。验证失败返回错误状态码,例如 4xx。

整个过程都采用 AJAX 异步过程,用户提交 JSON 数据,服务端获取数据后处理,返回 JSON。

URL:/user/login

METHOD:POST

1.9.1、路由配置

from .views import reg
from .views import login

urlpatterns = [
    re_path(r'^reg$', reg),
    re_path(r'^login$', login),
]

1.9.2、登录代码

def login(request: HttpRequest):
    payload = simplejson.loads(request.body)  # 获取登录信息数据
    print(payload)
    try:
        email = payload['email']
        user = User.objects.filter(email=email).get()

        if bcrypt.checkpw(payload['password'].encode(), user.password.encode()):
            # 验证通过
            token = gen_token(user.id)
            print(token)
            res = JsonResponse({
                'user': {
                    'user_id': user.id,
                    'name': user.name,
                    'email': user.email
                }, 'token': token
            })
            res.set_cookie('Jwt', token)  # 演示如何set cookie
            return res
        else:
            return HttpResponseBadRequest("This is no such user.")
    except Exception as e:
        print(e)
        return HttpResponseBadRequest("Something Wrong")  # 这里返回实例,这不是异常类

1.9.3、认证接口

如何获取浏览器提交的 token 信息?

  1. 使用 Header 中的 Authorization

    通过这个 header 增加 token 信息。

    通过 header 发送数据,所有方法可以是 Post、Get。

  2. 自定义header

    JWT 来发送 token。

    我们选择第二种方式。

认证

基本上所有的业务都需要认证用户的信息。在认证代码中比较时间戳,如果过期,就直接抛出 401 未认证,客户端收到后就直接跳转到登录页。

如果没有提交 user id,就直接重新登录。如果用户查到了,填充 user 对象。

request -> 时间戳比较 -> user id 比较 -> 向后执行。

1.9.4、Django 认证

django.contrib.auth 中提供了许多方法,这里主要介绍其中的三个:

  1. authenticate(**credentials)

    提供了用户认证,即验证用户名以及密码是否正确。

    user = authentica(username='someone',password='somepassword')

  2. login(HttpRequest, user, backend=None)

    该函数接受一个 HttpRequest 对象,以及一个认证了的 User 对象。

    此函数使用 django 的 session 框架给某个已认证的用户附加上 session id 等信息。

  3. logout(request)

注销用户

该函数接受一个 HttpRequest 对象,无返回值。当调用该函数时,当前请求的 session 信息会全部清除。该用户即使没有登录,使用该函数也不会报错。

还提供了一个装饰器来判断是否登录 django.contrib.auth.decorators.login_required

本项目使用了无 session 机制,且用户信息自己建表管理,所以,认证需要自己实现。

1.9.5、中间件技术 Middleware

官方定义,在 Django 的 request 和 response 处理过程中,由框架提供的 hook 钩子在全局作用域对 Django 的输入和输出进行修改。

https://docs.djangoproject.com/en/4.1/topics/http/middleware/#writing-your-own-middleware

class SimpleMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
        # One-time configuration and initialization.

    def __call__(self, request):
        # Code to be executed for each request before
        # the view (and later middleware) are called.

        response = self.get_response(request)

        # Code to be executed for each request/response after
        # the view is called.

        return response

但是,这样所有的请求和响应都会拦截,我们还得判断访问的是不是想要拦截的 view 函数,所以,考虑其他方法。

1.9.6、装饰器

在需要认证的 view 函数上增强认证功能,写一个装饰器函数。谁需要认证,就在这个 view 函数上应用这个装饰器。

AUTH_EXPIRE = 8 * 60 * 60  # 8小时过期

def authentication(view):
    def wrapper(request: HttpRequest):
        # 自定义header jwt
        payload = request.META.get("HTTP_JWT")  # 会被加前缀HTTP_且全大写
        print(payload)
        if not payload:  # None没有拿到,认证失败
            return HttpResponse(status=401)
        try:  # 解码
            payload = jwt.decode(payload, settings.SECRET_KEY, algorithms=['HS256'])
            print(payload)
        except:
            return HttpResponse(status=401)

        # 验证过期时间
        current = datetime.datetime.now().timestamp()
        if (current - payload.get('timestamp', 0)) > AUTH_EXPIRE:
            return HttpResponse(status=401)
        print('-' * 60)

        try:
            user_id = payload.get('user_id', -1)
            user = User.objects.filter(pk=user_id).get()
            request.user = user  # 如果正确,测注入user
            print('-' * 60)
        except Exception as e:
            print(e)
            return HttpResponse(status=401)

        # 调用视图函数
        # 特别注意view调用的时候,里面也有返回异常
        return view(request)

    return wrapper

@authentication
def test(request: HttpRequest):
    return HttpResponse('test')

1.9.7、JWT 过期问题

pyjwt 支持过期设定,在 decode 的时候,如果过期,则抛出异常。需要在 payload 中增加 claim exp。exp 要求是一个整数 int 的时间戳。

import jwt
import datetime
import threading

event = threading.Event()
key = 'brinnatt'

# 在jwt的payload中增加exp claim
data = jwt.encode({'name': 'tom', 'age': 20, 'exp': int(datetime.datetime.now().timestamp()) + 10},
                  key)
print(jwt.get_unverified_header(data))

try:
    while not event.wait(1):
        print(jwt.decode(data, key, algorithms=['HS256']))  # 过期,校验就会抛出异常
        print(datetime.datetime.now().timestamp())
except jwt.ExpiredSignatureError as e:
    print(e)

重写 Jwt 过期:

AUTH_EXPIRE = 60 # 60秒测试

def gen_token(user_id):
    # 生成token
    return jwt.encode(
        {
            'user_id': user_id,
            'exp': int(datetime.datetime.now().timestamp()) + AUTH_EXPIRE  # 要取整
        }, settings.SECRET_KEY, 'HS256')  # 返回的是字符串

def authentication(view):
    def wrapper(request: HttpRequest):
        # 自定义header jwt
        payload = request.META.get("HTTP_JWT")  # 会被加前缀HTTP_且全大写
        print(payload)
        if not payload:  # None没有拿到,认证失败
            return HttpResponse(status=401)
        try:  # 解码,同时验证过期时间
            payload = jwt.decode(payload, settings.SECRET_KEY, algorithms=['HS256'])
            print(payload)
        except:
            return HttpResponse(status=401)

        try:
            user_id = payload.get('user_id', -1)
            user = User.objects.filter(pk=user_id).get()
            request.user = user  # 如果正确,测注入user
            print('-' * 60)
        except Exception as e:
            print(e)
            return HttpResponse(status=401)

        # 调用视图函数
        # 特别注意view调用的时候,里面也有返回异常
        return view(request)
    return wrapper

@authentication
def test(request: HttpRequest):
    return HttpResponse('test')

1.10、博文相关接口

1.10.1、功能分析

功能 函数名 Request 方法 路径
发布(增) pub post /pub
看文章(查) get get /(\d+)
列表(分页) fetchall get /

1.10.2、创建博文应用

$ python manage.py startapp post

注意:一定要把应用 post 加入到 settings.py 中,否则不能迁移。

1.10.3、模型

from django.db import models
from user.models import User

# Create your models here.
class Post(models.Model):
    class Meta:
        db_table = 'post'

    id = models.AutoField(primary_key=True)
    title = models.CharField(max_length=256, null=False)
    postdate = models.DateTimeField(null=True)

    # 从post查作者,从post查内容
    author = models.ForeignKey(User, on_delete=models.CASCADE)  # 指定外键,migrate会生成author_id字段

    # self.content可以访问Content实例,其内容是self.content.content

    def __repr__(self):
        return '<Post {} {} {} {} >'.format(
            self.id, self.title, self.author_id, self.content)

    __str__ = __repr__

class Content(models.Model):
    class Meta:
        db_table = 'content'

    # 没有主键,会自动创建一个自增主键
    post = models.OneToOneField(Post, on_delete=models.CASCADE)  # 一对一,这边会有一个外键引用post.id
    content = models.TextField(null=False)

    def __repr__(self):
        return '<Content {} {}>'.format(self.post.id, self.content[:20])

    __str__ = __repr__
(venv) PS D:\JetBrains\BrinnattProjects\blog> python.exe .\manage.py makemigrations 
(venv) PS D:\JetBrains\BrinnattProjects\blog> python.exe .\manage.py migrate 

1.10.4、路由

全局 settings.py

urlpatterns = [
    path('admin/', admin.site.urls),
    re_path(r'^$', index),
    re_path(r'^index/$', index),
    re_path(r'^user/', include('user.urls')),
    re_path(r'^post/', include('post.urls')),
]

post 应用 urls.py

# 新建user/urls.py
from django.urls import re_path
from .views import pub, get, fetchall

urlpatterns = [
    re_path(r'^pub$',pub),
    re_path(r'^(\d+)$', get),  # 给get传入一个参数str类型
    re_path(r'^$', fetchall),
]

1.10.5、pub 接口实现

用户从浏览器端提交 Json 数据,包含 title,content。提交博文需要认证用户,从请求的 header 中验证 jwt。

request:POST-> @authenticate pub -> return post_id

from django.http import HttpRequest, HttpResponse, JsonResponse, HttpResponseBadRequest, HttpResponseNotFound
from user.views import authentication
from user.models import User
import simplejson
import datetime
from .models import Post, Content

@authentication
def pub(request: HttpRequest):
    post = Post()
    content = Content()
    try:
        payload = simplejson.loads(request.body)
        post.title = payload['title']
        post.author = User(id=request.user.id)  # 注入的
        post.postdate = datetime.datetime.now(
            datetime.timezone(datetime.timedelta(hours=8))
        )
        post.save()

        content.content = payload['content']
        content.post = post
        content.save()

        return JsonResponse({'post_id': post.id})
    except Exception as e:
        print(e)
        return HttpResponseBadRequest()

1.10.6、get 接口实现

根据 post_id 查询博文并返回。这里需要认证吗?

如果博文只能作者看,就需要认证。我们这里公开给所有人看,所以不需要认证,同样,下面的 list 接口也是不需要认证的。

request: GET-> get Post by id -> return post+content

def get(request: HttpRequest, id):  # 分组捕获传入
    try:
        id = int(id)
        post = Post.objects.get(pk=id)
        print(post, '~~~~~~~~~~~~~~')
        if post:
            return JsonResponse({
                'post': {
                    'post_id': post.id,
                    'title':post.title,
                    'author': post.author.name,
                    'author_id': post.author_id, # post.author.id
                    'postdate': post.postdate.timestamp(),
                    'content': post.content.content
                }
            })
        # get方法保证必须只有一条记录,否则抛异常
    except Exception as e:
        print(e)
        return HttpResponseNotFound()

1.10.7、getall 接口实现

发起 get 请求,通过查询字符串 http://url/post/?page=2 查询第二页数据。

request: GET-> fetch all (page=1) -> return post list

def fetchall(request: HttpRequest):
    try: # 页码(太多要分页)
        page = int(request.GET.get('page', 1))
        page = page if page > 0 else 1
    except:
        page = 1

    try: # 页码行数
        # 注意,这个数据不要轻易让浏览器端改变,如果允许改变,一定要控制范围
        size = int(request.GET.get('size', 20))
        size = size if 0 < size < 101 else 20
    except:
        size = 20

    try:
        # 按照id倒排
        start = (page - 1) * size
        posts = Post.objects.order_by('-id')[start:start+size]
        print(posts.query)
        return JsonResponse({
            'posts':[
                {
                    'post_id': post.id,
                    'title': post.title,
                } for post in posts
            ]
        })
    except Exception as e:
        print(e)
        return HttpResponseBadRequest()

1.10.8、完善分页

分页信息,一般有:当前页/总页数、行限制数、记录总数。

当前页:page。

行限制数:size,每页最多多少行。

总页数:pages = math.ceil(count/size)

记录总数:count,从 select * from table 来。

def fetchall(request: HttpRequest):
    try:  # 页码(太多要分页)
        page = int(request.GET.get('page', 1))
        page = page if page > 0 else 1
    except:
        page = 1

    try:  # 页码行数
        # 注意,这个数据不要轻易让浏览器端改变,如果允许改变,一定要控制范围
        size = int(request.GET.get('size', 20))
        size = size if 0 < size < 101 else 20
    except:
        size = 20

    try:
        # 按照id倒排
        start = (page - 1) * size
        posts = Post.objects.order_by('-id')
        print(posts.query)
        count = posts.count()
        posts = posts[start:start + size]
        print(posts.query)

        return JsonResponse({
            'posts': [
                {
                    'post_id': post.id,
                    'title': post.title,
                } for post in posts
            ],
            'pagination': {
                'page': page,
                'size': size,
                'count': count,
                'pages': math.ceil(count / size)
            }
        })
    except Exception as e:
        print(e)
        return HttpResponseBadRequest()

1.10.9、优化

def validate(d: dict, name: str, type_func, default, validate_func):
    try:
        result = type_func(d.get(name, default))
        result = validate_func(result, default)
    except:
        result = default
    return result

def fetchall(request: HttpRequest):
    # try:  # 页码(太多要分页)
    #     page = int(request.GET.get('page', 1))
    #     page = page if page > 0 else 1
    # except:
    #     page = 1
    #
    # try:  # 页码行数
    #     # 注意,这个数据不要轻易让浏览器端改变,如果允许改变,一定要控制范围
    #     size = int(request.GET.get('size', 20))
    #     size = size if 0 < size < 101 else 20
    # except:
    #     size = 20

    page = validate(request.GET, 'page', int, 1, lambda x, y: x if x > 0 else y)
    size = validate(request.GET, 'size', int, 20, lambda x, y: x if 0 < x < 101 else y)

    try:
        # 按照id倒排
        start = (page - 1) * size
        posts = Post.objects.order_by('-id')
        print(posts.query)
        count = posts.count()
        posts = posts[start:start + size]
        print(posts.query)

        return JsonResponse({
            'posts': [
                {
                    'post_id': post.id,
                    'title': post.title,
                } for post in posts
            ],
            'pagination': {
                'page': page,
                'size': size,
                'count': count,
                'pages': math.ceil(count / size)
            }
        })
    except Exception as e:
        print(e)
        return HttpResponseBadRequest()
标签云