Project

General

Profile

DjangoTornadoNginxAuth » History » Version 6

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