Přejít k navigační liště

Zdroják » Různé » web.py – autentizace a autorizace

web.py – autentizace a autorizace

Články Různé

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.

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()

Komentáře

Subscribe
Upozornit na
guest
8 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
SUK

SHA256 neni sifrovaci, nybrz hashovaci funkce. Bylo by hezke mezi tim rozlisovat ;)

DavidGrudl

Nechcete to opravit a všude v článku změnit slovo šifrovat na hashovat?

Jirka Jr

Neni nutne, staci na konec clanku pripojit popis desifrovaciho algoritmu …

Martin Hassman

Prošel jsem to a upravil, snad je to teď dobře.

Jan Švec

Při čtení kódu jsem narazil na následující chybu:

except webmod.auth.UserNotFound, webmod.auth.WrongPassword:

Správně má být:

except (webmod.auth.UserNotFound, webmod.auth.WrongPassword):

Z dokumentace Pythonu (2.x, pro 3.x je to obdobné, https://docs.python.org/2/tutorial/errors.html):

A try statement may have more than one except clause, to specify
handlers for different exceptions. At most one handler will be
executed. Handlers only handle exceptions that occur in the
corresponding try clause, not in other handlers of the same try
statement. An except clause may name multiple exceptions as a
parenthesized tuple, for example:

… except (RuntimeError, TypeError, NameError):
… pass

Note
that the parentheses around this tuple are required, because except
ValueError, e: was the syntax used for what is normally written as
except ValueError as e: in modern Python (described below). The old
syntax is still supported for backwards compatibility. This means
except RuntimeError, TypeError is not equivalent to except
(RuntimeError, TypeError): but to except RuntimeError as TypeError:
which is not what you want.

Schválně si zkuste experimentovat s následujícím kódem:

import random

class e1(Exception):
    pass

class e2(Exception):
    pass

try:
    if random.choice([True, False]):
        raise e1('message')
    else:
        raise e2('message')
except e1, e2:
    print 'error'

Jedná se o jednu ze záludností Pythonu, na kterou je třeba dávat obzvláš pozor.

IT expert

Pro odchytavani vice vyjimek doporucuji vytvorit si tuple se vsemi vyjimkami a pak chytavat tuto „meta“ vyjimku.

AuthError = ( webmod.auth.UserNotFound, webmod.auth.WrongPassword )
try:
   ...
except AuthError, e:
   ...

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.

Pocta C64

Za prvopočátek své programátorské kariéry vděčím počítači Commodore 64. Tehdy jsem genialitu návrhu nemohl docenit. Dnes dokážu lehce nahlédnout pod pokličku. Chtěl bych se o to s vámi podělit a vzdát mu hold.