使用Flask代理到另一个Web服务

81

我希望将发送到我的Flask应用程序的请求代理到另一台在本地运行的Web服务。 我宁愿使用Flask来实现这个功能,而不是我们的高级nginx实例,这样我们就可以重用我们应用程序中内置的现有身份验证系统。 这样我们就能更好地保持“单点登录”的效果。

是否有现成的模块或其他代码可用于此? 尝试通过类似httplib或urllib的方式连接Flask应用程序已经变得非常棘手。


此问题也与为旧浏览器(如不支持跨域安全的IE7)提供AJAX服务相关。 - Mikko Ohtamaa
你在使用httplib时遇到了什么具体的问题? - jd.
@jd:鉴于Flask位于WSGI应用程序端,我不确定是否能够有效地转发所有数据。例如,Flask请求对象似乎没有包括我想要传递到httplib的原始请求(甚至是请求头)。虽然这并非不可能,但很麻烦,我希望有一个现有的模块可以完成这个任务。 - Joe Shaw
3个回答

138
我花了很多时间在同样的事情上工作,最终找到了一个使用 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://dev59.com/rWw15IYBdhLWcg3wYKmo#36601467
        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 section 13.5.1定义的所有“逐跳头部”。

2
@Evan 不错的解决方案。但是它无法处理3xx重定向,因为重定向URL可能指向代理主机。 - Ire
2
有人可以添加如何在MWE应用程序中调用它吗? - user1717828
5
非常好,非常感谢!(这就是人们需要让ngrok在前端和后端都可以工作的方法) 但是,对于我来说,request.host_url包括http://和一个尾随斜杠,因此对于我来说,替换行如下:request.url.replace(request.host_url, 'http://new-domain.com/') - jbasko
2
@Ire 我遇到了这个问题,并添加了一个编辑来解决。我所做的就是用 headers = [(name, value) if (name.lower() != 'location') else (name, value.replace('http://new-domain.com/', request.host_url)) for (name, value) in resp.raw.headers.items() if name.lower() not in excluded_headers] 替换了头部过滤器行。这只是修复了 Location 头中的 URL。(感谢 @jbasko 指出了尾随斜杠的问题) - Eric Reed
6
你也可以通过流的形式获取响应内容,而不必在服务器上完全读取它。为此,请将上面的 resp.content 替换为 resp.iter_content(chunk_size=10*1024),并在 Response 构造函数中添加 content_type=r.headers['Content-Type'] 参数。 - Tim
显示剩余12条评论

14

我在一个基于Werkzeug的应用程序中使用httplib实现了一个代理(就像你的情况一样,我需要使用Web应用程序的身份验证和授权)。

尽管Flask文档没有说明如何访问HTTP头,但是你可以使用request.headers(参见Werkzeug文档)。如果你不需要修改响应,并且被代理应用程序使用的标头是可预测的,那么代理就很容易。

请注意,如果您不需要修改响应,则应使用werkzeug.wsgi.wrap_file来包装httplib的响应流。这允许传递打开的操作系统级文件描述符以获得最佳性能。


谢谢,我今天下午简单地写了一些代码。然而,由于httplib无法很好地处理cookie,我遇到了各种问题。不幸的是,我认为我需要修改响应来进行一些简单的URL重写(例如,将<src img="/static/foo.jpg">更改为<src img="/myserver/proxy/static/foo.jpg">)。 - Joe Shaw
在我的情况下,只有一个cookie需要捕获,所以使用正则表达式来解析它非常简单,比设置Python的cookie库要容易得多。 - jd.
1
你能在回答正文中提供你的实现链接或代码本身吗? - Carlos Pinzón
这里有一个相关的stackoverflow答案,附带有实际的代码实现。 https://stackoverflow.com/a/50231825/399573 - gt6989b

9
我的原始计划是将公开的URL设置为类似于http://www.example.com/admin/myapp,代理到http://myapp.internal.example.com/。但这条路会走向疯狂。
大多数Web应用程序,特别是自托管的应用程序,都假定它们将在HTTP服务器的根目录下运行,并执行诸如绝对路径引用其他文件之类的操作。为了解决这个问题,您必须在各个位置重写URL:位置标头和HTML、JavaScript和CSS文件。
编写了一个Flask代理蓝图,虽然它足以代理我真正想要代理的一个Web应用程序,但它并不可持续。它是一堆正则表达式的混乱。
最终,我在nginx中设置了一个新的虚拟主机,并使用其自己的代理。由于两者都在主机的根目录下,因此大多数情况下不需要重写URL。(只有极少量需要,nginx的代理模块处理。)被代理的Web应用程序执行自己的身份验证,现在已经足够好了。

3
可以提供一些关于“我设置了一个新的虚拟主机”的图示,这将会很有帮助。 - PascalVKooten
1
绝对是你最后一段。Flask的优势不在于代理,因此尽可能避免将其用作代理更好。我能想到的唯一有效的原因是,某些应用程序逻辑(如身份验证或授权)是必要的,而其他应用程序不支持。 - jpmc26

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