用户在应用中注册账户时,应该赋予其适当的角色。多数用户在注册时赋予的角色是“普通用户”,因为这是默认角色。唯一例外的是管理员,在注册应用时就应该被赋予管理员。当前应用中,管理员由保存在环境变量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!"
注意:
app/templates/403.html:禁止访问错误界面
{% extends "base.html" %}
{% block title %}Flasky - Forbidden{% endblock %}
{% block page_content %}
Forbidden
{% 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测试: