Społecznościówka w 3 krokach - krok 2 - uwierzytelnianie
Wstęp do uwierzytelniania
W pierwszym wpisie stworzyliśmy zarys naszej aplikacji. Wyposażyliśmy ją w model użytkownika przechowywany w bazie danych. Umożliwiliśmy też odwiedzającym stronę rejestrację. Dziś zajmiemy się uwierzytelnianiem (ang. authentication).
Aby uwierzytelnić użytkowników HTTP możemy zastosować kilka metod. Najprostszą jest skorzystanie z uwierzytelniania typu BASIC lub DIGEST. Nie jest ona jednak bardzo popularna. Możemy ją zobaczyć np. w systemie trac - przeglądarka otrzymawszy odpowiedni kod stanu i nagłówki wyświetla okienko, gdzie należy wpisać login i hasło. Następnie wysyła odpowiednio zahashowane (metoda DIGEST jest tu bezpieczniejsza) dane uwierzytelniające na serwer.
My użyjemy metody chyba obecnie najpopularniejszej - skorzystamy z formularza na stronie. Nie jest to metoda bardzo bezpieczna. Jeśli połączenie nie jest szyfrowane, login i hasło przesyłane są czystym tekstem. Pokazuje ona jednak podstawową zasadę uwierzytelniania.
Jeśli chcemy zbudować system uwierzytelniający oparty o HTTP, musimy przede wszystkim mieć na uwadze, że jest to protokół bezstanowy. Każda para zapytanie-odpowiedź jest zamkniętą całością i serwer HTTP nie zapamiętuje sam z siebie nic, co by było związane z poprzednim zapytaniem. Musimy więc sami zadbać o to, aby użytkownik, który raz się zalogował pozostał zalogowany przez całą sesję. Technicznie więc instalujemy dwie metody uwierzytelniania. Jedną opartą o formularz, hasło i login. Drugą - o ciasteczko. Przeciętny użytkownik oczywiście nie jest świadom tego, że uwierzytelniony zostaje przy każdym odświerzeniu strony. Wszystko co potrzebne robi za niego przeglądarka.
Instalacja repoze w aplikacji
Zajrzyjmy do pliku social/config/middleware.py. Plik ten został utworzony automatycznie razem z całym szkieletem naszej aplikacji. Zawiera on tzw. middleware - wtyczki, które są dla aplikacji WSGI tym czym dekoratory dla funkcji. Przechwytują one zapytanie do oraz odpowiedź z aplikacji i wykonują jakąś modyfikację ich treści, lub obiektu environ zawierającego zmienne środowiskowe (więcej o WSGI i middleware jeszcze kiedyś napiszę. Niecierpliwych odsyłam do wiki o WSGI).
Dodajmy do tego pliku następujący import:
from repoze.what.plugins.config import make_middleware_with_config as AuthMiddleware
A następnie umieśćmy zaimportowane middleware repoze w stosie domyślnych.
app = AuthMiddleware(
app, config, app_conf['auth.what_config_file'],
who_config_file = app_conf['auth.who_config_file']
)
Możemy to zrobić w miejscu podpowiedzianym przez komentarz "CUSTOM MIDDLEWARE HERE". Możemy też nieco niżej w stosie (a wyżej w pliku) - pomiędzy RoutesMiddleware a SessionMiddleware. Ta ostatnia pozycja uzasadniona jest, jeśli będziemy kiedyś chcieli do uwierzytelniania wykorzystać sesję beakera.
W powyższym kodzie wskazaliśmy na dwie opcje konfiguracyjne, wskazujące pliki konfiguracji odpowiednio uwierzytelniania i autoryzacji. Dodajmy te opcje w pliku development.ini w sekcji [app:main]:
auth.who_config_file = %(here)s/social/config/who.ini auth.what_config_file = %(here)s/social/config/what.ini
Ponieważ nie będziemy ładować danych grup i upoważnień dla repoze.what, pozostawimy plik social/config/what.ini pusty. Natomiast plik social/config/who.ini będzie wyglądał tak:
[plugin:auth_tkt]
use = repoze.who.plugins.auth_tkt:AuthTktCookiePlugin
secret = CitJargAmrirwejPhashCebZacsurcOajvehaWri
[plugin:social]
use = social.lib.auth:SocialPlugin
[plugin:redirector]
use = repoze.who.plugins.redirector:RedirectorPlugin
login_url = /login
[general]
request_classifier = repoze.who.classifiers:default_request_classifier
challenge_decider = repoze.who.classifiers:default_challenge_decider
remote_user_key = REMOTE_USER
[identifiers]
plugins =
auth_tkt
[authenticators]
plugins =
auth_tkt
social
[challengers]
plugins =
redirector
Użyliśmy tutaj trzech wtyczek: wtyczka auth_tkt odpowiadać będzie za zapamiętanie tożsamości użytkownika w ciasteczku (bezpieczeństwo danych zapewni szyfrowanie, w który użyta będzie wartość secret - generujemy ją losowo i nikomu nie udostępniamy) i jej odzyskanie. Wtyczka social (napiszemy ją sami) uwierzytelni użytkowników na podstawie danych z formularza (porównując je z bazą danych). Wreszcie wtyczka redirector przechwyci odpowiedź ze statusem błędu 401 (nieautoryzowano) i zamieni ją na przekierowanie na stronę logowania.
Nasza wtyczka uwierzytelniająca
Zgodnie z architekturą repoze.who wtyczka uwierzytelniająca powinna implementować metodę authenticate, która przyjmuje argumenty environ i identity oraz zwracać bądź to None (jeśli uwierzytelnienie się nie powiodło), bądź to identyfikator użytkownika, który będzie używany przez aplikację (my skorzystamy po prostu z loginu).
from zope.interface import implements
from repoze.who.interfaces import IAuthenticator
from social.model import Session, User
from sqlalchemy.orm.exc import NoResultFound
class SocialPlugin(object):
implements(IAuthenticator)
def authenticate(self, environ, identity):
if identity.has_key('login'):
try:
user = Session.query(User).filter(
User.login == identity['login']
).one()
except NoResultFound:
return
if user.check_passwd(identity['passwd']):
return user.login
Powyższy kod nie jest skomplikowany. Jeśli wtyczka otrzyma obiekt identity sprawdza, czy zawiera on login. Jeśli tak - szuka użytkownika w bazie. Jeśli go znajdzie - sprawdza jego hasło. Jeśli jest poprawne - zwraca login użytkownika. Każde niepowodzenia na tej drodze powoduje zwrócenie None (pamiętajmy, że zarówno dojście do zakończenia bloku funkcji, jak i instrukcja return bez wyrażenia są równoważne zwróceniu None).
Logowanie
Przejdźmy do stworzenia frontendu logowania. Najpierw do pliku routing.py dodajmy jeszcze trzy ścieżki:
map.connect('/', controller = 'users', action = 'welcome')
map.connect('/login', controller = 'users', action = 'login')
map.connect('/logout', controller = 'users', action = 'logout')
A do naszego kontrolera - następujące metody:
def welcome(self):
if not request.environ.has_key('REMOTE_USER'):
abort(401)
else:
c.username = request.environ['REMOTE_USER']
return render('/users/welcome.mako')
Ta metoda sprawdza, czy jakiś użytkownik jest zalogowany. W przeciwnym wypadku zwraca status 401 (który jak już wspomnieliśmy zostanie przechwycony przez challengera repoze). W przeciwnym wypadku wyświetli następujący szablon:
<%inherit file="/base.mako" />
<p>Witaj ${c.username}!!!</p>
Ponieważ metoda ta jest zmapowana na URL '/' musimy wywalić domyślny statyczny plik powitalny social/public/index.html, inaczej zamiast wywołać tę metodę Pylons będzie go wyświetlał. Metoda:
def login(self):
c.message = ''
who_api = get_api(request.environ)
if 'login' in request.POST:
authenticated, headers = who_api.login({
'login': request.POST['login'],
'passwd': request.POST['passwd'],
})
if authenticated:
response.headers = headers
return redirect('/')
c.message = 'Zły login lub hasło'
else:
# Forcefully forget any existing credentials.
authenticated, headers = who_api.login({})
if 'REMOTE_USER' in request.environ:
del request.environ['REMOTE_USER']
return render('/users/login.mako')
Albo wyświetla szablon logowania, albo też (jeśli w zapytaniu są parametry POST z formularza) wywołuje uwierzytelnianie przez API repoze.who. Zauważmy, że słownik z parametrami login i passwd to ten sam słownik, który zostanie przekazany do naszego pluginu. Jeśli uwierzytelnienie się powiedzie, zostajemy przekierowani na stronę startową. Jeśli nie - wyrenderuje się znowu szablon logowania, który wygląda tak:
<%inherit file="/base.mako" />
${c.message}
<form method="POST">
<div><label for="login">Login:</label><input name="login" /></div>
<div><label for="passwd">Hasło:</label><input name="passwd" type="password" /></div>
<input type="submit" />
</form>
Nie różni się więc on specjalnie od szablonu rejestracji. Nie ma tylko pola repeatPasswd i renderuje dodatkowo opcjonalną wiadomość c.message od kontrolera logowania.
I to wszystko, choć nasuwa się jedno pytanie
Mechanizm uwierzytelniania już działa. Możemy zalogować się (aplikacja powita nas) i wylogować. Wielu jednak zapyta, po co tyle wysiłku. Funkcjonalność, jaką obecnie oferuje nam nasza aplikacja moglibyśmy przecież uzyskać znacznie prościej, bez bawienia się we wtyczki, middleware itp. Odpowiedź jest prosta - pracując na frameworku typu repoze.who możemy z łatwością rozwijać możliwości aplikacji. Jeśli kiedyś zamarzy nam się dodanie np. uwierzytelniania użytkowników na podstawie openID ze sprawdzeniem ich w katalogu LDAP - wystarczy nam wstawić dwie nowe wtyczki i bez refaktoryzacji połowy kodu (która pewnie byłaby nieuchronna w przypadku "najprostszych rozwiązań") nasz serwis będzie dalej śmigał.
Za tydzień - dodawanie przyjaciół i uwierzytelnianie. A zadania na dzisiaj:
- Zajrzeć na stronę pluginów repoze.who i spróbować zainstalować któryś z nich w naszej aplikacji.
- Spróbować napisać własny plugin, który uwierzytelni zahardcode'owanego użytkownika.