8

给Django应用的所有URL添加前缀(SCRIPT_NAME)的六种方案

 3 years ago
source link: https://note.qidong.name/2017/11/uwsgi-script-name/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

给Django应用的所有URL添加前缀(SCRIPT_NAME)的六种方案

2017-11-03 19:03:10 +08  字数:2202  标签: uWSGI Nginx Django

需求

有个常见需求是这样的:一个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的旧方案,以及两个野路子,还是算了吧。

优雅……优雅……


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK