有多种方法可用于在程序中实现角色。具体采用何种实现方法取决于所需角色的数量和细分程度。例如,简单的程序可能只需要两个角色,一个表示普通用户,一个表示管理员。对于这种情况,在 User 模型中添加一个 is_administrator 布尔值字段就足够了。复杂的程序可能需要在普通用户和管理员之间在细分出多个不同等级的角色。有些程序甚至不能使用
分立的角色,这时赋予用户某些权限的组合或许更合适。
角色在数据库中的表示
我们原先已经创建了一个简单的 roles 表,用来表示一对多关系。现在我们来建立改进后的 Role 模型。
修改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 __repr__(self):
return '<Role %r>' % self.name
只有一个角色的 default 字段要设为 True ,其它都要设为 False 。用户注册时,其角色会被设为默认值。
这个模型的第二处改动是添加了 permissions 字段,其值是一个整数,表示位标志。各操作都对应一个位位置,能执行某项操作的角色,其位会被设为 1.
显然,各操作所需的程序权限是不一样的。
权限表
操作 | 位值 | 说明 |
---|---|---|
订阅专辑 | 0b00000001(0x01) | 订阅专辑 |
在他人的文章中发表评论 | 0b00000010(0x02) | 在他人撰写的文章发表评论 |
写文章 | 0b00000100(0x04) | 写原创文章 |
管理他人发表的评论 | 0b00010000(0x08) | 审核他人发表的评论 |
管理员权限 | 0b10000000(0x80) | 管理网站 |
注意,操作的权限使用 8 位表示,现在只用了其中的 5 位,其它的 3 位可用于将来的扩展。
修改app/models.py
的内容,添加权限常量:
class Permission(object):
FOLLOW = 0x01
COMMENT = 0x02
WRITE_ARTICLES = 0x04
MODERATE_COMMENTS = 0x08
ADMINISTER = 0x80
用户角色表
用户角色 | 权限 | 说明 |
---|---|---|
匿名 | 0b00000000(0x00) | 未登录的用户。在程序中只有阅读权限 |
一般用户 | 0b00000001(0x01) | 具有订阅专辑的权限。这是新用户的默认角色 |
认证用户 | 0b00000111(0x07) | 具有发布文章、发表评论和订阅专辑的权限 |
协管员 | 0b00011111(0x0f) | 增加审核不当评论的权限 |
超级管理员 | 0b11111111(0xff) | 具有所有权限 |
使用权限组织角色,这一做法让你以后添加新角色时只需要使用不同的权限组合即可。
添加一个类方法,完成将角色手动添加到数据库中的功能。
修改app/models.py
的内容,在数据库中创建角色:
class Role(db.Model):
# ...
def insert_roles():
roles = {
u'一般用户': (Permission.FOLLOW, True),
u'认证用户': (Permission.FOLLOW | |
Permission.WRITE_ARTICLES, False),
u'协管员': (Permission.FOLLOW |
|
Permission.WRITE_ARTICLES |
Permission.MODERATE_COMMENTS, False),
u'超级管理员': (0xff, False)
}
for r in roles:
role = Role.query.filter_by(name=r).first()
if role is None:
role = Role(name=r)
role.permissions = roles[r][0]
role.default = roles[r][1]
db.session.add(role)
insert_roles()
函数并不直接创建新角色对象,而是通过角色查找现有的角色,然后再进行更新。只有当数据库中没有某个角色时才会创建新角色对象。如此一来,如果以后更新了角色列表,就可以执行更新操作了。要想添加新角色,或者修改角色的权限,修改 roles 数组,再运行函数即可。
若想把角色写入数据库,可使用 shell 会话。
(flask) $ python manage.py shell
>>>Role.insert_roles()
>>>Role.query.all()
[<Role u' 一般用户 '>, <Role u' 认证用户 '>, <Role u' 布道者 '>, <Role u' 协
管员 '>, <Role u' 超级管理员 '>]
赋予角色
用户在程序中注册账户时,会被赋予适当的角色。大多数用户在注册时赋予的角色都是“用户”,即默认角色。唯一例外的是管理员,管理员在最开始就应该赋予“管理员”角色。管理员由保存在 config.py
中的变量OUSI_ADMIN
电子邮件地址识别,只要这个电子邮件地址出现在注册请求中,就会被赋予正确的角色。
修改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['OUSI_ADMIN']:
self.role = Role.query.filter_by(permissions=0xff).first()
if self.role is None:
self.role = Role.query.filter_by(default=True).first()
User 类的构造函数首先调用基类的够找函数,如果创建基类对象后还没有定义角色,则根据电子邮件地址决定将其设置为管理员还是默认角色。
角色验证
为了简化角色和权限的实现过程,我们可在 User 模型中添加一个辅助方法,检查是否有指定的权限。
修改app/models.py
的内容,添加检查用户是否有指定的权限的功能:
from flask.ext.login import UserMixin, AnonymousUserMixin
class User(UserMixin, db.Model):
# …
def can(self, permissions):
return self.role is not None and \
(self.role.permissions & permissions) == permissions
def is_administrator(self):
return self.can(Permission.ADMINISTER)
class AnonymousUser(AnonymousUserMixin):
def can(self, permissions):
return False
def is_administrator(self):
return False
login_manager.anonymous_user = AnonymousUser
User模型中添加的can()方法在请求和赋予角色这两种权限之间进行位与操作。如果角色中包含请求的所有权限位,则返回True,表示允许用户执行此项操作。检查管理员权限的功能经常用到,因此使用单独的方法is_administrator()
实现。
出于一致性考虑,我们还定义了AnonymousUser
类,并实现了can()
方法和is_administrator()
方法。这个对象继承子Flask-Login中的AnonymousUserMixin
类,并将其设为用户未登录时current_user的值。这样程序不用先检查用户是否登录,就能自由调用 current_user.can()
和 current_user.is_administrator()
。
很多时候,我们需要特定的页面向特定权限的用户开放,为了实现这个功能,我们可以使用自定义的修饰器。
修改app/models.py
的内容,添加检查用户权限的自定义修饰器:
# -*- coding:utf-8 -*-
__author__ = u'东方鹗'
from threading import Thread
from functools import wraps
from flask import abort
from flask.ext.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.ADMINISTER)(f)
这两个修饰器都使用了Python标准库中的functools包,如果用户不具有指定权限,则返回403错误码,即HTTP“禁止”错误。
在模板中可能也需要检查权限,所以Permission
类为所有位定义了常量以便于获取。为了避免每次调用render_template()
时都多一个模板参数,可以使用上下文处理器。上下文处理器能让变量在所有模板中全局可访问。
修改app/main/__init__.py
内容,把Permission类加入模板上下文
from ..models import Permission
@main.app_context_processor
def inject_permissions():
return dict(Permission=Permission)