我们的web程序用户有不同的角色——普通用户, 管理评论的人, 权限最高的管理员。 不同的权限可以浏览不同的页面, 权限越大, 可以浏览的页面就越多。
本节我们为试图函数添加权限限制, 使其只可以被管理员或评论管理员访问。
————————————————————————————————————
一. 修改|-app/-main/views.py
@main.route('/admin')
@login_required
@admin_required #只有管理员能访问该视图函数
def for_admin_only():
return "For administrator!"
@main.route('/moderator')
@login_required
@permission_required(Permission.MODERATE_COMMIT) #只有评论员能访问该视图函数
def for_moderator_only():
return "For moderator!"
上图的两个修饰器是很简单明了的, 如果flask为我们提供了这样的修饰器, 就会很方便, 不过现实是flask并没有为我们封装这个功能, 需要我们自己来实现;
二.增加|-app/decorators.py
这就需要python闭包的知识, 网上有很多教程我就不多说了, 但是只用理解闭包才会理解我下面说的,可以谈一谈写出这个闭包函数的思路:
- 首先我们看第二个视图函数, 修饰器的作用就相当于下面这行代码:
- for_moderator_only = permission_required(Permission.MODERATE_COMMIT)(for_moderator_only)
- 所以for_moderator_only() = permission_required(Permission.MODERATE_COMMIT)(for_moderator_only)()
- 先看permission_required(Permission.MODERATE_COMMIT), 可知permission_required是最外部的函数, 参数应该是表示权限的一个变量permission, 应该返回一个内部函数——decrator(名字随便起)
- 然后第三点的等号右边的代码就变成了decorator(for_moderator_only)()
- 再看decorator(for_moderator_only), 可知内部函数decorator的参数是被修饰的函数f,然后这个decorator函数返回一个内部函数——decrated_function
- 第五点的代码就变成decrated_function()
- 最后调用这个内部函数, 根据两个外部函数提供的permissions参数和func参数,判断是否有相对应的权限, 决定返回403页面或者执行视图函数:
def permission_required(permission):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.can(permission): #该函数由我们自己实现, 如果用户有该权限, 返回True, 见标题三
abort(403)
return f(*args, **kwargs)
return decorated_function
return decorator
再来看第一个视图函数, 修饰器@admin_required, 此时我们已经可以有验证权限的函数了, 所以这个修饰器只需要借助上面的函数来实现即可:
- for_moderator_only = admin_required(for_moderator_only)
- for_moderator_only() = admin_required(for_moderator_only)()
- 上面我们分析过程中有一句:permission_required(Permission.MODERATE_COMMIT)(for_moderator_only)()
- 只要把Permission.MODERATE_COMMIT, 改成Permission.ADMINISTER不就好了吗,
- permission_required(Permission.ADMINISTER)(for_moderator_only)()
所以admin_required代码如下:
def admin_required(f):
return permission_required(Permission.ADMINISTER)(f)
三. 修改|-app/models.py
class User(UserMixin, db.Model):
def can(self, permissions): #判断用户是否有该权限
return self.role is not None and (self.role.permissions & permissions) == permissions
def is_admin(self): #判断用户是否是管理员
return self.can(Permission.ADMINISTER)
class AnonymousUser(AnonymousUserMixin): 匿名用户无任何权限
def can(self, permissions):
return False
def is_admin(self):
return False
login_manager.anonymous_user = AnonymousUser
- AnonymousUserMixin是匿名用户混合类, 本来login_manager.anonymous_user=AnonymousUserMixin,
- 当用户未登录时, current_user是一个匿名用户, 应该被赋值为AnonymousUserMixin类的实例, 这个类的is_authenticated()和is_active()方法都返回False;
- 此处我们自己实现了匿名用户类, 并把login_manager.anonymous_user赋值为AnonymousUser,
- 这样用户未登录时current_user就被赋值为AnonymousUser的实例, 这样用户不登录也可以调用can和is_admin
举例说明:
完成以上三步工作后, 如果我们访问/moderator url, 因为@login_required, 程序会要求我们先登录, 登录以后, 由于auth.login视图函数的重定向, return redirect(requests.args.get('next')), 程序会重新发送/moderator的请求, 然后@permission_required的作用, 我们会逐级访问闭包函数,判断是该返回403还是执行视图函数。
为什么要使用闭包?
- 为什么要使用闭包, 因为如果把判断条件放在视图函数里面显然是不合适的, 我们目的就是阻止无权限用户访问视图函数;
- 使用闭包可以减少代码的重复量, 否则我们每个视图函数都要添加同样你的代码。
四. 把Permission添加到模板的上下文, |-app/-main/__init__.py
from flask import Blueprint
from ..models import Permission
main = Blueprint('main', __name__) #定义蓝本
@main.app_context_processor #添加上下文, 这样在模板中就可以使用Permission类了
def inject_permissions():
return dict(Permission=Permission)
from . import views, errors
五. 过程演示
我们用用户leo来说明访问管理页面/admin的过程
一开始leo用户没有角色(role_id为NULL):
我们在浏览器访问管理页面, 因为修饰器@login_required的原因, 重定向到登录页面:
输入leo的email和密码后, 点击提交按钮, 用户登录后, auth.login函数会重定向到main.for_admin_only, return redirect(url_for(request.args.get('next')))(ps: 重定向到上次未授权的页面url), 然后调用视图函数, for_admin_only(), 根据我们上面的分析, 此时用户没有角色, 返回403页面:
我们为leo添加管理员角色:
添加成功:
然后启动服务器, 再次尝试访问/admin页面:
这次因为leo有角色, 且权限为Permission.ADIMNISTER, 所以执行视图函数返回页面。