第九章 用户角色(二)

一. 赋予角色

用户在应用中注册账户时,应该赋予其适当的角色。多数用户在注册时赋予的角色是“普通用户”,因为这是默认角色。唯一例外的是管理员,在注册应用时就应该被赋予管理员。当前应用中,管理员由保存在环境变量APP_ADMIN中的电子邮件地址识别,只要改电子邮件地址出现在注册请求中,就会被赋予admin。

app/models.py:定义默认的用户角色

class User(UserMixin, db.Model):
    __tablename__ = "users"
    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(64), unique=True, index=True)
    username = db.Column(db.String(64), unique=True, index=True)
    role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
    password_hash = db.Column(db.String(128))
    confirmed = db.Column(db.Boolean, default=False)

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        if self.role is None:
            if self.email == current_app.config['APP_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类的构造函数首先调用基类的构造函数。如果创建基类对象后还没有定义角色,则根据电子邮件将其设为管理员或普通用户。

二. 检验角色

为了简化角色和权限的实现过程,可在User模型中添加一个方法,检查赋予用户的角色是否有某项权限。

app/models.py:检查用户是否有指定的权限

from flask_login import UserMixin, AnonymousUserMixin
from . import login_manager
...

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, perm):
        return False

    def is_administrator(self):
        return False


login_manager.anonymous_user = AnonymousUser

如果用户角色中包含某项权限,那么User模型中添加的can()方法会返回True,表示允许用户执行此项操作。因为经常需要检查是否具有管理员权限,所以还单独实现了is_administrator()方法。

我们还实现了自定义的匿名用户类AnonymousUser,继承自Flask-Login的AnonymousUserMixin类,并实现了can和is_administrator()方法。这样无需检查用户是否登录系统,就可放心调用can()和is_administrator()。注意:我们必须通过login_manager.anonymous_user属性告诉Flask-Login使用应用自定义的匿名用户类。

在单元测试中测试以上新增方法:

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.COMMENT))
        self.assertTrue(u.can(Permission.FOLLOW))
        self.assertTrue(u.can(Permission.WRITE))
        self.assertFalse(u.can(Permission.ADMIN))
        self.assertFalse(u.can(Permission.MODERATE))

    def test_moderator_role(self):
        r = Role.query.filter_by(name="Moderator").first()
        u = User(email="[email protected]", password="cat", role=r)
        self.assertTrue(u.can(Permission.MODERATE))
        self.assertTrue(u.can(Permission.WRITE))
        self.assertTrue(u.can(Permission.FOLLOW))
        self.assertTrue(u.can(Permission.COMMENT))
        self.assertFalse(u.can(Permission.ADMIN))

    def test_administrator_role(self):
        r = Role.query.filter_by(name="Administrator").first()
        u = User(email="[email protected]", password="cat", role=r)
        self.assertTrue(u.can(Permission.ADMIN))
        self.assertTrue(u.can(Permission.COMMENT))
        self.assertTrue(u.can(Permission.FOLLOW))
        self.assertTrue(u.can(Permission.WRITE))
        self.assertTrue(u.can(Permission.MODERATE))

三. 检查用户权限的自定义装饰器

如果想让视图函数只对具有特定权限的用户开放,可以使用自定义的装饰器。下面我们将定义2个装饰器,一个用户检查常规权限,另一个专门用于检查管理员权限。(关于装饰器知识点的学习,可以查看:函数式编程基础(五)此篇博文)

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)

用法示例:

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


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

注意:

  1. 如果用户不具备指定权限,则返回403响应,即HTTP“禁止”;同样我们需要自定义403错误处理界面;
  2. 在视图函数上调用多个装饰器时,应该把route装饰器放在首位,余下的装饰器按照调用视图函数的先后顺序排序;
  3. 以上示例中,要先检查用户的身份验证状态,如果发现用户未通过身份验证,要将其重定向到登录界面。

app/templates/403.html:禁止访问错误界面

{% extends "base.html" %}

{% block title %}Flasky - Forbidden{% endblock %}

{% block page_content %}
    
{% endblock %}

app/main/error.py:新增403错误处理路由

@main.app_errorhandler(403)
def internal_server_error(e):
    return render_template('403.html'), 403

此外,在模板中可能也需要检查权限,所以Permission类的所有常量要在模板中能访问。为了避免每次调用render_template()时都多添加一个模板参数,可以使用上下文管理器。在渲染时,上下文管理器能让变量在所有模板中可访问。

app/main/__init__.py:把Permission类加入模板上下文

"""main模块的蓝本:在蓝本中定义路由和错误处理程序"""
from flask import Blueprint

# 参数:蓝本的名称和蓝本所在的包或模块
main = Blueprint('main', __name__)

# 把路由和错误处理程序与蓝本关联起来
from . import views, error
from ..models import Permission


# 把Permission类加入模板上下文,以便访问类中定义的常量
@main.app_context_processor
def inject_permissions():
    return dict(Permission=Permission)

以管理员身份登录系统后,分别访问如下URL测试:

第九章 用户角色(二)_第1张图片  第九章 用户角色(二)_第2张图片

你可能感兴趣的:(Flask)