Flask Web DEvelopment翻译8

第九章 用户角色

在web程序中并不是所有的用户都是平等的。大部分程序里,需要小比例的可信用户具备额外的权力来保证系统更好的运行。管理员组就是最好的例子,但多数情况下,还有中层的高级用户组(如内容版主)。实现角色管理的方式不一,方法合适与否很大程度上取决于需要支持多少角色和角色有多复杂。例如,一个简单的系统可能只需要两种角色:普通用户和管理员。这种情况下,可能也就是需要在User模型中添加个is_adminstrator字段就行了。而一个复杂的程序可能需要介于普通用户和管理员两者之间的多种用户角色,他们拥有更多、更详细的权力划分。还有可能是,在一些程序中讨论独立的用户角色毫无意义,相反 ,给用户指派一套合适的权限|许可(permissions)组合更为可行。
  本章的用户角色设计,是基于独立角色和许可权限二者的混合体。给用户指定一个独立角色,但该角色是基于一系列的权限定义的。

角色数据库设计

第五章我们设计过一个简单的roles表用来演示一对多关系,例子9-1展示了对这个role模型的改进:

Example 9-1. app/models.py: Role permissions

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')

如果只有一个角色,default字段应该设置为True,反之有多个角色就设置为False。标记为default的角色也是新用户注册时默认的角色。
  模型第二处添加是permissions字段,这是一个整数类型,但被用作二进制位标志。每个任务操作都会分配一个二进制位置,对于任务操作许可的每个角色,任务会把这个二进制位设置为1。(译注:有点乱,应该结合程序权限表9-1和9-2理解:二进制bit值共八位<0b后面8个零>默认无权限用户是8位全部0,操作分别有5种(关注,评论,撰写,屏蔽,管理权) 从右往左,依次设置为1,管理权使第八位。表9-2则是合并权限组合定义了角色,无权用户8位全是零,普通用户拥有撰写,评论,关注三项许可权限(权限标志位分别是1,2,3),所以二进制许可代码组合就是0b00000111,其他同理
  权限许可所需要的任务列表当然是由程序指定好的,清单如表9-1所示:

Table 9-1. Application permissions

任务名                          二进制值              描述
Follow users                      0b00000001 (0x01)     关注别的用户
Comment on posts made by others    0b00000010 (0x02)    评论别人的文章
Write articles                  0b00000100 (0x04)   撰写文章
Moderate comments made by others   0b00001000 (0x08)    屏蔽|抑制攻击性言论
Administration access            0b10000000 (0x80)  管理访问权限

注意,一共八位分配给任务,但实际使用了5位,剩下三位冗余将来扩展备用。
表9-1的实现显示在例子9-2中

Example 9-2. app/models.py: Permission constants
class Permission:
    FOLLOW = 0x01
    COMMENT = 0x02
    WRITE_ARTICLES = 0x04
    MODERATE_COMMENTS = 0x08
    ADMINISTER = 0x80

表9-2与权限许可二进制定义一起显示了可以支持的用户角色列表,

Table 9-2. User roles
用户角色        权限许可组合           说明
Anonymous     0b00000000 (0x00)     未登录用户,只读方式访问程序所有标志为都是0
User           0b00000111 (0x07)    基本权限许可,可以写文章、评论、关注他人。这也是默认的新用户权限许可组合。
Moderator     0b00001111 (0x0f)     在user基础上增加屏蔽|抑制不恰当或攻击性言论的许可
Administrator   0b11111111 (0xff)   完全访问,包括更改其他用户角色的许可权限组合,所有标志位都是1

通过许可权限组合定义角色,这一设计将允许你在将来使用不同许可组合增加新的角色。手工向数据库添加角色即耗费时间,又易出错。我们向Role类添加一个类方法来实现这一点,如例子9-3所示

Example 9-3. app/models.py: Create roles in the database
class Role(db.Model):
    # ...
    @staticmethod
    def insert_roles():
        roles = {
            'User': (Permission.FOLLOW |Permission.COMMENT|Permission.WRITE_ARTICLES, True),
            'Moderator': (Permission.FOLLOW |Permission.COMMENT |Permission.WRITE_ARTICLES |Permission.MODERATE_COMMENTS, False),
            'Administrator': (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)
        db.session.commit()

insert_roles()函数并没有直接创建新的角色对象。它试图通过名称来搜索已存在的角色并更新。只有数据库里并不存在指定名称的角色时才会创建一个新的角色对象。这就保证了将来一旦需要就可以用来更新角色列表。为了添加一个新角色或者更改指派给角色的权限许可,更改roles数组并把这个函数作为结果返回。需要注意的是,"Anonymouse"角色并不需要在数据库里指定,实际上它就是为不存在于数据库中的用户设计的。
  我们使用shell会话把这些角色添加到数据库:

(venv) $ python manage.py shell
>>> Role.insert_roles()
>>> Role.query.all()
[<Role u'Administrator'>, <Role u'User'>, <Role u'Moderator'>]

2016-08-20注:此处需要修改manage.py引入role对象,并添加到shell上下文中。否则执行shell命令将提醒你role未定义。同理,如果要在shell中使用User,Permission,Post,Follow,Comment之类,一并引入即可,后文中如遇错误,请参此处。
#mange.py
from app.models import Role,User,Permission
...
def make_shell_context():
return dict(app=app,db=db,Role=Role,User=User,Permission=Permission)

角色指派

当用户注册一个帐户时,就应该给他指派一个合适的角色。对于大部分用户,注册时赋予的角色应该是“User”,我们标记为default的那个。唯一例外就是创建管理员帐户,一开始它就需要指派为“administrator”。这个用户使用FLASK_ADMIN配置变量的email值来注册,所以一旦email地址出现在注册请求中,就可以被赋予正确的角色。例子9-4展示了User模型的构造函数如何完成这一步:

Example 9-4. app/models.py: Define a default role for users
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(permissions=0xff).first()
            if self.role is None:
                self.role = Role.query.filter_by(default=True).first()
    # ...

User构造函数首先调用基础类的构造函数,如果基础类对象并没有定义角色,就会根据email地址判断是设置管理员还是默认角色。

角色认证

为了简化角色和权限的实现,我们给User模型添加一个辅助方法,来检查是否存在指定权限。如例子9-5所示

Example 9-5. app/models.py: Evaluate whether a user has a given permission

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()方法对请求的权限和角色权限执行了"位与"(bitwise and)操作。如果所有请求的位(允许用户执行该操作的二进制位标志)已经在角色指定,即用户请求的操作被放行,这个方法就返回True。管理员权限的检查非常简单,也就是实现了一个单独的is_administrator()方法。
  为了保持一致性,我们创建了一个自定义的AnonymousUser类,仅仅实现了can()和is_administrator()两个方法。它继承自flask-login的AnonymousUserMixin类,并被注册为未登录用户的父类——用户没有登录时current_user对象将继承该类。这样,程序就能无需提前检查用户是否登录,即可自由调用current_user.can()和current_user.is_aministraotr()这两个方法。
  在有些情况下,一个实体视图函数需要仅仅在用户具备权限许可的情况下才能使用,我们可以自定义一个装饰器。例子9-6展示了如何实现这连个装饰器,一个是检查普通权限,另一个是专门检查管理员权限的。

Example 9-6. app/decorators.py: Custom decorators that check user permissions

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错误代码,代表“禁止访问”("forbidden")HTTP错误。在第三章,我们曾经为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('/moderator')
@login_required
@permission_required(Permission.MODERATE_COMMENTS)
def for_moderators_only():
    return "For comment moderators!"

在模板中也可能需要检查权限,所以Permission类以及所有的二进制常量也应该让模板可以访问。为了避免在每次调用render_template()函数时都不得不加上参数传递,我们使用了上下文处理器(context processor)。上下文处理器保证了变量在全部模板中都可用。变化如例子9-7所示:

Example 9-7. app/main/__init__.py: Adding the Permission class to the template context
from flask import Blueprint
from app.models import Permission

main = Blueprint('main', __name__)

from . import views, errors

@main.app_context_processor
def inject_permissions():
    return dict(Permission=Permission)

注意,原文此处代码没有导入Permision,如果你在此处运行代码就会出现找不到Permission定义的错误。所以添加了第二行导入Permession

新角色和权限可以在单元测试中测试一下。例子9-8显示了这两个简单的测试,同时也可以作为用法的演示。

Example 9-8. tests/test_user_model.py: Unit tests for roles and permissions
class UserModelTestCase(unittest.TestCase):
    # ...
    def test_roles_and_permissions(self):
        Role.insert_roles()
        u = User(email='john@example.com', password='cat')
        self.assertTrue(u.can(Permission.WRITE_ARTICLES))
        self.assertFalse(u.can(Permission.MODERATE_COMMENTS))
    def test_anonymous_user(self):
        u = AnonymousUser()
        self.assertFalse(u.can(Permission.FOLLOW))

在你转到下一章之前,你最好是重新创建或者升级一下开发数据库,保证所有的帐户都指派了角色(在role和permission创建前创建的帐户)。
  现在用户系统已经完成了,下一章我们将使用它来创建用户属性页面。
<<第八章 用户验证 第十章 用户资料>>

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容