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:

  1. Zajrzeć na stronę pluginów repoze.who i spróbować zainstalować któryś z nich w naszej aplikacji.
  2. Spróbować napisać własny plugin, który uwierzytelni zahardcode'owanego użytkownika.

Meta

Opublikowany: Lut. 22, 2011

Autor: zefciu

Komentarze:  

Liczba słów: 1,228

Następny: Społecznościówka w 3 krokach - krok 3

Poprzedni: Iteratory - prawie wszystko

Bookmark and Share

Tagi

autoryzacja baza_danych blog cookies middleware nose overloading przeciążenie przeładowanie pylons repoze repoze.what repoze.who sqlalchemy testy uwierzytelnianie vcs wersjonowanie wsgi

Kontynuacje

Follows Up On

Linki z wpisu

  1. Pythonista - Dekoratory część 1
  2. repoze.who-use_beaker 0.3 : Python Package Index
  3. About repoze.who Plugins — repoze.who 2.0 documentation
Comments powered by Disqus