Django Tornado Nginx Auth¶
Si suppone che si abbia un progetto django nella directory /home/utente/projects/django/
e un progetto tornado nella directory /home/utente/projects/tornado/
. Si vuole configurare nginx in modo che django venga servito all'url http://example.com/
e tornado all'url http://example.com/tornado/
, facendo in modo che l'accesso a tornado sia inibito se non si è autenticati a django.
La guida è condotta su un sistema Debian Jessie, al momento in testing (per altre versioni probabilmente andrà adattato qualcosa).
Prerequisiti¶
Servono (oltre a django e tornado, ovviamente) anche i pacchetti: devscripts
, mercurial
, fakeroot
.
Compilazione di nginx¶
Esiste un plugin per nginx (scritto da uno dei core developer di nginx stesso) che nella versione 1.4.x del server (quella attualmente in debian) non è incluso. Quindi bisogna scaricare il plugin e ricompilare nginx. Per comodità si useranno il più possibile i sorgenti e il sistema di build di debian.
$ mkdir /home/utente/projects/nginx $ cd /home/utente/projects/nginx $ dget http://ftp.de.debian.org/debian/pool/main/n/nginx/nginx_1.4.4-1.dsc
Questo per scaricare il pacchetto sorgente di nginx 1.4.4 e averlo automaticamente scompattato.
$ cd nginx-1.4.4/debian/modules $ hg clone http://mdounin.ru/hg/ngx_http_auth_request_module nginx-auth-request
A questo punto nella sotto-directory debian/modules/nginx-auth-request
c'è il codice del plugin richiesto (preso direttamente dal repository mercurial dove viene sviluppato, dato che il repository su github è meno aggiornato).
# apt-get build-dep nginx
In questo modo verranno installati i pacchetti necessari a compilare nginx. Ora bisogna modificare il file debian/rules
nella sezione riguardante il pacchetto binario nginx-full
(solo in questa, nelle altre non è necessario), aggiungendo la riga:
--add-module=$(MODULESDIR)/nginx-auth-request \
sotto alle altre direttive --add-module
, in modo che il file si presenti così:
[...] config.status.full: config.env.full ### In questa sezione... cd $(BUILDDIR_full) && CFLAGS="$(CFLAGS)" CORE_LINK="$(LDFLAGS)" ./configure \ --prefix=/usr/share/nginx \ --conf-path=/etc/nginx/nginx.conf \ --error-log-path=/var/log/nginx/error.log \ --http-client-body-temp-path=/var/lib/nginx/body \ --http-fastcgi-temp-path=/var/lib/nginx/fastcgi \ --http-log-path=/var/log/nginx/access.log \ --http-proxy-temp-path=/var/lib/nginx/proxy \ --http-scgi-temp-path=/var/lib/nginx/scgi \ --http-uwsgi-temp-path=/var/lib/nginx/uwsgi \ --lock-path=/var/lock/nginx.lock \ --pid-path=/run/nginx.pid \ --with-pcre-jit \ --with-debug \ --with-http_addition_module \ --with-http_dav_module \ --with-http_geoip_module \ --with-http_gzip_static_module \ --with-http_image_filter_module \ --with-http_realip_module \ --with-http_stub_status_module \ --with-http_ssl_module \ --with-http_sub_module \ --with-http_xslt_module \ --with-ipv6 \ --with-mail \ --with-mail_ssl_module \ --add-module=$(MODULESDIR)/nginx-auth-pam \ --add-module=$(MODULESDIR)/nginx-dav-ext-module \ --add-module=$(MODULESDIR)/nginx-echo \ --add-module=$(MODULESDIR)/nginx-upstream-fair \ --add-module=$(MODULESDIR)/ngx_http_substitutions_filter_module \ --add-module=$(MODULESDIR)/nginx-auth-request \ ### ...aggiungere questa riga $(CONFIGURE_OPTS) >$@ touch $@ [...]
A questo punto bisogna compilare i pacchetti binari:
$ cd /home/utente/projects/nginx/nginx-1.4.4 $ fakeroot debian/rules binary
Alla fine della compilazione nella directory /home/utente/projects/nginx
saranno presenti i pacchetti binari:
nginx_1.4.4-1_all.deb nginx-common_1.4.4-1_all.deb nginx-doc_1.4.4-1_all.deb nginx-extras_1.4.4-1_amd64.deb nginx-extras-dbg_1.4.4-1_amd64.deb nginx-full_1.4.4-1_amd64.deb nginx-full-dbg_1.4.4-1_amd64.deb nginx-light_1.4.4-1_amd64.deb nginx-light-dbg_1.4.4-1_amd64.deb nginx-naxsi_1.4.4-1_amd64.deb nginx-naxsi-dbg_1.4.4-1_amd64.deb nginx-naxsi-ui_1.4.4-1_all.deb
Si può procedere ad installare i pacchetti necessari direttamente da questa directory:
# dpkg -i nginx_1.4.4-1_all.deb nginx-common_1.4.4-1_all.deb nginx-full_1.4.4-1_amd64.deb
Avvio delle applicazioni¶
Dato che la guida si concentra sulla gestione dell'autenticazione tra django e tornado, si assume per semplicità che l'applicazione tornado sia hello-world (link) e che le due applicazioni siano servite dai server di sviluppo:
$ cd /home/utente/projects/django $ ./manage.py runserver
$ cd /home/utente/projects/tornado $ ./hello-world.py
In questo modo il server di sviluppo di django risponde a 127.0.0.1:8000
e tornado a 127.0.0.1:8888
Creazione del file di configurazione di nginx¶
Si crei il file /etc/nginx/sites-available/django
:
server { listen 80; root /home/utente/projects/django; server_name django; access_log /home/utente/projects/django/access.log; error_log /home/utente/projects/django/error.log; location / { proxy_pass http://127.0.0.1:8000; } location /tornado { auth_request /is_logged_in/; proxy_pass http://127.0.0.1:8888; } }
e si attivi con:
# ln -s /etc/nginx/sites-available/django /etc/nginx/sites-enabled/django # /etc/init.d/nginx restart
In questo modo si dice a nginx che per URL sotto a /tornado
deve fare una request a /is_logged_in/
e accettare di passare il controllo a tornado solo se riceve una risposta HTTP 200.
Aggiungere la riga
127.0.0.1 django
al file /etc/hosts
.
Modifica delle applicazioni Django e Tornado¶
Dato che i cookie in passaggio da django a tornado possono essere modificati dall'utente (e quindi si rischia di permettere a un utente di impersonarne un altro all'interno di tornado), si può fare in modo che django usi i cookie firmati, e riutilizzare l'infrastruttura relativa anche da tornado.
Bisogna fare un link simbolico al file settings.py
di django all'interno della directory di tornado ed esportare una variabile d'ambiente prima di lanciare l'applicazione tornado stessa:
$ ln -s /home/utente/projects/django/my_project/settings.py /home/utente/projects/tornado/django_settings.py $ cd /home/utente/projects/tornado && export DJANGO_SETTINGS_MODULE="django_settings" && ./hello_world.py
In questo modo tornado può caricare i settings di django, ed accedere alle classi relative alla gestione dei cookie firmati (che necessitano di settings come SIGNING_BACKEND
). L'applicazione hello world va modificata così:
#!/usr/bin/env python
import tornado.ioloop
import tornado.web
class MainHandler(tornado.web.RequestHandler):
def get_django_signed_cookie(self, key):
from django.core import signing
signed_value = self.get_cookie(key)
try:
value = signing.get_cookie_signer(salt=key).unsign(signed_value, max_age=None)
except:
value = None
return value
def get_current_user(self):
user = self.get_django_signed_cookie('user')
return user
def get(self):
username = self.get_current_user()
self.write("Hello, %s!" % username) ### 'Hello, world!' diventa 'Hello, username!'
### Il setup del cookie viene fatto nella view di login di django (v. sotto)
application = tornado.web.Application([
# (r"/", MainHandler), ### Commentare/cancellare questa riga...
(r'/tornado', MainHandler), ### ...e aggiungere questa
])
if __name__ == "__main__":
application.listen(8888)
tornado.ioloop.IOLoop.instance().start()
in modo che MainHandler
risponda all'URL /tornado
anziché all'URL /
. Di fatto il metodo MainHandler.get_django_signed_cookie()
è paragonabile a HttpRequest.get_signed_cookie()
di django. Nell'applicazione django va aggiunto in urls.py
:
url(r'^is_logged_in/$', 'myproject.views.is_logged_in', name='is_logged_in'),
e una view di questo tipo:
from django import http
def is_logged_in(request):
if request.user.is_anonymous():
return http.HttpResponseForbidden("NO")
else:
return http.HttpResponse("OK")
che non fa altro che restituire un HTTP 403 in caso l'utente non sia loggato, e un HTTP 200 diversamente.
Bisogna poi settare un cookie con i dati che si vuole che passino da django a tornado nella view di login (e cancellarlo in quella di logout), ad esempio in questo modo:
from django import http
from django.contrib import messages
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.decorators import login_required
from django.core.urlresolvers import reverse
from django.shortcuts import render
from django.utils.translation import ugettext_lazy as _
def login_view(request):
username = password = ''
_next = request.GET.get("next", None)
if request.POST:
username = request.POST['username']
password = request.POST['password']
user = authenticate(username=username, password=password)
if user is not None:
if user.is_active:
login(request, user)
messages.info(request, _('Welcome, %s!' % username))
if _next:
response = http.HttpResponseRedirect(_next)
else:
response = http.HttpResponseRedirect(reverse('home'))
### Il cookie sarà firmato dall'infrastruttura di Django in modo che se viene modificato
### dall'utente, tornado se ne accorga in fase di authorization
response.set_signed_cookie(
key='user',
value=request.user.username,
expires=request.session.get_expiry_age(),
)
return response
else:
messages.error(request, _('Account %s is disabled' % username))
else:
messages.error(request,
_("The credentials you supplied were not correct or did not grant access to this resource."))
return render(request, 'login.html')
@login_required
def logout_view(request):
logout(request)
response = http.HttpResponseRedirect(reverse('home'))
response.delete_cookie('user')
return response
In questo esempio il cookie user
contiene lo username dell'utente appena loggato, ma si possono definire cookie per qualsiasi dato sia necessario passare tra django e tornado. Come si vede il valore di expires
del cookie è settato uguale all'expiry_age
della sessione attuale.
Se un template dell'applicazione django ha un link di questo tipo (o se si tenta di scrivere a mano l'URL):
<a href="/tornado" class="btn">Tornado</a>
nginx si comporterà così:
- Vede la richiesta a
http://django/tornado
- La direttiva
auth_request
passa il controllo all'URLhttp://django/is_logged_in/
- Django controlla se l'utente è loggato o meno, e dà una response di tipo 200 o 403
- Se la risposta è 200, allora nginx prosegue e passa il controllo a
http://127.0.0.1:8888
dove risponde tornado, che avrà accesso ai cookie eventualmente settati dalla view di login di django - Se la risposta è 403, nginx non prosegue e l'utente si vede impossibilitato a proseguire
Dubbi/problemi¶
- Compilare un plugin (seppur proveniente da una fonte autorevole, come un core dev di nginx) non mi piace troppo, specialmente se in un pacchetto così fondamentale come il server web/proxy
- Non ho ancora risolto il problema per cui l'utente non loggato vede un errore 403 anziché un più educato redirect alla view di login. Purtroppo il plugin non supporta i redirect, ma soltanto 403/200, quindi perché funzioni la direttiva
auth_request
non ho potuto usare il decoratorelogin_required
nella view django. - Il changelog di nginx dice che dalla versione 1.5.4 del 27 agosto 2013 il modulo
ngx_http_auth_request_module
è incluso, quindi ci si può aspettare che anche i futuri pacchetti debian lo includano.
Aggiornato da Mark Caglienzi quasi 11 anni fa · 7 revisions