创建用户相关模型
创建权限和角色模型
一个网站最开始做的功能应该是用户系统,因为后面许多功能都需要与用户系统交互,用户系统最核心的部分就是用户相关的 ORM 模型。该系统的前台和后台用的是同一个用户系统,而后台系统中需要角色和权限管理。首先来添加权限 ORM 模型,在 models/user.py 中添加以下代码。
class PermissionEnum(Enum):
BOARD = "板块"
POST = "帖子"
COMMENT = "评论"
FRONT_USER = "前台用户"
CMS_USER = "后台用户"
class PermissionModel(db.Model):
__tablename__ = "permission"
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.Enum(PermissionEnum), nullable=False, unique=True)
上述代码中,添加了 PermissionEnum 枚举类型和 PermissionModel 模型,为了在程序中更好地分辨普通类和 ORM 模型,在所有 ORM 模型的名称后面加上 Model 后缀。PermissionEnum 是存放权限类型的枚举,下面的 PermissionModel 中 name 的值就需要从这个枚举中获取。PermissionModel 中有两个字段,分别是主键 id 以及权限名称,需要指定权限名称不能为空,且值也是唯一的。后期可以根据业务需求添加权限,如管理帖子的权限、管理评论的权限等,没有相应权限的用户则无法执行相关操作。PermissionModel 不是直接和用户关联,而是先跟角色关联,角色再和用户关联。在执行某个操作时,会先判断用户所属的角色是否包含对应的权限。其中角色和权限属于多对多的关系,即一个权限可以被多个角色拥有,一个角色也可以拥有多个权限。下面再来实现角色模型,在 models/user.py 中添加以下代码。
from exts import db
from datetime import datetime
from enum import Enum
class PermissionEnum(Enum):
BOARD = "板块"
POST = "帖子"
COMMENT = "评论"
FRONT_USER = "前台用户"
CMS_USER = "后台用户"
class PermissionModel(db.Model):
__tablename__ = "permission"
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.Enum(PermissionEnum), nullable=False, unique=True)
# 角色与权限的多对多关联表
role_permission_table = db.Table(
"role_permission_table",
db.Column("role_id", db.Integer, db.ForeignKey("role.id")),
db.Column("permission_id", db.Integer, db.ForeignKey("permission.id"))
)
class RoleModel(db.Model):
__tablename__ = 'role'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String(50), nullable=False)
desc = db.Column(db.String(200), nullable=True)
create_time = db.Column(db.DateTime, default=datetime.now)
# 多对多关系:角色与权限
permissions = db.relationship(
"PermissionModel",
secondary=role_permission_table,
backref="roles"
)
上述代码中,添加了一个 RoleModel 模型,并且给该模型添加了 4 个常规字段,分别是主键 id、角色名称 name、角色描述 desc,以及创建时间 create_time。除了这 4 个常规字段外,还添加了一个关系属性 permissions,并与 PermissionModel 进行了关联,因为 RoleModel 和 PermissionModel 属于多对多的关系,所以在 db.relationship 中通过 secondary 参数设置中间表为 role_permission_table,在 role_permission_table 中也分别添加了外键 role_id 和 permission_id 来引用 role 和 permission 表。另外,还在 db.relationship 中指定了 backref 参数值为 roles,以后通过 PermissionModel 对象的 roles 属性即可访问到该权限下所有与其关联的角色。
在 PermissionModel 和 RoleModel 创建完成后,我们再把模型映射到数据库中。这里需要借助 Flask-Migrate 插件,下面返回 app.py 中,创建 Migrate 对象,代码如下。
from flask_migrate import Migrate
# 创建 Flask 应用实例
app = Flask(__name__)
# 从配置类中加载配置
app.config.from_object(config.DevelopmentConfig)
# 初始化数据库迁移
migrate = Migrate(app, db)
...
下面重新打开 PyCharm Terminal,输入以下命令完成迁移环境的初始化。
$ flask db init
现在 models/user.py 模块并没有被直接或者间接地导入 app.py,为了在迁移时能让程序识别到 models/user.py 中的 ORM 模型,先手动在 app.py 中导入 models/user.py,代码如下。
...
from models import user
...
接着同样在 PyCharm 的 Terminal 中执行以下命令,以生成迁移脚本并完成迁移脚本的执行,执行 migrate 命令后的效果如图 9-11 所示。
flask db migrate -m "create permission and role model"

执行完以上命令后,已经为 ORM 模型生成了迁移脚本,路径在图 9-11 中的最后一行。但是此时并没有真正同步到数据库中,因此还需要执行以下命令才会同步到数据库中,执行 upgrade 命令后的效果如图 9-12 所示。
flask db upgrade

创建权限和角色
权限和角色模型已经创建完成,为了方便后期开发,需要在这两张表中添加数据。一般情况下,权限和角色的数据在网站上线运营后便不会轻易更改。我们在实际开发中可以跟产品经理或者运营同事沟通,确定好权限规则以及角色安排,然后在项目中把这些数据写好,并且集成到命令中,项目上线时,只要执行这条命令即可完成数据的初始化。
我们首先来学习在 Flask 项目中如何创建命令。在安装 Flask 时,默认会安装 click 库,读者依次单击 PyCharm 的 File→Settings→Project: pythonbbs→Python Interpreter,可以看到已经安装了 click 库,如图 9-13 所示。

click 库的主要作用就是用来实现命令,Flask 中已经针对 click 库进行了集成,通过 app.cli 即可访问到 click 对象。我们先来实现一个简单的命令,在 app.py 中输入以下代码。
import click
...
@app.cli.command("my-command")
def my_command():
click.echo("这是我自定义的命令")
...
上述代码中,通过 @app.cli.command 装饰器将 my_command 函数添加到命令中,并且指定命令的名称为 my-command,然后在 PyCharm 的 Terminal 中输入以下命令。
$ flask my-command
执行命令即可看到打印文字 “这是我自定义的命令”,效果如图 9-14 所示。

学会了如何在 Flask 中集成命令后,我们再来添加创建权限和角色的命令。按照项目需求,权限和角色是针对后台用户的,因此权限和角色都是针对后台数据管理的。后台需要管理的数据有板块、帖子、评论、前台用户、后台用户,我们针对每个模块分别添加一个权限。在 app.py 中,实现一个名叫 create-permission 的命令,代码如下。
from models.user import PermissionModel, RoleModel, PermissionEnum
import click
from exts import db
@app.cli.command("create-permission")
def create_permission():
for permission_name in dir(PermissionEnum):
if permission_name.startswith("__"):
continue
permission = PermissionModel(name=getattr(PermissionEnum, permission_name))
db.session.add(permission)
db.session.commit()
click.echo("权限添加成功!")
下面在 PyCharm 的 Terminal 中输入命令 flask create-permission,看到输出文字 “权限添加成功!”,说明权限数据已经创建成功,效果如图 9-15 所示。

权限数据创建成功后,我们再来添加角色。这里创建 3 个角色,分别为稽查、运营、管理员。这 3 个角色包含的权限如表 9-1 所示。

稽查角色主要是审核用户发布的帖子和评论是否存在违法或者违反社区正常运营的情况。如果存在,则进行处理。而运营角色则有较大权限,可以管理除后台用户以外的其他所有功能。管理员角色除拥有运营角色下所有权限以外,还拥有管理后台用户的权限,即可以设置谁为稽查、谁为运营等。根据以上需求,创建添加角色的命令 create-role,代码如下。
@app.cli.command("create-role")
def create_role():
# 稽查
inspector = RoleModel(name="稽查", desc="负责审核帖子和评论是否合法合规!")
inspector.permissions = PermissionModel.query.filter(
PermissionModel.name.in_([PermissionEnum.POST, PermissionEnum.COMMENT])
).all()
# 运营
operator = RoleModel(name="运营", desc="负责网站持续正常运营!")
operator.permissions = PermissionModel.query.filter(
PermissionModel.name.in_([
PermissionEnum.POST,
PermissionEnum.COMMENT,
PermissionEnum.BOARD,
PermissionEnum.FRONT_USER,
PermissionEnum.CMS_USER
])
).all()
# 管理员
administrator = RoleModel(name="管理员", desc="负责整个网站所有工作!")
administrator.permissions = PermissionModel.query.all()
db.session.add_all([inspector, operator, administrator])
db.session.commit()
click.echo("角色添加成功!")
上述代码中,创建了 3 个 RoleModel 对象,分别为稽查 inspector、运营 operator、管理员 administrator,并且按照表 9-1 分别给 3 个对象设置了 permissions 属性。完成命令编写后,在 PyCharm 的 Terminal 下执行命令 flask create-role,效果如图 9-16 所示。

现在虽然成功添加了命令,但是命令代码全部写在 app.py 中,如果以后还要增加其他命令,那么会导致 app.py 越来越臃肿。为了给 app.py 瘦身,我们把命令代码单独放到一个模块中,在 pythonbbs 项目根路径下创建一个 commands.py 文件,然后把 create-permission 和 create-role 函数及其装饰器代码剪切到 commands.py 文件中,并且再把命令代码中依赖的包也剪切过去,剪切后的 commands.py 文件内容如图 9-17 所示。

从图 9-17 可以看到,commands.py 中有两个错误,原因是 create_permission 和 create_role 的 @app.cli.command 装饰器中的 app 对象找不到。如果从 app.py 中导入 app 对象,那么会造成循环引用,只有把 commands.py 导入 app.py 才能把命令注册到项目中。为了解决这个问题,把 create_permission 和 create_role 函数上的 @app.cli.command 装饰器删除,然后在 app.py 中手动添加,修改后的代码如下,效果如图 9-18 所示。
...
import commands
...
# 添加命令
app.cli.command("create-permission")(commands.create_permission)
app.cli.command("create-role")(commands.create_role)

在 Python 语法中,装饰器本质上是函数,所以可以把 @app.cli.command 装饰器直接当作函数来使用。通过以上代码重构,在不产生循环引用的前提下,就实现了将 app.py 瘦身的目的。
创建用户模型
接下来回到 models/user.py 文件中创建用户模型。用户数据是网站最重要的数据之一,如果把用户表主键存储为自增长的整型,则容易被竞争对手猜测出总共有多少用户,猜测方法非常简单,一般网站都有查看用户信息的个人中心页面,而个人中心页面的 URL 中必须携带用户 ID 参数,如果用户 ID 是自增长的整型,则竞争对手只要获取到用户 ID 的最大值,也就知道了此网站用户的数量,很有可能为公司带来巨大损失。因此在商业网站中,都用唯一的字符串作为用户表的主键。产生唯一字符方法的库有许多,最常用的是 UUID(universally unique identifier),UUID 会出现重复值的概率几乎为零,可以忽略不计。UUID 的长度为 32 个字符,再加上 4 个横线,总共有 36 个字符,考虑到 UUID 太长会对数据库查询造成性能上的影响,我们使用 shortuuid。shortuuid 是一个第三方 Python 库,会对原始 UUID 进行 base57 编码,然后删除相似的字符,如 I、1、L、o 和 0,最后生成默认 22 位长度的字符串。打开 PyCharm 的 Terminal,输入以下命令安装 shortuuid。
$ pip install shortuuid
在使用的时候,直接从 shortuuid 中导入 uuid 方法,然后调用该方法,便会自动生成一串唯一的字符串,如图 9-19 所示。

安装完 shortuuid 后,就可以将其应用到用户模型中。我们再回到 models/user.py 中,添加以下代码。
...
from shortuuid import uuid
...
class UserModel(db.Model):
__tablename__ = 'user'
id = db.Column(db.String(100), primary_key=True, default=uuid)
username = db.Column(db.String(50), nullable=False, unique=True)
password = db.Column(db.String(200), nullable=False)
email = db.Column(db.String(50), nullable=False, unique=True)
avatar = db.Column(db.String(100))
signature = db.Column(db.String(100))
join_time = db.Column(db.DateTime, default=datetime.now)
is_staff = db.Column(db.Boolean, default=False)
is_active = db.Column(db.Boolean, default=True)
# 外键
role_id = db.Column(db.Integer, db.ForeignKey("role.id"))
role = db.relationship("RoleModel", backref="users")
上述代码中,我们添加了 UserModel 模型,关于 UserModel 的字段及相关介绍如下。
-
id:主键,字符串类型,默认会使用 shortuuid.uuid 函数自动生成主键。
-
username:用户名,不能为空,并且值必须唯一。
-
password:密码,最大长度为200,应该先加密再存储。
-
email:邮箱,不能为空,值唯一,作为登录的凭证。
-
avatar:头像,存储图片在服务器中保存的路径,可以为空。
-
signature:签名,可以为空。
-
join_time:加入时间,在第一次创建时会使用当前时间存储。
-
is_staff:是否是员工,只有员工才能进入后台系统,默认为 False。
-
is_active:是否可用,默认情况下是可用的,如果不可用,则会限制其登录。
-
role_id:角色外键,引用 role 表的 id 字段。
-
role:关系属性,引用 RoleModel。反过来,也可以通过 RoleModel 对象的 users 属性获取此角色下的所有用户。
用户的密码数据,必须经过加密才能存储进去,这样做的目的是,即使服务器遭到黑客攻击,用户数据被泄露,黑客也只能获取到加密的密码,而不是原始密码。人们面对繁多的互联网产品,为了方便记忆,通常会设置同一个密码,如果黑客能获取到原始密码,并且通过某些手段获取到了用户在其他平台的账号,则大概率在其他平台也能登录成功,这对用户来讲无疑是灾难性的。在 Flask 项目中,可以通过 werkzeug.securit 模块中的以下两个方法实现密码的加密和验证。
-
generate_password_hash(password):对 password 进行加密,并返回加密后的密码。
-
check_password_hash(pwhash,password):pwhash 是加密后的密码,password 是原始密码,将 password 按照相同的加密方式加密,然后与 pwhash 进行对比,如果相等则认为密码正确,否则认为密码错误。
现在使用 UserModel 来添加一条数据,代码如下。
from werkzeug.security import generate_password_hash
user = UserModel(username='example', email="example@domain.com")
user.password = generate_password_hash("password")
为了简化创建用户时生成密码的代码,我们将 UserModel 进行重构,重构的思路是,在创建用户时,直接传 password 进去就会自动完成加密;或者如果用户通过 user.password="password" 的方式设置密码时,也要自动完成加密。重构后的 UserModel 代码如下。

上述代码中,为了实现通过 password 属性设置密码时能自动加密,把原来的 password 属性修改成了 _password。然后通过 Python 中的 @property 装饰器将 password() 方法定义成属性,以后通过 user.password 可以获取加密后的密码,通过 user.password="password" 会触发 @password.setter 下的 password 方法,在这个方法中把原始密码加密后赋值给了 _password 属性。为了以后验证密码方便,实现了 check_password 方法,该方法调用了 check_password_hash,以后通过 user.check_password("password") 即可返回密码是否正确。我们还定义了 has_permission 方法,用来快速判断当前用户是否拥有某个权限。至此,UserModel 模型就创建成功了,打开 PyCharm 的 Terminal,然后输入以下两条命令即可完成数据库的同步更新。
$ flask db migrate -m "create user model"
$ flask db upgrade
创建测试用户
为了方便后续开发,按照角色个数创建 3 个员工账号。在 commands.py 文件中,添加以下代码。
...
def create_test_user():
admin_role = RoleModel.query.filter_by(name="管理员").first()
zhangsan = UserModel(username="张三", email="zhangsan@zlkt.net", password="111111", is_staff=True, role=admin_role)
operator_role = RoleModel.query.filter_by(name="运营").first()
lisi = UserModel(username="李四", email="lisi@zlkt.net", password="111111", is_staff=True, role=operator_role)
inspector_role = RoleModel.query.filter_by(name="稽查").first()
wangwu = UserModel(username="王五", email="wangwu@zlkt.net", password="111111", is_staff=True, role=inspector_role)
db.session.add_all([zhangsan, lisi, wangwu])
db.session.commit()
click.echo("测试用户添加成功!")
上述代码中,添加了张三、李四、王五 3 个员工账号,分别对应管理员、运营、稽查 3 种角色,方便后期进行测试使用。创建测试用户的代码写完后,再回到 app.py 中,把 create_test_user 集成到项目中,代码如下。
# 添加命令
app.cli.command("create-permission")(commands.create_permission)
app.cli.command("create-role")(commands.create_role)
# 添加创建测试用户命令
app.cli.command("create-test-user")(commands.create_test_user)
重新打开 PyCharm 的 Terminal,然后输入以下命令。
$ flask create-test-user
执行以上命令即可把代码中的 3 个账号添加到数据库中。
创建管理员
项目后期在部署到服务器后,应该通过命令完成第一个管理员的初始化。在 commands.py 中添加以下命令。
@click.option("--username", '-u')
@click.option("--email", '-e')
@click.option("--password", '-p')
def create_admin(username, email, password):
admin_role = RoleModel.query.filter_by(name="管理员").first()
admin_user = UserModel(username=username, email=email, password=password, is_staff=True, role=admin_role)
db.session.add(admin_user)
db.session.commit()
click.echo("管理员创建成功!")
上述代码中,在命令函数 create_admin 中,通过 @click.option 装饰器添加了 3 个参数。以后在命令行中即可使用 --username、--email、--password 将用户名、邮箱、密码当作参数传到函数中。在 app.py 中注册命令,代码如下。
# 添加创建管理员命令
app.cli.command("create-admin")(commands.create_admin)