Jinja2模板中如何隐藏无法访问的链接

8

我们正在工作中使用Flask + Jinja2编写Web应用程序。 该应用程序有注册用户,根据其角色可以访问特定页面。为了在服务器端实现这一点,我们只需为页面添加装饰器:

@app.route('/action1')
@security_requirements(roles=['some_role'])
def action1():
    ...

装饰器检查已登录用户的角色列表中是否有“some_role”,并决定是将调用传递给装饰函数还是只是重定向用户到“访问被拒绝”页面。
该应用程序还使用Bootstrap实现了导航栏。导航栏使用基础模板在每个页面中显示。目前为止,应用程序中的每个页面都在导航栏中有条目,无论当前用户是否可以访问它。尽管这不是安全漏洞,但我想隐藏用户无法访问的页面。此外,我希望在不在Jinja模板中重复允许角色列表的情况下实现此功能。是否可能通过使用我的当前装饰器在Jinja中以某种方式实现此功能?

1
security_requirements 是你的装饰器吗?它可以被更改吗? - twil
@twil - 是的,这是我的。 - reish
2个回答

8
我使用Flask-Security,它将许多登录/安全模块组合成一个美好的包。它带有来自Flask-Principal的角色管理,使您可以执行以下操作:
{% if current_user.has_role('admin') %}
    <li><a href="#">Manage Site</a></li>
{% endif %}

你可以在源代码中看到它是如何实现的, current_user代理来自于Flask-Login

我想使用一个单一的地方来存储每个操作允许的角色列表。如果我理解正确,即使使用Flask-Security,我也需要为每个操作编写两个列表 - 在服务器端代码中和模板内部。 - reish
所以你想做类似这样的事情:{% if current_user.can_access(url_for('protected_endpoint')) %} 仅限管理员 {% endif %} - Doobeh
3
理想情况是扩展Flask的url_map本身,并在其中实际保存权限信息,这样当您定义app.route时,就可以在那里指定角色,例如: @app.route('/', methods=['post','get'], roles=['admin'])。 这将更加便于使用,并且通过查看url_map可以轻松查找。这是一个有趣的问题,我有空闲时间会研究一下。 - Doobeh
为了可能更简单的解决方案(但不够美观),您可以创建一个将端点链接到角色的存储。然后,您可以在装饰器中不指定角色,而是根据数据存储查找它。您的jinja2模板过滤器也可以执行相同的操作(current_user.roles与数据存储所列出的所需角色)。 - Doobeh

2

我将 security_requirements 装饰器更改为以下内容:

def security_requirements(logged_in=True,
                          roles=None):
def wrapper(f):
    # Store the security attributes as a member of the function object
    f.access_control = dict(logged_in=logged_in, roles=roles)
    @functools.wraps(f)
    def wrapped(*args, **kwargs):
        access_result = _eval_access(logged_in, roles)
        # Redirect the user to the appropriate page (Access denied / Login Required / Actual Page) based on the result
        ...

这个修饰器与之前的版本唯一的真正区别在于将安全属性存储到函数对象中的那一行。从修饰器内部来看,这行代码是无用的。但是,现在我可以在Jinja模板中调用以下操作:

{% if can_access(func) %}
<li><a>...</a></li>
{% endif %}

can_access函数定义在Flask应用程序模块中。它接收一个字符串,必须将其转换为函数对象。它通过调用app.view_functions来实现:

def can_access(func):
    return auth.can_access(app.view_functions[func])

这个函数应该直接从Jinja模板中调用。因此,它需要添加到Jinja的全局变量中:

app.jinja_env.globals.update(can_access=can_access)

最后,auth.can_access
def can_access(f):
    if not hasattr(f, 'access_control'):
        return True

    # Use the access_control member set by the decorator
    return _eval_access(**f.access_control) == AccessResult.ALLOWED

这个解决方案意味着访问控制只在一个地方定义,即函数装饰器。

1
我不完全理解这个,但是很好! - 8oh8
你应该将此作为补丁提交,这将是一个很棒的功能。 - Plasma
我听说Python生态系统有适用于各种需求的pip库,但是我发现自己每天都要实现这样的基本功能。干得好,你真的应该接手Flask-Security或Flask-Principal并提交这个。 - Tamas Hegedus

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接