如何通过Bokeh Server配置Tornado解决OpenShift部署的CORS跨域问题
问题描述
- 部署环境:OpenShift/K8s
- 应用架构:Flask服务运行在
0.0.0.0:8080,将请求转发至0.0.0.0:5006的Bokeh Server - 现象:后台运行正常,但浏览器无法显示Bokeh组件,控制台抛出CORS相关错误
- 具体报错:
Access to XMLHttpRequest at 'http://0.0.0.0:5006/bkapp/autoload.js?bokeh-autoload-element=de42b2ca-6eb9-40fc-97d8-6b5b52f88d33&bokeh-app-path=/bkapp&bokeh-absolute-url=http://0.0.0.0:5006/bkapp' from origin '
' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
应用代码
from threading import Thread import panel as pn from flask import Flask, render_template from tornado.ioloop import IOLoop from bokeh.embed import server_document from bokeh.layouts import column from bokeh.models import ColumnDataSource, Slider from bokeh.plotting import figure from bokeh.sampledata.sea_surface_temperature import sea_surface_temperature from bokeh.server.server import Server from bokeh.themes import Theme from bokeh.io import show from bokeh.models import Button, CustomJS app = Flask(__name__) def bkapp(doc): doc.add_root(Button(label="Foo", button_type="success")) doc.theme = Theme(filename="theme.yaml") @app.route('/', methods=['GET', 'OPTIONS', 'HEAD']) def bkapp_page(): script = server_document('http://0.0.0.0:5006/bkapp') return render_template("embed.html", script=script, template="Flask") def bk_worker(): # Can't pass num_procs > 1 in this configuration. If you need to run multiple # processes, see e.g. flask_gunicorn_embed.py server = Server({'/bkapp': bkapp}, io_loop=IOLoop(), allow_websocket_origin=["*"]) server.start() server.io_loop.start() Thread(target=bk_worker).start() if __name__ == '__main__': print('Opening single process Flask app with embedded Bokeh application on http://0.0.0.0:8080/') print() print('Multiple connections may block the Bokeh app in this configuration!') print('See "flask_gunicorn_embed.py" for one way to run multi-process') app.run(host='0.0.0.0', port=8080)
嵌入页面模板(templates/embed.html)
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>Embedding a Bokeh Server With {{ framework }}</title> </head> <body> <div> This Bokeh app below served by a Bokeh server that has been embedded in another web app framework. For more information see the section <a target="_blank" href="https://docs.bokeh.org/en/latest/docs/user_guide/server.html#embedding-bokeh-server-as-a-library">Embedding Bokeh Server as a Library</a> in the User's Guide. </div> {{ script|safe }} </body> </html>
解决方案
方案一:直接修改Bokeh Server的Tornado配置添加CORS头
通过自定义Tornado的RequestHandler,为Bokeh Server的所有HTTP请求注入CORS响应头:
- 定义CORS处理基类
from tornado.web import RequestHandler class CORSHandler(RequestHandler): def prepare(self): # 允许所有来源,可替换为OpenShift应用的具体外部域名 self.set_header("Access-Control-Allow-Origin", "*") # 允许的请求方法 self.set_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS, HEAD") # 允许的请求头 self.set_header("Access-Control-Allow-Headers", "Content-Type, Authorization") # 预检请求缓存时长 self.set_header("Access-Control-Max-Age", 86400) # 处理OPTIONS预检请求,直接返回200 if self.request.method == "OPTIONS": self.finish()
- 修改Bokeh Server初始化逻辑
更新bk_worker函数,替换默认的请求处理器为带CORS的版本:
def bk_worker(): from bokeh.server.views import autoload_js, static_handler # 替换Bokeh核心路由的处理器 extra_patterns = [ (r"/bkapp/autoload.js", CORSHandler, dict(handler=autoload_js)), (r"/bkapp/static/(.*)", CORSHandler, dict(handler=static_handler)), ] server = Server( {'/bkapp': bkapp}, io_loop=IOLoop(), allow_websocket_origin=["*"], extra_patterns=extra_patterns ) # 给所有注册的Tornado处理器添加CORS头逻辑 for handler in server._tornado_app.handlers[0][1]: original_prepare = handler.prepare def new_prepare(self): CORSHandler.prepare(self) original_prepare(self) handler.prepare = new_prepare server.start() server.io_loop.start()
方案二:通过Flask反向代理避免跨域
浏览器直接请求Flask服务,由Flask转发请求到Bokeh Server,彻底规避跨域问题:
- 修改Flask路由中的server_document地址
@app.route('/', methods=['GET', 'OPTIONS', 'HEAD']) def bkapp_page(): # 使用Flask代理的路径,而非Bokeh的直接地址 script = server_document('/bkapp_proxy') return render_template("embed.html", script=script, template="Flask")
- 添加Flask代理路由
from flask import request import requests @app.route('/bkapp_proxy/<path:path>') def bkapp_proxy(path): # 容器内部访问Bokeh Server的地址 bokeh_url = f'http://localhost:5006/bkapp/{path}' resp = requests.request( method=request.method, url=bokeh_url, headers={k: v for k, v in request.headers.items() if k != 'Host'}, data=request.get_data(), cookies=request.cookies, allow_redirects=False ) # 过滤掉不需要转发的响应头 excluded_headers = ['content-encoding', 'content-length', 'transfer-encoding', 'connection'] headers = [(k, v) for k, v in resp.raw.headers.items() if k.lower() not in excluded_headers] return app.make_response((resp.content, resp.status_code, headers))
关键注意点
- 不要在
server_document中使用0.0.0.0:5006,这是容器内部地址,浏览器无法直接访问,需替换为外部可访问的域名或代理路径 allow_websocket_origin设置为["*"]仅用于测试,生产环境建议指定具体的域名
内容的提问来源于stack exchange,提问作者jdramirezo
相关产品推荐
相关产品推荐

