Flask Web | Flask 的扩展

举例说明 Flask 生态中常见的几种扩展

Posted by Haauleon on December 20, 2022

本篇所有操作均在基于 Ubuntu 16.04 LTS 的虚拟机下完成,且使用 Vagrant 来操作虚拟机系统,虚拟机系统 VirtualBox Version: 7.0



一、Flask 的扩展

环境准备:
Python 2.7.11+
pip==9.0.3
flask==0.11.1
httpie==0.9.4
werkzeug==0.11.10

  Flask 扩展的生态非常繁荣,这里介绍其中最常用的 8 中扩展。如下:

  • Flask-Script
  • Flask-DebugToolbar
  • Flask-Migrate
  • Flask-WTF
  • Flask-Security
  • Flask-RESTful
  • Flask-Admin
  • Flask-Assets



二、Flask-Script

  Django 提供了如下管理命令:

1
2
> python manage.py startapp
> python manage.py runserver

  Flask 也可以通过 Flask-Script 添加运行服务器、设置数据库、定制 shell 等功能的命令。但如果已经使用了 Flask 0.11 版本,可以考虑使用 Flask 自带的命令行接口替代它。


1、安装 Flask-Script

  使用以下命令行安装 Flask-Script:

1
> pip2 install flask-script



2、使用 Flask-Script

  安装完成后在需要管理的项目根目录下创建一个 manage.py 文件并写入以下内容后保存,即可使用 manage.py 来管理项目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# coding=utf-8
"""
在很多地方可能都会看到 from flask.ext.package import X 的用法,但这种用法已经收到了官方反对。
要采用 from flask_script import X 的方式。
"""
from flask_script import Manager, Server, Shell, prompt_bool

from app import app, db, PasteFile

manager = Manager(app)


def make_shell_context():
    return {
        'db': db,
        'PasteFile': PasteFile,
        'app': app
    }


@manager.command
def dropdb():
    if prompt_bool(
            'Are you sure you want to lose all your data'):
        db.drop_all()


@manager.option('-h', '--filehash', dest='filehash')
def get_file(filehash):
    paste_file = PasteFile.query.filter_by(filehash=filehash).first()
    if not paste_file:
        print 'Not exists'
    else:
        print 'URL is {}'.format(paste_file.get_url('i'))


manager.add_command('shell', Shell(make_context=make_shell_context))
manager.add_command('runserver', Server(
    use_debugger=True, use_reloader=True,
    host='0.0.0.0', port=9000)
)


if __name__ == '__main__':
    manager.run()


现在可以使用 manage.py 来管理项目了,输入以下命令行来管理:

  1. 在终端通过 filehash 获取文件路径
    1
    2
    3
    4
    5
    
     (venv) ❯ python2 manage.py get_file -h 8583d66b6b2e467e8c6b586ca428db74.jpg
     URL is http://localhost/i/8583d66b6b2e467e8c6b586ca428db74.jpg
    
     (venv) ❯ python2 manage.py get_file -h 8583d66b6b2e467e8c6b586ca4hgnnfk.jpg
     Not exists
    
  2. 在测试环境中可以用来清理数据
    1
    2
    
     (venv) ❯ python2 manage.py dropdb
     Are you sure you want to lose all your data [n]: n
    
  3. 自带了三个内置变量的 shell
    1
    2
    3
    4
    5
    6
    7
    
     (venv) ❯ python2 manage.py shell
    
     In [1]: db
     Out[1]: <SQLAlchemy engine='mysql://web:web@localhost:3306/r'>
    
     In [2]: PasteFile
     Out[2]: models.PasteFile
    
  4. 通过 manage.py 启动服务
    1
    2
    3
    4
    5
    6
    7
    8
    
     (venv) ❯ python2 manage.py runserver
     10.0.2.2 - - [21/Dec/2022 06:16:30] "GET / HTTP/1.1" 200 -
     10.0.2.2 - - [21/Dec/2022 06:16:38] "GET / HTTP/1.1" 200 -
     10.0.2.2 - - [21/Dec/2022 06:16:39] "GET / HTTP/1.1" 200 -
     10.0.2.2 - - [21/Dec/2022 06:16:40] "GET / HTTP/1.1" 200 -
     10.0.2.2 - - [21/Dec/2022 06:17:21] "POST / HTTP/1.1" 200 -
     10.0.2.2 - - [21/Dec/2022 06:17:21] "GET /i/dae3aa929d0b4fb4a00d5758cecce502.jpg HTTP/1.1" 200 -
     10.0.2.2 - - [21/Dec/2022 06:20:19] "GET /i/dae3aa929d0b4fb4a00d5758cecce502.jpg HTTP/1.1" 304 -
    



三、Flask-DebugToolbar

  Django 有非常知名的 Django-DebugToolbar,而 Flask 也有对应的替代工具 Flask-DebugToolbar。它会在浏览器上添加右边栏,可以快速查看环境变量、上下文内容,方便调试。


1、安装 Flask-DebugToolbar

  使用以下命令行安装 Flask-DebugToolbar:

1
> pip2 install flask-debugtoolbar



2、使用 Flask-DebugToolbar

  使用它也很简单,但是注意 app.debug 要为 True 才可以看到调试边栏。如下使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# coding=utf-8
from flask import Flask
from flask_debugtoolbar import DebugToolbarExtension

app = Flask(__name__)

app.debug = True

app.config['SECRET_KEY'] = 'a secret key'

toolbar = DebugToolbarExtension(app)


@app.route('/')
def hello():
    return '<body></body>'


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=9000, debug=app.debug)

浏览器右边栏显示如下图:



四、Flask-Migrate

  使用关系型数据库时,修改数据库模型和更新数据库这样的工作时有发生,而且很重要。怎么做到既安全又方便呢?

  SQLAlchemy 作者为此开发了迁移框架 Alembic,Flask-Migrate 就是基于 Alembic 做了轻量级封装,并集成到 Flask-Script 中。所有操作都通过 Flask-Script 命令完成。它能跟踪数据库结构的变化,把变化的部分应用到数据库中。


1、安装 Flask-Migrate

  使用以下命令安装 Flask-Migrate:

1
> pip2 install Flask-Migrate==1.8.1  



2、使用 Flask-Migrate

(1)先定义一个 User 类(users.py)

1
2
3
4
5
6
7
8
9
10
# coding=utf-8
from ext import db


class User(db.Model):
    __tablename__ = 'login_users'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(128), nullable=False)
    login_count = db.Column(db.Integer, default=0)
    last_login_ip = db.Column(db.String(128), default='unknown')


(2)现在在项目根目录下添加迁移支持文件 app_migrate.py 并写入以下内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# coding=utf-8
from flask import Flask
from flask_script import Manager
from flask_migrate import Migrate, MigrateCommand

from ext import db

app = Flask(__name__)
app.config.from_object('config')

db.init_app(app)

import users  # noqa

migrate = Migrate(app, db)
manager = Manager(app)
manager.add_command('db', MigrateCommand)


if __name__ == '__main__':
    manager.run()


我们现在想要在 login_users 表中扩充两个字段 email 和 password,流程如下:

  1. 初始化迁移工作
      使用以下命令进行初始化迁移,迁移完成后会在当前目录下增加一个 migrate 目录,这个目录应该放进版本库。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    
     (venv) ❯ python2 app_migrate.py db init
     Creating directory
     /home/ubuntu/web_develop/migrations ... done
     Creating directory
     /home/ubuntu/web_develop/migrations/versions ... done
     Generating /home/ubuntu/web_develop/migrations/script.py.mako ... done
     Generating
     /home/ubuntu/web_develop/migrations/env.pyc ... done
     Generating
     /home/ubuntu/web_develop/migrations/alembic.ini
     ... done
     Generating
     /home/ubuntu/web_develop/migrations/env.py ... done
     Generating
     /home/ubuntu/web_develop/migrations/README ... done
     Please edit configuration/connection/logging settings in '/home/ubuntu/web_develop/migrations/alembic.ini' before proceeding.
    
     (venv) ❯ ls -al migrations
     total 32
     drwxrwxr-x 3 ubuntu ubuntu 4096 Dec 21 09:57 .
     drwxrwxr-x 6 ubuntu ubuntu 4096 Dec 21 09:57 ..
     -rw-rw-r-- 1 ubuntu ubuntu  770 Dec 21 09:57 alembic.ini
     -rwxrwxr-x 1 ubuntu ubuntu 2883 Dec 21 09:57 env.py
     -rw-rw-r-- 1 ubuntu ubuntu 2734 Dec 21 09:57 env.pyc
     -rwxrwxr-x 1 ubuntu ubuntu   38 Dec 21 09:57 README
     -rwxrwxr-x 1 ubuntu ubuntu  412 Dec 21 09:57 script.py.mako
     drwxrwxr-x 2 ubuntu ubuntu 4096 Dec 21 09:57 versions
    
  2. 修改 login_users 表的模型结构
      给 User 类添加 email 和 password 这两个字段,字段描述如下:
    1
    2
    
     email = db.Column(db.String(128), nullable=False)
     password = db.Column(db.String(256), nullable=False)
    
  3. 创建迁移的脚本
      执行以下命令将会在 migration/versions/ 目录下添加一个执行的脚本,文件名就是版本号。版本的对应关系在当前库的 alembic_version 表中。
      需要注意的是,假如你的数据库里还有其他的表没有放在迁移脚本中,那么在执行迁移操作时就会从数据库中删掉,所以 app_migrate.py 这样的管理脚本应该覆盖所有重要的表,而所有模型文件中都要使用 from ext import db,就可以保证这一点。
      创建迁移的脚本这一步其实并没有操作数据库,所以一定要注意终端输出,确定和自己的预想一样。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
     (venv) ❯ python2 app_migrate.py db migrate
     INFO  [alembic.runtime.migration] Context impl MySQLImpl.
     INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
     INFO  [alembic.autogenerate.compare] Detected removed table u'changes'
     INFO  [alembic.autogenerate.compare] Detected removed table u'change_properties'
     INFO  [alembic.autogenerate.compare] Detected removed table u'buildset_sourcestamps'
     ...
     INFO  [alembic.autogenerate.compare] Detected removed table u'scheduler_changes'
     Generating /home/ubuntu/web_develop/migrations/versions/b75fbc434302_.py ... done
    

      以上创建的迁移脚本 migrations/versions/b75fbc434302_.py 内容部分如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    
     """empty message
    
     Revision ID: b75fbc434302
     Revises: None
     Create Date: 2022-12-21 10:13:12.074444
    
     """
    
     # revision identifiers, used by Alembic.
     revision = 'b75fbc434302'
     down_revision = None
    
     from alembic import op
     import sqlalchemy as sa
     from sqlalchemy.dialects import mysql
    
     def upgrade():
         ### commands auto generated by Alembic - please adjust! ###
         op.drop_table('changes')
         op.drop_table('change_properties')
         op.drop_table('buildset_sourcestamps')
         ...
         ...
         ### end Alembic commands ###
    
    
     def downgrade():
         ### commands auto generated by Alembic - please adjust! ###
         op.create_table('login_users',
         sa.Column('id', mysql.INTEGER(display_width=11), nullable=False),
         sa.Column('name', mysql.VARCHAR(length=128), nullable=False),
         sa.Column('email', mysql.VARCHAR(length=128), nullable=False),
         sa.Column('password', mysql.VARCHAR(length=256), nullable=False),
         sa.Column('login_count', mysql.INTEGER(display_width=11), autoincrement=False, nullable=True),
         sa.Column('last_login_ip', mysql.VARCHAR(length=128), nullable=True),
         sa.PrimaryKeyConstraint('id'),
         mysql_default_charset=u'latin1',
         mysql_engine=u'InnoDB'
         )
         op.create_table('users',
         sa.Column('id', mysql.INTEGER(display_width=11), nullable=False),
         sa.Column('name', mysql.VARCHAR(length=50), nullable=True),
         sa.PrimaryKeyConstraint('id'),
         mysql_default_charset=u'latin1',
         mysql_engine=u'InnoDB'
         )
         op.create_table('migrate_version',
         sa.Column('repository_id', mysql.VARCHAR(length=250), nullable=False),
         sa.Column('repository_path', mysql.TEXT(), nullable=True),
         sa.Column('version', mysql.INTEGER(display_width=11), autoincrement=False, nullable=True),
         sa.PrimaryKeyConstraint('repository_id'),
         mysql_default_charset=u'latin1',
         mysql_engine=u'MyISAM'
         )
         ...
         ...
         ### end Alembic commands ###
    
  4. 更新数据库
      使用以下命令更新数据库,这一步才是实际操作数据库。
    1
    2
    3
    4
    
     (venv) ❯ python2 app_migrate.py db upgrade
     INFO  [alembic.runtime.migration] Context impl MySQLImpl.
     INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
     INFO  [alembic.runtime.migration] Running upgrade  -> b75fbc434302, empty message
    

      更新后的表结构如下,可以看到这两个字段已经出现了,方便且安全。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
     mysql> desc login_users;
     +---------------+--------------+------+-----+---------+----------------+
     | Field         | Type         | Null | Key | Default | Extra          |
     +---------------+--------------+------+-----+---------+----------------+
     | id            | int(11)      | NO   | PRI | NULL    | auto_increment |
     | name          | varchar(128) | NO   |     | NULL    |                |
     | email         | varchar(128) | NO   |     | NULL    |                |
     | password      | varchar(256) | NO   |     | NULL    |                |
     | login_count   | int(11)      | YES  |     | NULL    |                |
     | last_login_ip | varchar(128) | YES  |     | NULL    |                |
     +---------------+--------------+------+-----+---------+----------------+
     6 rows in set (0.00 sec)
    
  5. 取消更新数据库
      如果发现有问题也可以很轻松地取消更新,将当前版本降级回退到上一个版本,使用以下命令即可。
    1
    2
    3
    4
    
     (venv) ❯ python2 app_migrate.py db downgrade
     INFO  [alembic.runtime.migration] Context impl MySQLImpl.
     INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
     INFO  [alembic.runtime.migration] Running downgrade b75fbc434302 -> , empty message
    

      取消更新后表结构也回退到上一个版本。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
     mysql> desc login_users;
     +---------------+--------------+------+-----+---------+----------------+
     | Field         | Type         | Null | Key | Default | Extra          |
     +---------------+--------------+------+-----+---------+----------------+
     | id            | int(11)      | NO   | PRI | NULL    | auto_increment |
     | name          | varchar(128) | NO   |     | NULL    |                |
     | login_count   | int(11)      | YES  |     | NULL    |                |
     | last_login_ip | varchar(128) | YES  |     | NULL    |                |
     +---------------+--------------+------+-----+---------+----------------+
     4 rows in set (0.00 sec)
    


  上述表结构中的 last_login_ip 在存储 IP 时使用的是字符串类型 varchar,这只是为了展示起来更直观,更好的方法是把 IP 转换为整数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
In [1]: import struct

In [2]: import socket

In [3]: def ip2int(addr):
   ...:     return struct.unpack("!I", socket.inet_aton(addr))[0]
   ...:

In [4]: def int2ip(addr):
   ...:     return socket.inet_ntoa(struct.pack("!I", addr))
   ...:

In [5]: ip2int('10.0.2.2')
Out[5]: 167772674

In [6]: int2ip(167772674)
Out[6]: '10.0.2.2'



五、Flask-WTF

  Flask-WTF 是一个集成 WTForms 的表单验证和渲染的扩展。


1、安装 Flask-WTF

  使用以下命令安装 Flask-WTF:

1
> pip2 install Flask-WTF==0.12  



2、使用 Flask-WTF

  以下通过实现一个注册功能的应用来了解 Flask-WTF 的使用。

(1)先定义一个 app_wtf.py 文件并写入以下注册表单的内容

1
2
3
4
5
6
7
8
9
10
11
12
from flask_wtf import Form
from wtforms import TextField, PasswordField
from wtforms.validators import length, Required, EqualTo


class RegistrationForm(Form):
    name = TextField('Username', [length(min=4, max=25)])
    email = TextField('Email Address', [length(min=6, max=35)])
    password = PasswordField('New Password', [
        Required(), EqualTo('confirm', message='Passwords must match')
    ])
    confirm = PasswordField('Repeat Password')


(2)应用代码内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# coding=utf-8
from flask import Flask, render_template, request
from flask_wtf.csrf import CsrfProtect

from ext import db
from users import User

app = Flask(__name__, template_folder='../../templates')
app.config.from_object('config')
CsrfProtect(app)
db.init_app(app)


@app.route('/register', methods=['GET', 'POST'])
def register():
    form = RegistrationForm(request.form)
    if request.method == 'POST' and form.validate():
        user = User(name=form.name.data, email=form.email.data,
                    password=form.password.data)
        db.session.add(user)
        db.session.commit()
        return 'register successed!'
    return render_template('register.html', form=form)


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=9000, debug=app.debug)


(3)注册模板文件 register.html 的内容如下


  这样就完成了一个带有 CSRF 保护的注册表单的功能了,请求结果如下:



六、Flask-Security

  Flask-Security 非常强大,它提供角色管理、权限管理、用户登录、邮箱验证、密码重置、密码加密、跟踪用户登录状态等功能。


1、安装 Flask-Security

  使用以下命令安装 Flask-Security:

1
> pip2 install flask-security==1.7.5



2、使用模板类型和用途

  Flask-Security 提供了 7 种基本模板。如果想要定制模板,可以在应用的模板目录下创建名为 security 的目录,添加对应名字的模板,然后指定对应的变量即 SECURITY_LOGIN_USER_TEMPLATE 替换模板。这 7 种模板类型和用途如下表所示:

模板 功能
security/forgot_password.html 忘记密码
security/login_user.html 用户登录
security/register_user.html 用户注册
security/reset_password.html 重置密码
security/change_password.html 更新密码
security/send_confirmation.html 发送确认信息
security/send_login.html 无密码方式



3、上下文处理器

  Flask-Security 还提供了 8 种上下文处理的装饰器,类似于钩子。这 8 种处理器类型如下表所示:

处理器 功能
context_processor 所有视图都会触发
forgot_password_context_processor 忘记密码时视图就会触发
login_context_processor 登录时视图会触发
register_context_processor 注册时视图会触发
reset_password_context_processor 重置密码时视图会触发
change_password_context_processor 更新密码时视图会触发
send_confirmation_context_processor 发送确认信息时视图会触发
send_login_context_processor 无密码方式登录时视图会触发



4、表单

  Flask-Security 也提供了 8 种表单,表单作用如下表所示:

表单 用途
login_form 登录
confirm_register_form 注册确认
register_form 注册
forgot_password_form 忘记密码
reset_password_form 重置密码
change_password_form 更新密码
send_confirmation_form 发送确认信息
passwordless_login_form 无密码登录



5、用户角色应用实现

  先了解一个新的概念:“角色”。角色定义了用户的类型。如果一个站点功能很少哦,只需要普通用户和管理员两种权限类型就可以了。但随着业务的扩展,用户具有的特殊的权限类型越来越多,对于权限管理而言,不能为每个人都授予管理员这样的角色,这个时候就需要实现多种类型的角色权限,不同的角色甚至可以具备多种角色权限。

  我们 使用位运算做权限控制。位运算在 Linux 文件系统上就有体现,一个用户对文件或目录所拥有的权限分为三种:可读(4)可写(2)可执行(1),它们之间可以任意组合:有可读和可写权限就用 6 来表示(4+2=6);有可读和可执行权限就用 5 来表示(4+1=5);三种权限全部拥有就用 7 来表示(4+2+1=7)。为什么选择 1(2^0)、2(2^1)、4(2^2) 这样的有规律的数据呢?先看看下面的例子:

1
2
3
4
5
6
7
8
In [1]: int('00000001', 2)
Out[1]: 1

In [2]: int('00000010', 2)
Out[2]: 2

In [3]: int('00000100', 2)
Out[3]: 4

  通过 标志位 判断是否有权限,如果有权限,对应位就置位 1,如果三种权限都有,就是:

1
2
In [4]: int('00000111', 2)
Out[4]: 7

  它其实就是对三个二进制位做按位或计算得到的:

1
2
In [5]: int('00000001', 2) | int('00000010', 2) | int('00000100', 2)
Out[5]: 7

  判断权限的时候就把这个值(假设叫作 A)和要判断的权限(叫作 B)做按位与计算,这样就把它们中都为 1 的标志位置为 1,如果结果还等于 B,就说明它有这样的权限,否则说明对应的标志位没有置位 1 即没有权限:

1
2
3
4
5
In [9]: int('00000111', 2) & int('0000001', 2) == int('00000001', 2)
Out[9]: True

In [10]: int('00000110', 2) & int('0000001', 2) == int('00000001', 2)
Out[10]: False




  Flask-Security 支持 SQLAlchemy、MongoEnine 和 Peewee 定义模型。现在基于 Flask-SQLAlchemy 和之前的 文件托管服务 来实现一个简单的应用 app_security.py。

(1)权限类

1
2
3
4
5
6
7
8
9
10
11
12
# 权限定义
class Permission(object):
    LOGIN = 0x01
    EDITOR = 0x02
    OPERATOR = 0x04
    ADMINISTER = 0xff  # 使用 0xff 最大值表示 ADMINISTER 拥有全部权限
    PERMISSION_MAP = {
        LOGIN: ('login', 'Login user'),
        EDITOR: ('editor', 'Editor'),
        OPERATOR: ('op', 'Operator'),
        ADMINISTER: ('admin', 'Super administrator')
    }


(2)使用 flask_security.RoleMixin 定义角色模型 Role

1
2
3
4
5
6
7
8
from flask_security import RoleMixin


class Role(db.Model, RoleMixin):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(255), unique=True)
    permissions = db.Column(db.Integer, default=Permission.LOGIN)
    description = db.Column(db.String(255))


(3)使用 flask_security.UserMixin 定义用户模型 User,给 User 这个模型添加 roles 字段,指向模型 Role,并且添加判断权限的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from functools import reduce
from operator import or_
from flask_security import UserMixin


class User(db.Model, UserMixin):
    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(255), unique=True)
    password = db.Column(db.String(255))
    active = db.Column(db.Boolean())
    confirmed_at = db.Column(db.DateTime())
    roles = db.relationship('Role', secondary=roles_users,
                            backref=db.backref('users', lazy='dynamic'))

    def can(self, permissions):
        if self.roles is None:
            return False
        all_perms = reduce(or_, map(lambda x: x.permissions, self.roles))
        return all_perms & permissions == permissions

    def can_admin(self):
        return self.can(Permission.ADMINISTER)

  db.relationship 还使用了 db.backref,表示反向引用。Role 对象 r 通过 users 就能反向获取有对应权限的用户列表了。如下例子所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
In [1]: r = Role()
In [2]: r.name = 'role_1'
In [3]: r.permissions = Permission.LOGIN
In [4]: r.description = 'test role'

In [5]: u = User()
In [6]: u.email = '1@qq.com'
In [7]: u.password = '123'
In [8]: u.confirmed_at = datetime.now()
In [9]: u.roles = [r]

In [10]: db.session.add(r)
In [11]: db.session.add(u)
In [12]: r.users
Out[12]: <sqlalchemy.orm.dynamic.AppenderBaseQuery at 0x7f6548084dd0>

In [13]: db.create_all()
In [14]: r.users.all()
Out[14]: [<__main__.User at 0x7f65480c2e90>]


(4)用户和角色是多对多的关系,需要定义一个用于关系的辅助表。对于这个辅助表,强烈建议不使用模型,而是使用一个实际的表

1
2
3
4
5
roles_users = db.Table(
    'roles_users',
    db.Column('user_id', db.Integer,
              db.ForeignKey('user.id')),
    db.Column('role_id', db.Integer, db.ForeignKey('role.id')))


(5)添加 login_context_processor 钩子

1
2
3
4
5
6
7
8
user_datastore = SQLAlchemyUserDatastore(db, User, Role)
security = Security(app, user_datastore, register_form=LoginForm)


@security.login_context_processor
def security_login_processor():
    print 'Login'
    return {}


(6)通过添加 before_first_request 钩子实现初始化
  每次在第一次接收请求的时候就会删除相关表,再重新创建这些表,并创建两个用户,用户权限分别如下:

  • dongwm@dongwm.com:它具有 LOGIN 与 EDITOR 两种权限,但有些页面访问不了
  • admin@dongwm.com:管理员,拥有全部的权限
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@app.before_first_request
def create_user():
    db.drop_all()
    db.create_all()

    for permissions, (name, desc) in Permission.PERMISSION_MAP.items():
        user_datastore.find_or_create_role(
            name=name, description=desc, permissions=permissions)
    for email, passwd, permissions in (
            ('dongwm@dongwm.com', '123', (
                    Permission.LOGIN, Permission.EDITOR)),
            ('admin@dongwm.com', 'admin', (Permission.ADMINISTER,))):
        user_datastore.create_user(email=email, password=passwd)
        for permission in permissions:
            user_datastore.add_role_to_user(
                email, Permission.PERMISSION_MAP[permission][0])
    db.session.commit()


(7)添加验证访问权限的装饰器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def permission_required(permission):
    def decorator(f):
        @wraps(f)
        def _deco(*args, **kwargs):
            if not current_user.can(permission):
                """
                current_user 就是一个 User 对象,通过 User 类添加的 can 方法判断权限
                """
                abort(403)
            return f(*args, **kwargs)

        return _deco

    return decorator


def admin_required(f):
    return permission_required(Permission.ADMINISTER)(f)


(8)给视图添加权限控制的方法

1
2
3
4
5
6
7
8
9
10
11
12
@app.route('/')
@login_required
@permission_required(Permission.LOGIN)
def index():
    return 'Login in'


@app.route('/admin/')
@login_required
@admin_required
def admin():
    return 'Only administrators can see this!'


(9)自定义登录模板 login_user.html 内容如下


  该应用使用了自定义的登录模板和自定义的处理器,当然还可以自定义表单。通过 dongwm@dongwm.com 登录后是看不到 /admin/ 页面的,而用 admin@dongwm.com 就可以看到全部的页面。

  User 类中的 password 字段使用了明文存储,这是为了让例子更清晰,生产环境中请勿使用明文存储重要的内容。



七、Flask-RESTful

  Flask-RESTful 可以快速创建 REST API 服务。


1、安装 Flask-RESTful

  使用以下命令安装 Flask-RESTful:

1
> pip2 install flask-restful==0.3.5



2、使用 Flask-RESTful

  现在实现一个能创建、查询和删除用户的 API 例子 app_restful.py。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# coding=utf-8
from flask import Flask, request
from flask_restful import Resource, Api, reqparse, fields, marshal_with
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config.from_object('config')
api = Api(app)
db = SQLAlchemy(app)


parser = reqparse.RequestParser()
parser.add_argument('admin', type=bool, help='Use super manager mode',
                    default=False)


resource_fields = {
    'id': fields.Integer,
    'name': fields.String,
    'address': fields.String
}


class User(db.Model):
    __tablename__ = 'restful_user'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(128), nullable=False)
    address = db.Column(db.String(128), nullable=True)

db.create_all()


class UserResource(Resource):
    @marshal_with(resource_fields)
    def get(self, name):
        user = User.query.filter_by(name=name).first()
        return user

    def put(self, name):
        address = request.form.get('address', '')
        user = User(name=name, address=address)
        db.session.add(user)
        db.session.commit()
        return {'ok': 0}, 201

    def delete(self, name):
        args = parser.parse_args()
        is_admin = args['admin']
        if not is_admin:
            return {'error': 'You do not have permissions'}
        user = User.query.filter_by(name=name).first()
        db.session.delete(user)
        db.session.commit()
        return {'ok': 0}


api.add_resource(UserResource, '/users/<name>')


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=9000, debug=True)

  在 Flask-RESTful 中,一个地址下的数据称为资源(Resource)。装饰器 marshal_with 做了把模型实例的属性组合成一个字典的抽象工作。在终端上验证效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
(venv) ❯ http -f put http://127.0.0.1:9000/users/xiaoming address='Beijing'
HTTP/1.0 201 CREATED
Content-Length: 16
Content-Type: application/json
Date: Fri, 23 Dec 2022 16:14:40 GMT
Server: Werkzeug/0.11.10 Python/2.7.11+

{
    "ok": 0
}


(venv) ❯ http -f put http://127.0.0.1:9000/users/wanglang --print b
{
    "ok": 0
}


(venv) ❯ http -f get http://127.0.0.1:9000/users/xiaoming --print b
{
    "address": "Beijing",
    "id": 1,
    "name": "xiaoming"
}


(venv) ❯ http -f delete http://127.0.0.1:9000/users/xiaoming --print b
{
    "error": "You do not have permissions"
}


(venv) ❯ http -f delete http://127.0.0.1:9000/users/xiaoming admin=1 --print
 b
{
    "ok": 0
}


(venv) ❯ http -f get http://127.0.0.1:9000/users/xiaoming --print b
{
    "address": null,
    "id": 0,
    "name": null
}



八、Flask-Admin

  有了 Flask-Admin 的帮助,我们用很少的代码就能像 Django 那样就可以实现一个管理后台。它支持 Pymongo、Peewee、Mongoengine、SQLAlchemy 等数据库使用方法,自带了基于模型的数据管理、文件管理、Redis 的页面命令行等类型后台。尤其是模型的管理后台,甚至可以细粒度定制字段级别的权限。


1、安装 Flask-Admin

  使用以下命令安装 Flask-Admin:

1
> pip2 install Flask-Admin==1.4.0   



2、后台功能实现

  现在基于 Flask-Login 和 Flask-SQLAlchemy 实现包含如下功能的后台(app_admin.py):

  • 可以在后台操作数据库中的数据
  • 静态文件管理
  • 在导航栏添加一些链接和视图,比如笔者的 Github 地址、Google 链接以及回首页的链接。还添加一个动态的链接,点击它可以登录和退出。当登录后会动态地添加一个 Authenticated 链接
  • 自定义点击 Authenticated 的链接后看到的模板


(1)定义 User 模型
  借用 users.py 里面的 User,再继承 UserMixin 即可。

1
2
3
4
5
from users import User as _User


class User(_User, UserMixin):
    pass


(2)添加主页、登录和退出的视图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from flask_login import login_user, logout_user

USERNAME = 'xiaoming'


@app.route('/')
def index():
    return '<a href="/admin/">Click me to get to Admin!</a>'


@app.route('/login/')
def login_view():
    user = User.query.filter_by(name=USERNAME).first()
    login_user(user)
    return redirect(url_for('admin.index'))


@app.route('/logout/')
def logout_view():
    logout_user()
    return redirect(url_for('admin.index'))


(3)添加以下的视图仅作为管理后台的可点击链接来使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from flask_admin import Admin
from flask_admin.base import MenuLink


class AuthenticatedMenuLink(MenuLink):
    def is_accessible(self):
        return current_user.is_authenticated


class NotAuthenticatedMenuLink(MenuLink):
    def is_accessible(self):
        return not current_user.is_authenticated


admin.add_link(NotAuthenticatedMenuLink(name='Login',
                                        endpoint='login_view'))
admin.add_link(AuthenticatedMenuLink(name='Logout',
                                     endpoint='logout_view'))

# 也可以直接使用 url 参数指定地址
admin.add_link(MenuLink(name='Back Home', url='/'))
# 这里的 category 会创建一个叫作 Links 的下拉菜单,把 Google 和 Github 链接放进去
admin.add_link(MenuLink(name='Google', category='Links',
                        url='http://www.google.com/'))
admin.add_link(MenuLink(name='Github', category='Links',
                        url='https://github.com/dongweiming'))


(4)指定视图
  在 Flask-Admin 中指定视图需要继承它提供的 BaseView,或者使用 contrib 中自带的视图类,比如 FileAdmin 和 ModelView:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from flask_admin.contrib.sqla import ModelView
from flask_admin.contrib.fileadmin import FileAdmin
from flask_admin.base import BaseView, expose


class MyAdminView(BaseView):
    """
    定义的首页地址没有验证是不能访问的
    """
    
    @expose('/')
    def index(self):
        return self.render('chapter4/section2/authenticated-admin.html')

    def is_accessible(self):
        return current_user.is_authenticated


"""
可以通过设置 endpoint 参数自定义链接,比如 admin.add_view(ModelView(User, db.session) 生成的子路径是 /admin/user,
修改为 admin.add_view(ModelView(User, db.session, endpoint='new_user') ,就可以使用 /admin/new_user 来访问了
"""
admin.add_view(ModelView(User, db.session))
path = os.path.join(os.path.dirname(__file__), '../../static')

# 创建一个名为 Authenticated 的链接,但是必须登录才能访问
admin.add_view(MyAdminView(name='Authenticated'))


(5)使用 before_first_request 钩子初始化数据库

1
2
3
4
5
6
7
8
9
@app.before_first_request
def create_user():
    db.drop_all()
    db.create_all()

    # 使用默认用户 xiaoming 来进行登录
    user = User(name=USERNAME, email='a@dongwm.com', password='123')
    db.session.add(user)
    db.session.commit()


  添加管理后台,当访问 http://127.0.0.1:9000/ 的时候,将显示 index 函数的返回内容。访问 http://127.0.0.1:9000/admin/ 就可以看到未登录的管理后台了,页面只有一个菜单栏。

访问结果如下:

  1. 访问 http://127.0.0.1:9000/
  2. 访问 http://127.0.0.1:9000/admin/
  3. 访问 http://127.0.0.1:9000/admin/user/
      User 这个链接是通过 ModelView 实现的,也就是在后台操作 User 表。操作功能包含修改、创建、删除等。
  4. 访问 http://127.0.0.1:9000/admin/fileadmin/
      Static Files 这个链接是通过 FileAdmin 实现的,它可以管理项目的静态文件。操作功能包含重命名、上传、查看和删除文件,创建目录等。
  5. 访问 Links 下拉菜单
      Links 下拉菜单中包含了 Google 和 Github 选项,点击可跳转至对应的链接。
  6. 登录操作

  7. 未登录时访问 http://127.0.0.1:9000/admin/myadminview/
  8. 登录成功后访问 http://127.0.0.1:9000/admin/myadminview/



九、Flask-Assets

待研究。