有个常见需求是这样的:一个Django应用,在开发时,URL是以/为根目录的;而部署时,需要给它一个前缀,比如叫/prefix/。 它的使用场景是,在一个域名里托管多个应用,它们仅以前缀区分。

这个需求可以拆解成两个具体的要求:

  1. 在外面访问时是有/prefix/前缀的,而在应用那一层,不知道有这个前缀,仍然以为是/
  2. 在内部进行相对URL的生成时,虽然不知道前缀,但是要有前缀。 比如,从外面的首页/prefix/,希望点击一个链接后跳转到/prefix/home/; 而里面的应用在不知道前缀的情况下,生成的链接是/prefix/home/,而非/home/

WSGI协议的相关知识

What is WSGI

WSGI is the Web Server Gateway Interface. It is a specification that describes how a web server communicates with web applications, and how web applications can be chained together to process one request.

WSGI协议通过SCRIPT_NAMEPATH_INFO,来实现加前缀的操作。

from urllib import quote
url = environ['wsgi.url_scheme']+'://'

if environ.get('HTTP_HOST'):
    url += environ['HTTP_HOST']
else:
    url += environ['SERVER_NAME']

    if environ['wsgi.url_scheme'] == 'https':
        if environ['SERVER_PORT'] != '443':
           url += ':' + environ['SERVER_PORT']
    else:
        if environ['SERVER_PORT'] != '80':
           url += ':' + environ['SERVER_PORT']

url += quote(environ.get('SCRIPT_NAME', ''))
url += quote(environ.get('PATH_INFO', ''))
if environ.get('QUERY_STRING'):
    url += '?' + environ['QUERY_STRING']

从上述代码,源于WSGI的v1.01版本的URL Reconstruction描述,是推荐的URL构建算法。

一个URL的通用形式如下:

scheme:[//[user[:password]@]host[:port]][/path][?query][#fragment]

可以看出,SCRIPT_NAMEPATH_INFO,共同构成了[/path]这部分。 一般情况下,SCRIPT_NAME相当于为空,/path就等价于PATH_INFO。 而如果SCRIPT_NAME=/prefix,就实现了孤前面的第1个要求; 如果PATH_INFO等于去掉前缀后的部分,就实现了第2个要求。

WSGI协议参考

HTTP服务器方案

现在,主要的两个HTTP服务器------Apache httpd(通常简称Apache)和Nginx,都支持WSGI协议。

Apache httpd

Apache httpd通过模块mod_wsgiWSGI协议进行支持。 它对这个功能的实现与配置,简单而强大。

WSGIScriptAlias /prefix /PATH/TO/DJANGO/wsgi.py

但由于孤目前对Apache httpd并不太熟,也不实际使用,所以就说到这。

Nginx

旧方案

location ~ ^/prefix/ {
        ...
        uwsgi_param SCRIPT_NAME /prefix;
        uwsgi_modifier1 30;
    }

这是网上流传最多的设置方式。 然而,由于Nginx不支持修改PATH_INFO,所以需要uwsgi_modifier1 30这种丑陋的设置。 否则,内部被访问的应用,实际得到的访问链接是/prefix/prefix/...这种形式的。

uwsgi_modifier1 30的机制,就是把向内传的PATH_INFO,先删除开头部分的SCRIPT_NAME字符串。

Standard WSGI request followed by the HTTP request body. The PATH_INFO is automatically modified, removing the SCRIPT_NAME from it.

虽然还能工作,但是已经不推荐了。

Note: ancient uWSGI versions used to support the so called "uwsgi_modifier1 30" approach. Do not do it. It is a really ugly hack.

新方案

参考《uWSGI 2.0.11》的更新日志,可以在uWSGI的配置里,使用route-run = fixpathinfo:来替代uwsgi_modifier1

配置部分,不再需要uwsgi_modifier1

location ~ ^/prefix/ {
        ...
        uwsgi_param SCRIPT_NAME /prefix;
    }

而在uWSGI的ini配置文件中,新增一行:

[uwsgi]
...
route-run = fixpathinfo:

uWSGI方案

其实,除了在HTTP服务器以外,在WSGI应用服务器这一层,也是可以实现这个需求的。

[uwsgi]
...
mount = /prefix=/PATH/TO/DJANGO/wsgi.py
manage-script-name = true

这个wsgi.py的路径,可以是相对路径或绝对路径。 这相当于把应用以wsgi.py为入口,挂载到/prefix这个位置,并且自动处理SCRIPT_NAMEPATH_INFO

野路子

再记录两个孤用过的野路子。 它们能工作,只是有点怪怪的。

Nginx与Django直接配合

首先,用Nginxrewrite,实现向内传递时去除SCRIPT_NAME

location ~ ^/prefix/ {
        rewrite /prefix/(.*) /$1 break;
        proxy_pass http://127.0.0.1:8000;
        ...
    }

当然,这里是直接使用的HTTP反向代理。 同时,uWSGI也需要用--http的方式,直接提供HTTP服务。 这样就实现了要求1。

然后,修改Djangosettings.py,添加一行配置。

FORCE_SCRIPT_NAME = '/prefix'

这个配置,支持在Django这一层,覆盖WSGI协议中的SCRIPT_NAME。 (参考《stackoverflow.com/questions/10806836》。) 这样就实现了要求2。

这个路子,在uWSGI直接提供HTTP服务时,非常有效。 它相当于绕过了WSGI这一层,由HTTP服务器与应用合作实现。

不过缺点还是相当显著的。 两个修改相互依赖,FORCE_SCRIPT_NAME影响调试。 这是一种比uwsgi_modifier1还要丑陋的Hack。

直接在Django添加prefix

这个需求之所以如此麻烦,是因为外面需要额外加一个前缀。 如果在开发时就直接把前缀加上,不就没有这么多事了?

幸运的是,在Django中,可以非常简单地添加一个前缀。

urlpatterns = [
    url(...),
    ...
]

urlpatterns = [
    url('^prefix/', include(urlpatterns))
]

无论原先的urlpatterns是什么,直接再用include包一层,三行代码就能简单解决问题。

这个方案,可能是大多数开发者遇到这个需求时,所采用的方案。 它把一个运维部署的问题,转换成了一个开发问题。 在开发者自己做部署,而知识储备又不足时,就容易使用这个方案。 它的缺点......认真说来,也没什么不可忍受的。

也许只是不够优雅。

总结

本文介绍了几种实现方案。 Apache httpd的方案看上去最简洁,可惜孤不会用它。 Nginx的新方案是最好的方案,uWSGI方案也很不错,都是可选项。

至于Nginx的旧方案,以及两个野路子,还是算了吧。

优雅......优雅......

最后修改:2020 年 10 月 11 日
如果觉得我的文章对你有用,请随意赞赏