web.py – autentizace a autorizace

V dnešním díle se budeme věnovat správě uživatelů a přístupových práv, tedy autentizaci a autorizaci. Pomocí frameworku web.py a rozšiřujícího modulu si vytvoříme jednoduchou aplikaci vyžadující přihlášení. Také si popíšeme základy přechovávání uživatelských jmen a hesel.

Seriál: Webový framework web.py (5 dílů)

  1. Úvod do webového frameworku web.py 14.11.2013
  2. web.py – první aplikace 22.11.2013
  3. web.py – šablonovací systém 9.12.2013
  4. web.py – databáze 27.12.2013
  5. web.py – autentizace a autorizace 1.9.2014

V minulém díle jsme se věnovali práci s databázemi, dnes tyto znalosti využijeme v další důležité části webové aplikace, totiž autentizaci a autorizaci. Nejprve si ujasníme tyto pojmy, poté si krátce povíme něco o bezpečném přechovávání uživatelských údajů, dále si vytvoříme uživatelskou databázi a nakonec s využitím nového modulu vytvoříme kousek po kousku jednoduchou aplikaci, která vyžaduje přihlášení uživatele.

Předem přiznávám, že nejsem expert přes počítačovou bezpečnost. Budu tedy rád, když v komentářích upozorníte na bezpečnostní problémy či možná vylepšení popisovaného řešení.

Než budeme pokračovat, pro jistotu si zopakujeme dva základní pojmy tohoto článku:
Autentizace – při autentizaci zjišťujeme identitu uživatele, nejčastěji pomocí přihlášení.
Autorizace – při autorizaci kontrolujeme, zda autentizovaný uživatel má dostatečná práva pro provádění určité akce, například přístup na stránku či úpravu obsahu.

Balíček web.py modules

Nejprve jsem chtěl popsat celý postup výroby modulu pro správu uživatelů a přístupů, tento modul se mi ale rozrostl tak, že jeho kompletní popis by byl příliš zdlouhavý, proto jsem ho osamostatnil do podoby samostatného balíčku web.py-modules rozšiřujícího framework web.py. V dnešním díle budeme tento balíček využívat.

Jednotlivé funkce knihovny si zběžně probereme, detaily je možné dohledat v dokumentaci projektu. V případě návrhu na vylepšení můžete na GitHubu přidat nový issue či upozornit v komentářích.

web-modulu: https://github.com/PetrHoracek/webpy-modules
dokumentace: https://github.com/PetrHoracek/webpy-modules/blob/master/docs/auth.rst

Pro nainstalování pluginu je možné využít repozitáře PyPI:

pip install web.py-modules

Případně můžete nainstalovat vývojovou verzi přímo z GitHubu:

git clone https://github.com/PetrHoracek/webpy-modules/
cd webpy-modules
python setup.py install

Uchovávání a hashování

Začněme s ukládáním uživatelských záznamů, v našem případě uživatelského jména, hesla a uživatelské role. Pro případ prolomení přístupu do databáze není vhodné hesla ukládat ve formě prostého textu. Ani některé slabší hashovací algoritmy nemusí být pro případného hackera problém.

Hesla je dobré ukládat pomocí pokročilejších algoritmů (např. SHA256) doplněných o tzv. salt, tedy textový řetězec, který je přidán k heslu ještě před jeho zahashováním, tím značně zvětšíme délku řetězce a ztížíme prolomení hesla. Salt by měl být alespoň stejně dlouhý jako hashované heslo (SHA256 vytváří 32 bytů dlouhé řetězce) a měl by být originální pro každý uživatelský záznam (tímto znemožníme využití databází s již zahashovanými hesly, každý řetězec je originál).

Ještě bezpečnější, ale také výkonostně náročnější, možností je použití algoritmu Bcrypt. Ten provádí několik hashování po sobě a zaberou nezanedbatelný čas, jeho rozšifrování by bylo tedy extrémně časově náročné. Problém ale může nastat na vytíženém serveru, který nebude stíhat hesla zahashovávat.

Pro podrobnější rozbor tohoto tématu doporučuji prostudovat tento článek: https://crackstation.net/hashing-security.htm (anglicky).

Modul auth z balíčku web.py-modules obsahuje mimo jiné i objekt Crypt, ten obstarává funkce pro hashování a porovnávání hesel. Tyto funkce mohou být použity při ukládání hesel nových uživatelů atp. V současné době modul podporuje dva hashovací algoritmy.

Základním algoritmem je SHA256, který je doplněn o 32 bytů dlouhý salt. Funkce pro hashování ukládá zahashované heslo i jeho salt do jednoho řetězce ve formátu heslo$salt, díky tomu stačí pro uložení obou hodnot jediné pole databáze. Druhým algoritmem je již zmíněný Bcrypt.

Použití hashovavích funkcí:

from webmod import auth
# vytvoření instance není povinné, funkce objektu jsou statické
crypt = auth.Crypt()

# funkce pro zahashování řetězce
>>> crypt.encrypt("tiger") # zahashuj řetězec algoritmem SHA256
'0a57e44ff2...a2dc11f5$05f54e...495020d6f'
>>> crypt['sha256salt'].encrypt("tiger") # to samé (s jiným saltem)
'6b0b76fcd5...0734f80a$fa8f35...26d5b8cc0'
>>> crypt['bcrypt'].encrypt("tiger") # použij algoritmus Bcrypt
'$2a$10$aKiFSfoppYby82G.qFFDa.qL9DKOgGiiixedqC8f62UzgJpJ/j19.'

# funkce pro porovnání vloženého hesla a uloženého zahashovaného hesla
# vložené heslo je zahashováno společně se saltem uloženým v uloženém řetězci
>>> crypt.compare("tiger", cryptedPassword1) # porovnej heslo a uložený řetězec
True
>>> crypt['sha256salt'].compare("tiger", cryptedPassword1) # to samé
True
>>> crypt['bcrypt'].compare("tiger", cryptedPassword2) # použij algoritmus Bcrypt
True

Úložiště

Je mnoho způsobů, jak řešit systém uživatelů a od toho se odvíjející úložiště. Můžeme pracovat s dvojicemi jméno-heslo či trojicemi jméno-heslo-role(jedna) nebo jméno heslo-role(více). Třetí možnost však vyžaduje dvě, či lépe tři tabulky. My budeme dnes využívat trojice jméno-heslo-role(jedna).

Vytvořme si tedy potřebnou tabulku. Budeme požívat algoritmus SHA256 se saltem, záznam hesla tedy bude dlouhý 32+1+32=65 znaků. Následující kód vytvoří tabulku v SQL databázi a vloží nového uživatele jan s heslem tiger a rolí admin:

CREATE TABLE users (
    usr CHAR(30) NOT NULL PRIMARY KEY,
    passwd CHAR(65) NOT NULL,
    role CHAR(30) NOT NULL
);

INSERT INTO users (usr, passwd, role) VALUES ('jan', '34748930badf277ac5d7b0d4525ffa375b0e85fc4767943fea5a7c490e70628c$6a895e5e41c05b814584d31cce0fe83812d229f4b01e521f3ae1ae12504c42f6', 'admin');

Vytvoření objektu Auth

Pro autentizaci a autorizaci obsahuje modul auth objekt Auth. Před použitím následujících metod, je nutné vytvořit instanci tohoto objektu. Při vytváření jsou předány objekty session a databáze, díky tomu pak může objekt Auth komunikovat s dalšími částmi web.py. Nepovinným parametrem je lgn_pg, což je stránka, na kterou proběhne přesměrování v případě nesplnění autorizačních požadavků, pokud není tento parametr definován, je vyvolána metoda web.forbidden().

db = web.database(...)
session = web.session.Session(...)
auth = webmod.auth.Auth(session, db, lgn_pg='/login')

Autentizace

Pusťme se do autentizace uživatelů. Přihlášení uživatelů budeme řešit nejběžnějším způsobem přihlašovacím HTML formulářem. Přihlašovací jméno a heslo, předané tímto formulářem pomocí metody POST, přečteme v aplikaci a předáme metodě auth.login().

Metoda přihlašovací stránky může vypadat takto. Pokud je už někdo přihlášen, je přesměrován na úvodní stránku. Pokud není v session zapsán žádný uživatel, je vykreslen formulář. Ještě před jeho vykreslením je však změněna HTML hlavička. Díky této změně nebude stránka s formulářem cachována a nebude se na ni možné vrátit pomocí tlačíka Zpět prohlížeče.

def GET(self):
    if auth.getrole():
        raise web.seeother('/')
    else:
        web.header("Cache-Control",
                   "no-cache, max-age=0, must-revalidate, no-store")
        login_form = '''<form action="/login" method="POST">
            <input type="text" name="usr" placeHolder="Jméno" /><br />
            <input type="password" name="passwd" placeHolder="Heslo" />
            <br /><input type="submit" value="Přihlásit" />
        </form>'''
        return render(login_form)

Metoda login() se nejprve pokusí dohledat předané jméno v databázi uživatelů. Pokud jméno nenajde, vyvolá vyjímku UserNotFound. V opačném případě ze záznamu hesla přihlašovaného uživatele získá salt a pomocí něj zahashuje e předávané heslo. Pokud se bude zahashované heslo shodovat s heslem v databázi, modul zapíše uživatele do session. V opačném případě vyvolá výjimku WrongPassword.

Metoda pro přihlášení může vypadat například takto. Pokud se přihlášení povede, uživatel je přesměrován na úvodní stránku, v opačném případě je přesměrován na přihlašovací formulář.

def POST(self):
    usr = web.input().usr
    passwd = web.input().passwd
    try:
        auth.login(usr, passwd)
        raise web.seeother('/private')
    except webmod.auth.UserNotFound, webmod.auth.WrongPassword:
        raise web.seeother('/login')

Pro odhlášení slouží metoda auth.logout(). Ta vymaže uživatele ze současné session.

def GET(self):
    auth.logout()

Autorizace

Pro kontrolu přístupových práv uživatele obsahuje objekt Auth dvě prosté metody a jeden dekorátor. Při kontrole se vždy z databáze nahraje současná role přihlášeného uživatele, díky tomu má uživatel vždy pouza práva aktuálně přiřazené role.

Základní omezení přístupu je obstaráno pomocí dekorátoru @role(), ten stačí zapsat před metodu, které chcete omezit přístup. Jako argumenty názvy povolených rolí. Dekorátor jednoduše zkontroluje, zda se role přihlášeného uživatele nachází mezi vypsanými.

@auth.role('admin')
def GET(self):
    return render.text("Admin's page")

Pro získání role uživatele můžete využít metodu getrole().

>>> auth.getrole()
'admin'

Navíc má objekt Auth metodu hasrole(), která vrací True pokud se role přihlášeného uživatele nachází v předaných argumentech.

>>> auth.hasrole('user', 'admin')
True

Výsledná aplikace

Na závěr předkládám hotovou aplikaci. Pro její použití stačí stáhnout balíček web.py a web.py-modules. Dále je třeba vytvořit příslušnou databázi a poté jen spustit následující kód.

# -*- coding: utf-8 -*-
import web
import webmod.auth
web.config.debug = False

urls = (
    '/', 'public',
    '/login', 'login',
    '/logout', 'logout',
    '/private', 'private'
)

app = web.application(urls, globals())
db = web.database(dbn='sqlite', db='databaze')
session = web.session.Session(app, web.session.DiskStore('sessions'))

auth = webmod.auth.Auth(session, db, lgn_pg='/login')


class login:
    def GET(self):
        web.header("Cache-Control",
                   "no-cache, max-age=0, must-revalidate, no-store")
        if auth.getrole():
            raise web.seeother('/')
        else:
            login_form = '''<form action="/login" method="POST">
                <input type="text" name="usr" placeHolder="Jméno" /><br />
                <input type="password" name="passwd" placeHolder="Heslo" />
                <br /><input type="submit" value="Přihlásit" />
            </form>'''
            return render(login_form)

    def POST(self):
        usr = web.input().usr
        passwd = web.input().passwd
        try:
            auth.login(usr, passwd)
            raise web.seeother('/')
        except webmod.auth.UserNotFound, webmod.auth.WrongPassword:
            return web.seeother('/login')


class logout:
    def GET(self):
        auth.logout()
        raise web.seeother('/')


class public:
    def GET(self):
        role = auth.getrole()
        if role:
            return render("Veřejná stránka | jste přihlášen(á)")
        else:
            return render("Veřejná stránka")


class private:
    @auth.role('admin', 'boss')
    def GET(self):
        return render("Soukromá stránka")


def render(text):
    page = '''<meta charset="utf-8">
              <a href='/'>Veřejná stránka</a>
              <a href='/private'>Soukromá stránka</a> '''

    if auth.getrole():
        page += "<a href='/logout'>Odhlásit</a><br />"
    else:
        page += "<a href='/login'>Přihlásit</a><br />"

    page += text
    return page


if __name__ == "__main__":
    app.run()

Absolvent gymnázia a budoucí student FI MUNI se zájmem o programování, Linux, hudbu, elektroniku, umění a černobílou analogovou fotografii. Začal jsem u modrého Dosu, přešel přes C#, Javu a nyní se věnuji hlavně Pythonu, mimo jiné i jako stážista v Red Hatu.

Věděli jste, že nám můžete zasílat zprávičky? (Jen pro přihlášené.)

Komentáře: 8

Přehled komentářů

SUK Sifrovani?
Petr Horáček Re: Sifrovani?
DavidGrudl Re: Sifrovani?
Jirka Jr Re: Sifrovani?
Martin Hassman Re: Sifrovani?
honzas Pozor na obsluhu výjimek v Pythonu
Petr Horáček Re: Pozor na obsluhu výjimek v Pythonu
IT expert Re: Pozor na obsluhu výjimek v Pythonu
Zdroj: https://www.zdrojak.cz/?p=13195