DjangoTornadoNginxAuth » Cronologia » Versione 6
Mark Caglienzi, 12-12-2013 23:09
Uso dell'infrastruttura di Django per usare i cookie firmati (ed evitare problemi di modifica del cookie lato utente)
| 1 | 1 | Christopher R. Gabriel | h1. Django Tornado Nginx Auth |
|---|---|---|---|
| 2 | 2 | Mark Caglienzi | |
| 3 | 5 | Mark Caglienzi | 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. |
| 4 | 2 | Mark Caglienzi | |
| 5 | La guida è condotta su un sistema Debian Jessie, al momento in testing (per altre versioni probabilmente andrà adattato qualcosa). |
||
| 6 | |||
| 7 | h2. Prerequisiti |
||
| 8 | |||
| 9 | Servono (oltre a django e tornado, ovviamente) anche i pacchetti: @devscripts@, @mercurial@, @fakeroot@. |
||
| 10 | |||
| 11 | h2. Compilazione di nginx |
||
| 12 | |||
| 13 | 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. |
||
| 14 | |||
| 15 | <pre> |
||
| 16 | $ mkdir /home/utente/projects/nginx |
||
| 17 | $ cd /home/utente/projects/nginx |
||
| 18 | $ dget http://ftp.de.debian.org/debian/pool/main/n/nginx/nginx_1.4.4-1.dsc |
||
| 19 | </pre> |
||
| 20 | |||
| 21 | Questo per scaricare il pacchetto sorgente di nginx 1.4.4 e averlo automaticamente scompattato. |
||
| 22 | |||
| 23 | <pre> |
||
| 24 | $ cd nginx-1.4.4/debian/modules |
||
| 25 | $ hg clone http://mdounin.ru/hg/ngx_http_auth_request_module nginx-auth-request |
||
| 26 | </pre> |
||
| 27 | |||
| 28 | 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). |
||
| 29 | |||
| 30 | <pre> |
||
| 31 | # apt-get build-dep nginx |
||
| 32 | </pre> |
||
| 33 | |||
| 34 | 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: |
||
| 35 | |||
| 36 | <pre> |
||
| 37 | --add-module=$(MODULESDIR)/nginx-auth-request \ |
||
| 38 | </pre> |
||
| 39 | |||
| 40 | sotto alle altre direttive @--add-module@, in modo che il file si presenti così: |
||
| 41 | |||
| 42 | <pre> |
||
| 43 | [...] |
||
| 44 | config.status.full: config.env.full ### In questa sezione... |
||
| 45 | cd $(BUILDDIR_full) && CFLAGS="$(CFLAGS)" CORE_LINK="$(LDFLAGS)" ./configure \ |
||
| 46 | --prefix=/usr/share/nginx \ |
||
| 47 | --conf-path=/etc/nginx/nginx.conf \ |
||
| 48 | --error-log-path=/var/log/nginx/error.log \ |
||
| 49 | --http-client-body-temp-path=/var/lib/nginx/body \ |
||
| 50 | --http-fastcgi-temp-path=/var/lib/nginx/fastcgi \ |
||
| 51 | --http-log-path=/var/log/nginx/access.log \ |
||
| 52 | --http-proxy-temp-path=/var/lib/nginx/proxy \ |
||
| 53 | --http-scgi-temp-path=/var/lib/nginx/scgi \ |
||
| 54 | --http-uwsgi-temp-path=/var/lib/nginx/uwsgi \ |
||
| 55 | --lock-path=/var/lock/nginx.lock \ |
||
| 56 | --pid-path=/run/nginx.pid \ |
||
| 57 | --with-pcre-jit \ |
||
| 58 | --with-debug \ |
||
| 59 | --with-http_addition_module \ |
||
| 60 | --with-http_dav_module \ |
||
| 61 | --with-http_geoip_module \ |
||
| 62 | --with-http_gzip_static_module \ |
||
| 63 | --with-http_image_filter_module \ |
||
| 64 | --with-http_realip_module \ |
||
| 65 | --with-http_stub_status_module \ |
||
| 66 | --with-http_ssl_module \ |
||
| 67 | --with-http_sub_module \ |
||
| 68 | --with-http_xslt_module \ |
||
| 69 | --with-ipv6 \ |
||
| 70 | --with-mail \ |
||
| 71 | --with-mail_ssl_module \ |
||
| 72 | --add-module=$(MODULESDIR)/nginx-auth-pam \ |
||
| 73 | --add-module=$(MODULESDIR)/nginx-dav-ext-module \ |
||
| 74 | --add-module=$(MODULESDIR)/nginx-echo \ |
||
| 75 | --add-module=$(MODULESDIR)/nginx-upstream-fair \ |
||
| 76 | --add-module=$(MODULESDIR)/ngx_http_substitutions_filter_module \ |
||
| 77 | --add-module=$(MODULESDIR)/nginx-auth-request \ ### ...aggiungere questa riga |
||
| 78 | $(CONFIGURE_OPTS) >$@ |
||
| 79 | touch $@ |
||
| 80 | [...] |
||
| 81 | </pre> |
||
| 82 | |||
| 83 | A questo punto bisogna compilare i pacchetti binari: |
||
| 84 | |||
| 85 | <pre> |
||
| 86 | $ cd /home/utente/projects/nginx/nginx-1.4.4 |
||
| 87 | $ fakeroot debian/rules binary |
||
| 88 | </pre> |
||
| 89 | |||
| 90 | Alla fine della compilazione nella directory @/home/utente/projects/nginx@ saranno presenti i pacchetti binari: |
||
| 91 | |||
| 92 | <pre> |
||
| 93 | nginx_1.4.4-1_all.deb |
||
| 94 | nginx-common_1.4.4-1_all.deb |
||
| 95 | nginx-doc_1.4.4-1_all.deb |
||
| 96 | nginx-extras_1.4.4-1_amd64.deb |
||
| 97 | nginx-extras-dbg_1.4.4-1_amd64.deb |
||
| 98 | nginx-full_1.4.4-1_amd64.deb |
||
| 99 | nginx-full-dbg_1.4.4-1_amd64.deb |
||
| 100 | nginx-light_1.4.4-1_amd64.deb |
||
| 101 | nginx-light-dbg_1.4.4-1_amd64.deb |
||
| 102 | nginx-naxsi_1.4.4-1_amd64.deb |
||
| 103 | nginx-naxsi-dbg_1.4.4-1_amd64.deb |
||
| 104 | nginx-naxsi-ui_1.4.4-1_all.deb |
||
| 105 | </pre> |
||
| 106 | |||
| 107 | Si può procedere ad installare i pacchetti necessari direttamente da questa directory: |
||
| 108 | |||
| 109 | <pre> |
||
| 110 | # 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 |
||
| 111 | </pre> |
||
| 112 | |||
| 113 | h2. Avvio delle applicazioni |
||
| 114 | |||
| 115 | 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: |
||
| 116 | |||
| 117 | <pre> |
||
| 118 | $ cd /home/utente/projects/django |
||
| 119 | $ ./manage.py runserver |
||
| 120 | </pre> |
||
| 121 | |||
| 122 | <pre> |
||
| 123 | $ cd /home/utente/projects/tornado |
||
| 124 | $ ./hello-world.py |
||
| 125 | </pre> |
||
| 126 | |||
| 127 | In questo modo il server di sviluppo di django risponde a @127.0.0.1:8000@ e tornado a @127.0.0.1:8888@ |
||
| 128 | |||
| 129 | h2. Creazione del file di configurazione di nginx |
||
| 130 | |||
| 131 | Si crei il file @/etc/nginx/sites-available/django@: |
||
| 132 | |||
| 133 | <pre> |
||
| 134 | server { |
||
| 135 | listen 80; |
||
| 136 | root /home/utente/projects/django; |
||
| 137 | server_name django; |
||
| 138 | access_log /home/utente/projects/django/access.log; |
||
| 139 | error_log /home/utente/projects/django/error.log; |
||
| 140 | location / { |
||
| 141 | 1 | Christopher R. Gabriel | proxy_pass http://127.0.0.1:8000; |
| 142 | 2 | Mark Caglienzi | } |
| 143 | location /tornado { |
||
| 144 | auth_request /is_logged_in/; |
||
| 145 | 4 | Mark Caglienzi | proxy_pass http://127.0.0.1:8888; |
| 146 | 2 | Mark Caglienzi | } |
| 147 | } |
||
| 148 | </pre> |
||
| 149 | |||
| 150 | e si attivi con: |
||
| 151 | |||
| 152 | <pre> |
||
| 153 | # ln -s /etc/nginx/sites-available/django /etc/nginx/sites-enabled/django |
||
| 154 | # /etc/init.d/nginx restart |
||
| 155 | </pre> |
||
| 156 | |||
| 157 | 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. |
||
| 158 | |||
| 159 | Aggiungere la riga |
||
| 160 | |||
| 161 | <pre> |
||
| 162 | 127.0.0.1 django |
||
| 163 | </pre> |
||
| 164 | |||
| 165 | al file @/etc/hosts@. |
||
| 166 | |||
| 167 | h2. Modifica delle applicazioni Django e Tornado |
||
| 168 | |||
| 169 | 6 | Mark Caglienzi | 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. |
| 170 | |||
| 171 | 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: |
||
| 172 | <pre> |
||
| 173 | $ ln -s /home/utente/projects/django/my_project/settings.py /home/utente/projects/tornado/django_settings.py |
||
| 174 | $ cd /home/utente/projects/tornado && export DJANGO_SETTINGS_MODULE="django_settings" && ./hello_world.py |
||
| 175 | </pre> |
||
| 176 | |||
| 177 | 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ì: |
||
| 178 | |||
| 179 | 2 | Mark Caglienzi | <pre><code class="python"> |
| 180 | 1 | Christopher R. Gabriel | #!/usr/bin/env python |
| 181 | import tornado.ioloop |
||
| 182 | 2 | Mark Caglienzi | import tornado.web |
| 183 | |||
| 184 | 1 | Christopher R. Gabriel | class MainHandler(tornado.web.RequestHandler): |
| 185 | 6 | Mark Caglienzi | def get_django_signed_cookie(self, key): |
| 186 | from django.core import signing |
||
| 187 | |||
| 188 | signed_value = self.get_cookie(key) |
||
| 189 | try: |
||
| 190 | value = signing.get_cookie_signer(salt=key).unsign(signed_value, max_age=None) |
||
| 191 | except: |
||
| 192 | value = None |
||
| 193 | return value |
||
| 194 | |||
| 195 | def get_current_user(self): |
||
| 196 | user = self.get_django_signed_cookie('user') |
||
| 197 | return user |
||
| 198 | |||
| 199 | 1 | Christopher R. Gabriel | def get(self): |
| 200 | 6 | Mark Caglienzi | username = self.get_current_user() |
| 201 | self.write("Hello, %s!" % username) ### 'Hello, world!' diventa 'Hello, username!' |
||
| 202 | 2 | Mark Caglienzi | ### Il setup del cookie viene fatto nella view di login di django (v. sotto) |
| 203 | |||
| 204 | 4 | Mark Caglienzi | application = tornado.web.Application([ |
| 205 | # (r"/", MainHandler), ### Commentare/cancellare questa riga... |
||
| 206 | 2 | Mark Caglienzi | (r'/tornado', MainHandler), ### ...e aggiungere questa |
| 207 | ]) |
||
| 208 | |||
| 209 | if __name__ == "__main__": |
||
| 210 | application.listen(8888) |
||
| 211 | 1 | Christopher R. Gabriel | tornado.ioloop.IOLoop.instance().start() |
| 212 | 5 | Mark Caglienzi | </code></pre> |
| 213 | 2 | Mark Caglienzi | |
| 214 | 6 | Mark Caglienzi | 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@: |
| 215 | 2 | Mark Caglienzi | |
| 216 | 5 | Mark Caglienzi | <pre><code class="python"> |
| 217 | 2 | Mark Caglienzi | url(r'^is_logged_in/$', 'myproject.views.is_logged_in', name='is_logged_in'), |
| 218 | 5 | Mark Caglienzi | </code></pre> |
| 219 | 2 | Mark Caglienzi | |
| 220 | e una view di questo tipo: |
||
| 221 | |||
| 222 | 5 | Mark Caglienzi | <pre><code class="python"> |
| 223 | 2 | Mark Caglienzi | from django import http |
| 224 | 1 | Christopher R. Gabriel | |
| 225 | 2 | Mark Caglienzi | def is_logged_in(request): |
| 226 | 1 | Christopher R. Gabriel | if request.user.is_anonymous(): |
| 227 | 2 | Mark Caglienzi | return http.HttpResponseForbidden("NO") |
| 228 | 1 | Christopher R. Gabriel | else: |
| 229 | return http.HttpResponse("OK") |
||
| 230 | 5 | Mark Caglienzi | </code></pre> |
| 231 | 1 | Christopher R. Gabriel | |
| 232 | 4 | Mark Caglienzi | che non fa altro che restituire un HTTP 403 in caso l'utente non sia loggato, e un HTTP 200 diversamente. |
| 233 | |||
| 234 | 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: |
||
| 235 | 5 | Mark Caglienzi | |
| 236 | 4 | Mark Caglienzi | <pre><code class="python"> |
| 237 | from django import http |
||
| 238 | from django.contrib import messages |
||
| 239 | from django.contrib.auth import authenticate, login, logout |
||
| 240 | from django.contrib.auth.decorators import login_required |
||
| 241 | from django.core.urlresolvers import reverse |
||
| 242 | from django.shortcuts import render |
||
| 243 | from django.utils.translation import ugettext_lazy as _ |
||
| 244 | |||
| 245 | def login_view(request): |
||
| 246 | username = password = '' |
||
| 247 | _next = request.GET.get("next", None) |
||
| 248 | if request.POST: |
||
| 249 | username = request.POST['username'] |
||
| 250 | password = request.POST['password'] |
||
| 251 | user = authenticate(username=username, password=password) |
||
| 252 | if user is not None: |
||
| 253 | if user.is_active: |
||
| 254 | login(request, user) |
||
| 255 | messages.info(request, _('Welcome, %s!' % username)) |
||
| 256 | 1 | Christopher R. Gabriel | if _next: |
| 257 | response = http.HttpResponseRedirect(_next) |
||
| 258 | else: |
||
| 259 | 4 | Mark Caglienzi | response = http.HttpResponseRedirect(reverse('home')) |
| 260 | 6 | Mark Caglienzi | ### Il cookie sarà firmato dall'infrastruttura di Django in modo che se viene modificato |
| 261 | ### dall'utente, tornado se ne accorga in fase di authorization |
||
| 262 | response.set_signed_cookie( |
||
| 263 | 4 | Mark Caglienzi | key='user', |
| 264 | value=request.user.username, |
||
| 265 | expires=request.session.get_expiry_age(), |
||
| 266 | ) |
||
| 267 | return response |
||
| 268 | else: |
||
| 269 | messages.error(request, _('Account %s is disabled' % username)) |
||
| 270 | else: |
||
| 271 | messages.error(request, |
||
| 272 | _("The credentials you supplied were not correct or did not grant access to this resource.")) |
||
| 273 | return render(request, 'login.html') |
||
| 274 | |||
| 275 | @login_required |
||
| 276 | def logout_view(request): |
||
| 277 | logout(request) |
||
| 278 | response = http.HttpResponseRedirect(reverse('home')) |
||
| 279 | response.delete_cookie('user') |
||
| 280 | return response |
||
| 281 | 5 | Mark Caglienzi | </code></pre> |
| 282 | 4 | Mark Caglienzi | |
| 283 | 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. |
||
| 284 | |||
| 285 | Se un template dell'applicazione django ha un link di questo tipo (o se si tenta di scrivere a mano l'URL): |
||
| 286 | |||
| 287 | 5 | Mark Caglienzi | <pre><code class="html"> |
| 288 | 2 | Mark Caglienzi | <a href="/tornado" class="btn">Tornado</a> |
| 289 | 5 | Mark Caglienzi | </code></pre> |
| 290 | 2 | Mark Caglienzi | |
| 291 | nginx si comporterà così: |
||
| 292 | |||
| 293 | * Vede la richiesta a @http://django/tornado@ |
||
| 294 | * La direttiva @auth_request@ passa il controllo all'URL @http://django/is_logged_in/@ |
||
| 295 | * Django controlla se l'utente è loggato o meno, e dà una response di tipo 200 o 403 |
||
| 296 | 4 | Mark Caglienzi | * 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 |
| 297 | 2 | Mark Caglienzi | * Se la risposta è 403, nginx non prosegue e l'utente si vede impossibilitato a proseguire |
| 298 | |||
| 299 | 3 | Mark Caglienzi | h2. Dubbi/problemi |
| 300 | 2 | Mark Caglienzi | |
| 301 | * 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 |
||
| 302 | * 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. |
||
| 303 | * 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. |