本篇所有操作均在基于 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 来管理项目了,输入以下命令行来管理:
- 在终端通过 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
- 在测试环境中可以用来清理数据
1 2
(venv) ❯ python2 manage.py dropdb Are you sure you want to lose all your data [n]: n
- 自带了三个内置变量的 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
- 通过 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,流程如下:
- 初始化迁移工作
使用以下命令进行初始化迁移,迁移完成后会在当前目录下增加一个 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
- 修改 login_users 表的模型结构
给 User 类添加 email 和 password 这两个字段,字段描述如下:1 2
email = db.Column(db.String(128), nullable=False) password = db.Column(db.String(256), nullable=False)
- 创建迁移的脚本
执行以下命令将会在 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 ###
- 更新数据库
使用以下命令更新数据库,这一步才是实际操作数据库。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)
- 取消更新数据库
如果发现有问题也可以很轻松地取消更新,将当前版本降级回退到上一个版本,使用以下命令即可。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/ 就可以看到未登录的管理后台了,页面只有一个菜单栏。
访问结果如下:
- 访问 http://127.0.0.1:9000/
- 访问 http://127.0.0.1:9000/admin/
- 访问 http://127.0.0.1:9000/admin/user/
User 这个链接是通过 ModelView 实现的,也就是在后台操作 User 表。操作功能包含修改、创建、删除等。
- 访问 http://127.0.0.1:9000/admin/fileadmin/
Static Files 这个链接是通过 FileAdmin 实现的,它可以管理项目的静态文件。操作功能包含重命名、上传、查看和删除文件,创建目录等。
- 访问 Links 下拉菜单
Links 下拉菜单中包含了 Google 和 Github 选项,点击可跳转至对应的链接。
- 登录操作
- 未登录时访问 http://127.0.0.1:9000/admin/myadminview/
- 登录成功后访问 http://127.0.0.1:9000/admin/myadminview/
九、Flask-Assets
待研究。