使用Flask代理到其他网络服务
我想把发给我的Flask应用的请求转发到本地机器上运行的另一个网络服务。我更想用Flask来做这个,而不是用我们更高级的nginx,因为这样可以重用我们应用里已经建立的认证系统。我们越能保持这种“单点登录”的方式就越好。
有没有现成的模块或者其他代码可以做到这一点?尝试通过Flask连接到像httplib或urllib这样的东西真是让人头疼。
3 个回答
我最开始的计划是让公开的网址看起来像 http://www.example.com/admin/myapp
,然后把它转发到 http://myapp.internal.example.com/
。但这样做会让人很头疼。
大多数网络应用,特别是那些自己托管的,通常假设它们会在HTTP服务器的根目录下运行,所以它们会用绝对路径来引用其他文件。为了绕过这个问题,你需要到处修改网址,包括位置头和HTML、JavaScript、CSS文件。
我确实 写了一个Flask的代理蓝图 来解决这个问题,虽然它对我想要代理的那个网络应用来说效果还不错,但长期使用起来并不方便。代码里全是复杂的正则表达式,搞得很乱。
最后,我在nginx里设置了一个新的虚拟主机,使用它自己的代理功能。因为这两个都在主机的根目录下,所以大部分情况下不需要修改网址。(而那些少量需要修改的,nginx的代理模块也能处理。)被代理的网络应用自己有认证功能,这对我来说现在已经足够了。
我花了很多时间在这个问题上,最后找到了一种用requests库的解决方案,效果不错。这个方法甚至可以在一次响应中处理多个cookie,这个功能我研究了一下才搞明白。下面是flask的视图函数:
from dotenv import load_dotenv # pip package python-dotenv
import os
#
from flask import request, Response
import requests # pip package requests
load_dotenv()
API_HOST = os.environ.get('API_HOST'); assert API_HOST, 'Envvar API_HOST is required'
@api.route('/', defaults={'path': ''}, methods=["GET", "POST"]) # ref. https://medium.com/@zwork101/making-a-flask-proxy-server-online-in-10-lines-of-code-44b8721bca6
@api.route('/<path>', methods=["GET", "POST"]) # NOTE: better to specify which methods to be accepted. Otherwise, only GET will be accepted. Ref: https://flask.palletsprojects.com/en/3.0.x/quickstart/#http-methods
def redirect_to_API_HOST(path): #NOTE var :path will be unused as all path we need will be read from :request ie from flask import request
res = requests.request( # ref. https://stackoverflow.com/a/36601467/248616
method = request.method,
url = request.url.replace(request.host_url, f'{API_HOST}/'),
headers = {k:v for k,v in request.headers if k.lower() != 'host'}, # exclude 'host' header
data = request.get_data(),
cookies = request.cookies,
allow_redirects = False,
)
#region exlcude some keys in :res response
excluded_headers = ['content-encoding', 'content-length', 'transfer-encoding', 'connection'] #NOTE we here exclude all "hop-by-hop headers" defined by RFC 2616 section 13.5.1 ref. https://www.rfc-editor.org/rfc/rfc2616#section-13.5.1
headers = [
(k,v) for k,v in res.raw.headers.items()
if k.lower() not in excluded_headers
]
#endregion exlcude some keys in :res response
response = Response(res.content, res.status_code, headers)
return response
更新于2021年4月:excluded_headers
可能应该包含所有由RFC 2616第13.5.1节定义的“逐跳头部”。
我在一个基于Werkzeug的应用中实现了一个代理,使用的是httplib(就像你那样,我需要用到这个网页应用的认证和授权功能)。
虽然Flask的文档没有说明如何访问HTTP头信息,但你可以使用request.headers
(可以查看Werkzeug的文档)。如果你不需要修改响应,而且被代理的应用使用的头信息是可以预见的,那么代理的过程就很简单。
需要注意的是,如果你不打算修改响应,应该使用werkzeug.wsgi.wrap_file
来包装httplib的响应流。这样可以将打开的操作系统级文件描述符传递给HTTP服务器,从而获得更好的性能。