Progetto

Generale

Profilo

DjangoTornadoNginxAuth » Cronologia » Versione 6

Versione 5 (Mark Caglienzi, 12-12-2013 15:12) → Versione 6/7 (Mark Caglienzi, 12-12-2013 23:09)

h1. 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). 

 h2. Prerequisiti 

 Servono (oltre a django e tornado, ovviamente) anche i pacchetti: @devscripts@, @mercurial@, @fakeroot@. 

 h2. Compilazione di nginx 

 Esiste "un plugin per nginx":https://github.com/perusio/nginx-auth-request-module (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. 

 <pre> 
 $ 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 
 </pre> 

 Questo per scaricare il pacchetto sorgente di nginx 1.4.4 e averlo automaticamente scompattato. 

 <pre> 
 $ cd nginx-1.4.4/debian/modules 
 $ hg clone http://mdounin.ru/hg/ngx_http_auth_request_module nginx-auth-request 
 </pre> 

 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). 

 <pre> 
 # apt-get build-dep nginx 
 </pre> 

 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: 

 <pre> 
 --add-module=$(MODULESDIR)/nginx-auth-request \ 
 </pre> 

 sotto alle altre direttive @--add-module@, in modo che il file si presenti così: 

 <pre> 
 [...] 
 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 $@ 
 [...] 
 </pre> 

 A questo punto bisogna compilare i pacchetti binari: 

 <pre> 
 $ cd /home/utente/projects/nginx/nginx-1.4.4 
 $ fakeroot debian/rules binary 
 </pre> 

 Alla fine della compilazione nella directory @/home/utente/projects/nginx@ saranno presenti i pacchetti binari: 

 <pre> 
 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 
 </pre> 

 Si può procedere ad installare i pacchetti necessari direttamente da questa directory: 

 <pre> 
 # 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 
 </pre> 

 h2. 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":http://www.tornadoweb.org/en/stable/#hello-world) e che le due applicazioni siano servite dai server di sviluppo: 

 <pre> 
 $ cd /home/utente/projects/django 
 $ ./manage.py runserver 
 </pre> 

 <pre> 
 $ cd /home/utente/projects/tornado 
 $ ./hello-world.py 
 </pre> 

 In questo modo il server di sviluppo di django risponde a @127.0.0.1:8000@ e tornado a @127.0.0.1:8888@ 

 h2. Creazione del file di configurazione di nginx 

 Si crei il file @/etc/nginx/sites-available/django@: 

 <pre> 
 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; 
     } 
 } 
 </pre> 

 e si attivi con: 

 <pre> 
 # ln -s /etc/nginx/sites-available/django /etc/nginx/sites-enabled/django 
 # /etc/init.d/nginx restart 
 </pre> 

 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 

 <pre> 
 127.0.0.1 django 
 </pre> 

 al file @/etc/hosts@. 

 h2. 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: 
 <pre> 
 $ 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 
 </pre> 

 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 di tornado va modificata così: 

 
 <pre><code class="python"> 
 #!/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)                   self.get_cookie('user'))    ### '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() 
 </code></pre> 

 in modo che @MainHandler@ risponda all'URL @/tornado@ anziché all'URL @/@. Di fatto il metodo @MainHandler.get_django_signed_cookie()@ è paragonabile a @Request.get_signed_cookie()@ di django. Nell'applicazione django va aggiunto in @urls.py@: 

 <pre><code class="python"> 
     url(r'^is_logged_in/$', 'myproject.views.is_logged_in', name='is_logged_in'), 
 </code></pre> 

 e una view di questo tipo: 

 <pre><code class="python"> 
 from django import http 

 def is_logged_in(request): 
     if request.user.is_anonymous(): 
         return http.HttpResponseForbidden("NO") 
     else: 
         return http.HttpResponse("OK") 
 </code></pre> 

 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: 

 <pre><code class="python"> 
 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( response.set_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 
 </code></pre> 

 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): 

 <pre><code class="html"> 
 <a href="/tornado" class="btn">Tornado</a> 
 </code></pre> 

 nginx si comporterà così: 

 * Vede la richiesta a @http://django/tornado@ 
 * La direttiva @auth_request@ passa il controllo all'URL @http://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 

 h2. 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 decoratore @login_required@ nella view django. 
 * Il "changelog di nginx":http://nginx.org/en/CHANGES 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.