2

我们使用 Flask 将用户路由到 Bokeh 服务器。系统在 Docker 映像中运行。一切正常。但是现在我们想要添加身份验证,这很困难,因为我们不想将散景服务器端口映射到客户端。

让我向您展示它当前的工作方式(无需身份验证):

烧瓶 app.py(路由):

...
@app.route('/folder/report_x')
def page_folder_report_x():
    ''' embedded bokeh server for report_x '''
    script = server_document('http://localhost:5001/report_x')
    resp = {
        'title': 'Report X',
        'script': script,
        'template': 'Flask', }
    return render_template('embed.html', **resp)
...
app.run(host='0.0.0.0', port=5000, use_reloader=False)

Flask embed.py(模板):

...
{% extends "base.html" %}
{% block content %}
  {{ script|safe }}
{% endblock %}
...

Bokeh 服务器是从命令行使用 python 的 Panel 启动的(localhost:5000 代表 Flask 服务器):

panel serve report_x --port 5001 --allow-websocket-origin localhost:5000

Bokeh 服务器使用 main.ipynb 文件提供服务:

import panel as pn
from bokeh.models import ColumnDataSource, CustomJS
from bokeh.models.widgets import Button, DataTable, PreText
from bokeh.models.widgets import TableColumn, NumberFormatter, DateFormatter
...
gspec = pn.GridSpec(sizing_mode='stretch_both')
gspec[0:12, 0:12] = pn.WidgetBox(widgets)
...
gspec.servable()

我们的 Docker 镜像暴露了烧瓶服务器和散景服务器的端口:

...
RUN pip install -r /app/requirements.txt
EXPOSE 5000:5000
EXPOSE 5001:5001
...

最后,当我们运行 docker 容器时,我们映射端口:

# success!
docker run -p 5000:5000 -p 5001:5001 report_server:0.1

如果我们以这种方式运行 docker 镜像,一切都会完美运行。

但是如果我们在没有映射散景服务器的情况下运行它,我们将无法访问散景服务器(即使它在内部公开,正如您在 DockerFile 中看到的那样):

# fail
docker run -p 5000:5000 report_server:0.1

出于安全考虑,我们只想将一个端口映射到外部世界。关于如何在 Flask 中嵌入 Bokeh 服务器,是否只允许 Flask 与 Bokeh 服务器通信,我们是否遗漏了什么?

4

1 回答 1

2

关于如何在 Flask 中嵌入 Bokeh 服务器,是否只允许 Flask 与 Bokeh 服务器通信,我们是否遗漏了什么?

客户端(浏览器)必须能够与 Bokeh 服务器对话,句号。Bokeh 服务器的所有功能都通过 Bokeh 服务器和浏览器之间的直接 websocket 连接进行。所以你的问题的简短回答是“你不能”。

但是,您可以将 Bokeh 服务器配置为:

  • 不会在每个连接上自动创建新会话
  • 仅尊重具有加密签名会话 ID 的会话
  • 只接受来自白名单来源的连接

为此,您需要首先使用以下bokeh secret命令创建一个用于签署会话 ID 的秘密,例如

export BOKEH_SECRET_KEY=`bokeh secret` 

然后还要设置BOKEH_SIGN_SESSIONS和设置允许的 websocket 源:

BOKEH_SIGN_SESSIONS=yes bokeh serve --allow-websocket-origin=<app origin> app.py

然后在您的烧瓶应用程序中,您明确提供(签名的)会话 ID:

from bokeh.util.session_id import generate_session_id

script = server_session(url='http://localhost:5006/bkapp', 
                        session_id=generate_session_id())
return render_template("embed.html", script=script, template="Flask")

请注意,BOKEH_SECRET_KEY需要为 Bokeh 服务器和 Flask 进程设置(并且相同)环境变量。

现在,如果有人直接连接到 Bokeh 服务器,他们将返回 403 错误,除非连接 URL 包含签名会话 id,使用与 Bokeh 服务器启动时相同的密钥签名。大概只有你的 Flask 应用知道这个秘密,所以只有它才能成功启动新会话。

这足以完全保护事物吗?从技术上讲,任何可以访问发送到浏览器的连接字符串的人(例如查看应用程序的用户,或复杂的中间人攻击者,特别是如果您不在应用程序前终止 HTTPS)都可以提取签名的会话 ID。但是,只要您设置了允许的 websocket 源,那么此信息就不能用于从您的应用程序之外的任何地方启动新连接。如果有人尝试,服务器将返回 403:

错误:bokeh.server.views.ws:拒绝来自 Origin ' http://localhost:5006 ' 的 websocket 连接;使用 --allow-websocket-origin=localhost:5006 或设置 BOKEH_ALLOW_WS_ORIGIN=localhost:5006 以允许这样做;目前我们允许来源 {'localhost:8000'}

我不认为你可以从真正的浏览器伪造一个 Origin 标头,尽管也许有人可以从源代码构建一个修改过的 Chrome(这并不容易,但并非不可能)来欺骗一个。如果您需要防止这种情况发生,Bokeh Project Discourse可能是继续讨论的更好场所,因为它有点开放,并且可能指向新功能的开发(例如,指定会话连接限制的能力,或者会话 ID 永远不可重复使用)。

作为参考,这里有一个完整的示例,它还将 Bokeh 服务器直接嵌入到 Flask 进程中(如果您需要横向扩展或期望同时有多个用户,这将是过于幼稚的部署):

https://gist.github.com/bryevdv/481fc64c59620acb74c64bff0f4d47d0

作为最后的评论,您可能还可以(另外)将散景服务器 URL 放在某种身份验证代理后面,以防止 WS 升级首先发生,而无需身份验证。我不确定那会是什么样子。这也将在 Discourse 上得到更好的讨论 

于 2019-07-09T06:10:59.253 回答