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ů)
- Úvod do webového frameworku web.py 14. 11. 2013
- web.py – první aplikace 22. 11. 2013
- web.py – šablonovací systém 9. 12. 2013
- web.py – databáze 27. 12. 2013
- 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()
SHA256 neni sifrovaci, nybrz hashovaci funkce. Bylo by hezke mezi tim rozlisovat ;)
Díky za upozornění.
Nechcete to opravit a všude v článku změnit slovo šifrovat na hashovat?
Neni nutne, staci na konec clanku pripojit popis desifrovaciho algoritmu …
Prošel jsem to a upravil, snad je to teď dobře.
Při čtení kódu jsem narazil na následující chybu:
Správně má být:
Z dokumentace Pythonu (2.x, pro 3.x je to obdobné, https://docs.python.org/2/tutorial/errors.html):
Schválně si zkuste experimentovat s následujícím kódem:
Jedná se o jednu ze záludností Pythonu, na kterou je třeba dávat obzvláš pozor.
Máte pravdu, na tu starou
Except
syntaxi s čárkou jsem zapomněl. DíkyPro odchytavani vice vyjimek doporucuji vytvorit si tuple se vsemi vyjimkami a pak chytavat tuto „meta“ vyjimku.