第八章 用户权限

有多种方法可用于在程序中实现角色。具体采用何种实现方法取决于所需角色的数量和细分程度。例如,简单的程序可能只需要两个角色,一个表示普通用户,一个表示管理员。对于这种情况,在 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.COMMENT |
                    Permission.WRITE_ARTICLES, False),
        u'协管员': (Permission.FOLLOW |
                    Permission.COMMENT |
                    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)
    db.session.commit()

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

推荐阅读更多精彩内容