第9章:用户角色-Flask Web 开发(第2版)

Web 应用程序中的所有用户并非都是平等的。在大多数应用程序中,有一小部分用户拥有额外的权限,以维持应用程序的顺畅运行。管理员就是其中最好的例子,但是在很多情况下,中等级别的高级用户(例如内容审查员)也存在。要实现这个目标,所有用户都需要与 角色(role) 进行关联。

在应用程序中实现角色有很多方法。方法是否合适,在很大程度上取决于需要支持多少角色以及它们的粒度。比如,在一个只需要管理员和普通用户这两个角色的应用中,在 User 模型中添加一个 is_administrator 属性就可以实现。更复杂的应用程序可能需要额外的角色,在普通用户和管理员之间具有不同级别的权限。在某些应用程序中,孤立地讨论角色可能没有任何意义,给用户分配一组单独的权限(permission)可能是正确的方法。本章介绍的用户角色实现是角色和权限的混合。用户会被分配一个独立的角色,而每个角色定义了用户通过可进行操作的权限列表。

角色的数据库表示

在第5章,为了描述一对多关系,创建了一个简单的 Role 模型来表示角色。示例9-1展示了完善后的 Role 模型。

# 示例 9-1. app/models.py: 角色的数据库模型
class Role(db.Model):
    __tablename__ = 'roles'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), unique=True)
    default = db.Column(db.Boolean, default=False, index=True)
    permissions = db.Column(db.Integer)
    users = db.relationship('User', backref='role', lazy='dynamic')

    def __init__(self, **kwargs):
        super(Role, self).__init__(**kwargs)
        if self.permissions is None:
            self.permissions = 0

这个模型添加了一个 default 字段,这个字段应该只为其中一个角色设置为 True,其它角色为 False。设置为 True 的角色将成为默认角色。默认角色将是分配给新注册用户的角色。由于该字段设置成了索引(index=True),所以在数据库检索是否为默认角色时,速度会非常快。

另一个新加的字段的是 permissions,它以整数的形式定义了角色的权限。一开始,SQLAlchemy 会把它的值设置为 None,所以我们需要在模型类的构造函数中,对其进行改值。

【wrdll 注】其实可以在定义permissions字段时给它设置默认值为0。设置方法为:permissions = db.Column(db.Integer, nullable=False, default=0),如此,便可以省略 Role 模型类的 __init__ 方法。原书后文是否会提及本方法,在本站记录本章时不得而知,且先行将此方法进行记录。

所需的权限列表显然特定于具体的应用,而在本文所述的 Flasky 项目中,权限列表如下:

任务名 权限名 权限值
关注用户 FOLLOW 1
对文章进行评论 COMMENT 2
写文章 WRITE 4
审核评论 MODERATE 8
管理员操作 ADMIN 16

【wrdll 注】有关权限值使用这些整数值的原因,请参见:linux的wrx=421,为什么?

# 示例 9-2. app/models.py: 权限常量
class Permission:
    FOLLOW = 1
    COMMENT = 2
    WRITE = 4
    MODERATE = 8
    ADMIN = 16

一旦权限常量定义完毕,Role 模型类可以增加一些方法了。

# 示例 9-3. app/models.py: Role模型中的权限管理
class Role(db.Model):
    # ...

    def add_permission(self, perm):
        if not self.has_permission(perm):
            self.permissions += perm

    def remove_permission(self, perm):
        if self.has_permission(perm):
            self.permissions -= perm

    def reset_permissions(self):
        self.permissions = 0

    def has_permission(self, perm):
        return self.permissions & perm == perm

现在,可以进行测试了。

(venv) $ flask shell
>>> r = Role(name='User')
>>> r.add_permission(Permission.FOLLOW)
>>> r.add_permission(Permission.WRITE)
>>> r.has_permission(Permission.FOLLOW)
True
>>> r.has_permission(Permission.ADMIN)
False
>>> r.reset_permissions()
>>> r.has_permission(Permission.FOLLOW)
False

下表列出了 Flasky 项目所需的角色。角色的权限通过组合 Permission 类中的权限常量来生成。

角色 权限 说明
None None 只读访问应用程序。 这适用于未登录的游客。
User FOLLOW, COMMENT, WRITE 基本权限。用来撰写文章和评论,并关注其他用户。 这是新用户的默认设置。
Moderator FOLLOW, COMMENT, WRITE, MODERATE User 权限的基础上,添加了审核评论权限。
Administrator FOLLOW, COMMENT, WRITE, MODERATE, ADMIN 拥有所有权限,包括修改其他用户角色的权限

手动将角色保存到数据库不但费时,还容易出错。我们可以在 Role 模型类中添加一个方法来达到这一目的。这是一个可复用的、正确率高的方法。无论在开发环境、测试环境和生产环境,都可以通过这个方法来将角色数据正确地保存到数据库里。

# 示例 9-4. app/models.py: 在数据库中创建角色
class Role(db.Model):
    # ...
    @staticmethod
    def insert_roles():
        roles = {
            'User': [Permission.FOLLOW, Permission.COMMENT, Permission.WRITE],
            'Moderator': [Permission.FOLLOW, Permission.COMMENT,
                          Permission.WRITE, Permission.MODERATE],
            'Administrator': [Permission.FOLLOW, Permission.COMMENT,
                              Permission.WRITE, Permission.MODERATE,
                              Permission.ADMIN],
        }
        default_role = 'User'
        for r in roles:
            role = Role.query.filter_by(name=r).first()
            if role is None:
                role = Role(name=r)
            role.reset_permissions()
            for perm in roles[r]:
                role.add_permission(perm)
            role.default = (role.name == default_role)
            db.session.add(role)
        db.session.commit()

insert_roles() 方法并没有直接创建角色,而是先尝试从角色表中查找同名的角色是否存在,如果存在就进行更新,不存在则创建新角色。如果要增加更多角色,只需在 roles 字典中,新加一个项即可。注意,该方法没有为匿名用户创建角色,因为匿名用户并未登录,所以无需理会。另外请注意,insert_roles() 是静态方法。

分配角色

当用户在应用程序中注册时,需要给他分配一个角色。对于大部分用户而言,User 角色已经足够了,所以我们需要把该角色设置为默认角色。值得注意的是 Administrator 角色。只有特定的用户才会赋予该角色。本项目通过保存在 FLASKY_ADMIN 配置项里的 Email 地址来判断用户是否需要赋予 Administrator 角色。

# 示例 9-5. app/models.py: 给用户定义默认角色
class User(UserMixin, db.Model):
    # ...
    def __init__(self, **kwargs):
        super(User, self).__init__(**kwargs)
        if self.role is None:
            if self.email == current_app.config['FLASKY_ADMIN']:
                self.role = Role.query.filter_by(name='Administrator').first()
            if self.role is None:
                self.role = Role.query.filter_by(default=True).first()
                # ...

角色验证

为简化角色和权限的实现,可以给 User 模型增加一个检测这个用户被分配的角色及其对应的权限的辅助函数。

# 示例 9-6. app/models.py: 评估用户是否具有某个权限
from flask_login import UserMixin, AnonymousUserMixin

class User(UserMixin, db.Model):
    # ...

    def can(self, perm):
        return self.role is not None and self.role.has_permission(perm)

    def is_administrator(self):
        return self.can(Permission.ADMIN)

class AnonymousUser(AnonymousUserMixin):
    def can(self, permissions):
        return False

    def is_administrator(self):
        return False

login_manager.anonymous_user = AnonymousUser

can() 用来检测用户是否具有某项操作的权限;is_administrator() 用来判断用户是否是管理员。注意,我们添加了一个 AnonymousUser 类,它也实现了 can()is_administrator()。添加该类的意义在于,当我们使用 current_user.can()current_user.is_administrator() 时,不需要手动判断用户是否登录。

对于只具有特定权限的用户才能使用的视图函数,我们需要自定义一个装饰器进行限制。

# 示例 9-7. app/decorators.py: 用于检测用户权限的自定义装饰器
from functools import wraps
from flask import abort
from flask_login import current_user
from .models import Permission

def permission_required(permission):
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            if not current_user.can(permission):
                abort(403)
            return f(*args, **kwargs)
        return decorated_function
    return decorator

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

403 是 HTTP 中表示“禁止访问”的状态码。按第3章添加 404 和 500 错误处理程序的方法,可以很容易给 403 添加错误处理程序。

下面是两个使用这些装饰器的例子。

from .decorators import admin_required, permission_required

@main.route('/admin')
@login_required
@admin_required
def for_admins_only():
    return "For administrators!"

@main.route('/moderate')
@login_required
@permission_required(Permission.MODERATE)
def for_moderators_only():
    return "For comment moderators!"

注意装饰器链的顺序

模板中也需要检测权限,所以我们需要把 Permission 类传递给模板。app_context_processor 可以将所有 render_template() 都需要的变量自动传递给模板。

# 示例 9-8. app/main/__init__.py: 将 Permission 添加到模板上下文
@main.app_context_processor
def inject_permissions():
    return dict(Permission=Permission)

示例9-9对角色和权限进行单元测试。

# Example 9-9. tests/test_user_model.py: 角色和权限的单元测试
class UserModelTestCase(unittest.TestCase):
    # ...

    def test_user_role(self):
        u = User(email='[email protected]', password='cat')
        self.assertTrue(u.can(Permission.FOLLOW))
        self.assertTrue(u.can(Permission.COMMENT))
        self.assertTrue(u.can(Permission.WRITE))
        self.assertFalse(u.can(Permission.MODERATE))
        self.assertFalse(u.can(Permission.ADMIN))

    def test_anonymous_user(self):
        u = AnonymousUser()
        self.assertFalse(u.can(Permission.FOLLOW))
        self.assertFalse(u.can(Permission.COMMENT))
        self.assertFalse(u.can(Permission.WRITE))
        self.assertFalse(u.can(Permission.MODERATE))
        self.assertFalse(u.can(Permission.ADMIN))

如果你从 Github 克隆了示例代码,可以使用 git checkout 9a 签出本示例。

在开始下一章学习之前,请将角色添加到开发环境的数据库中,如下所示。

(venv) $ flask shell
>>> Role.insert_roles()
>>> Role.query.all()
[<Role 'Administrator'>, <Role 'User'>, <Role 'Moderator'>]

如果在创建角色之前,已经有用户了,请给这些用户赋予对应的角色,如下所示。

(venv) $ flask shell
>>> admin_role = Role.query.filter_by(name='Administrator').first()
>>> default_role = Role.query.filter_by(default=True).first()
>>> for u in User.query.all():
...     if u.role is None:
...         if u.email == app.config['FLASKY_ADMIN']:
...             u.role = admin_role
...         else:
...             u.role = default_role
...
>>> db.session.commit()

至此,用户系统几近完成。下一章将在此基础上创建用户资料页面。

本文并非《Flask Web Development(2nd edition)》的全文翻译。而只是在阅读该书时,对自认为重点的内容进行记录以及思考。由于水平有限,本文所述内容难免出现不足或错误。请通过购买正版图书来进行更系统地学习,原书地址。本站将依照本系列文章录制视频教程,届时本文未作记录的部分以及项目实际开发中所需要的知识都会在视频教程中进行补充。

flask Flask Web Development(2nd edition) 2018-03-27 18:40 1599782