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

Zdroják » Různé » Java na webovém serveru: autorizace a autentizace II

Java na webovém serveru: autorizace a autentizace II

Články Různé

Dnes budeme pokračovat v tématu zabezpečení aplikace. Od ukládání uživatelů do souboru přejdeme k praktičtější autentizaci vůči databázi. A do naší aplikace doplníme registrační formulář pro nové uživatele.

Uživatelé v databázi

Minule jsme pro jednoduchost uložili databázi uživatelů do souboru – použili jsme k tomu FileRealm. Jelikož je autentizační framework používaný v Javě modulární, snadno přenastavíme naši aplikaci, aby ověřovala uživatele vůči záznamům v databázi.

Jen pro připomenutí: jako obvykle si z Mercurialu stáhneme aktuální verzi zdrojových kódů k dnešnímu dílu seriálu:

$ hg pull
$ hg up "7. díl"

Potřebné databázové tabulky

Nejprve si v databázi vytvoříme tři nové tabulky:

CREATE TABLE uzivatel
(
  id integer NOT NULL DEFAULT nextval('uzivatel_seq'::regclass),
  prezdivka character varying(64) NOT NULL, -- Uživatelské jméno
  heslo character varying(512) NOT NULL,
  jmeno character varying(64),
  prijmeni character varying(64),
  email character varying(255),
  datum timestamp with time zone NOT NULL DEFAULT now(),
  CONSTRAINT uzivatel_pk PRIMARY KEY (id),
  CONSTRAINT uzivatel_prezdivka_uq UNIQUE (prezdivka)
)
…
CREATE TABLE uzivatel_role
(
  prezdivka character varying(64) NOT NULL,
  "role" character varying(16) NOT NULL,
  CONSTRAINT uzivatel_role_pk PRIMARY KEY (role, prezdivka),
  CONSTRAINT uzivatel_role_role_fk FOREIGN KEY ("role")
      REFERENCES "role" (kod) MATCH SIMPLE
      ON UPDATE CASCADE ON DELETE CASCADE,
  CONSTRAINT uzivatel_role_uzivatel_fk FOREIGN KEY (prezdivka)
      REFERENCES uzivatel (prezdivka) MATCH SIMPLE
      ON UPDATE CASCADE ON DELETE CASCADE
)
…
CREATE TABLE "role"
(
  kod character varying(16) NOT NULL,
  popis character varying(255),
  CONSTRAINT skupina_pk PRIMARY KEY (kod)
)

Kompletní definici tabulek naleznete ve zdrojových kódech aplikace. První tabulka obsahuje záznamy uživatelů, jejich hesla a profily. Druhá tabulka slouží k přiřazení rolí jednotlivým uživatelům. Třetí tabulka není nezbytně nutná, ale je vhodné si tento číselník rolí vytvořit. Jednak si k rolím můžeme doplnit vysvětlující poznámku a jednak nám pomůže referenční integrita v tom, aby se nám v aplikaci nevyskytovaly role, které tam být nemají (třeba v důsledku překlepů).

V naší aplikaci přiřazujeme každému uživateli roli bezny a budeme ji vyžadovat pro operace, které může provádět běžný přihlášený uživatel (např. přidávat nové podniky nebo psát komentáře). Pokud bychom někomu chtěli pozastavit účet, stačí mu v databázi tuto roli odebrat a tyto funkce nebude mít dostupné – opět výhoda deklarativního přístupu – nemusíme do kódu aplikace přidávat kontroly, zda uživatel nemá např. ve sloupečku neaktivni_ucet hodnotu true (zaručeně bychom na přidání kontroly alespoň na jednom místě zapomněli).

Úprava nastavení serveru

Po vytvoření tabulek k databázi přistoupíme k nastavení aplikačního serveru – samotnou aplikaci nebude potřeba měnit.

Java - autentizace 2

V Glassfishi jednoduše smažeme původní realm nekurakNET a vytvoříme nový se stejným jménem, tentokráte ale založený na třídě JDBCRealm a vyplníme tyto jeho parametry:

  • JAAS Context: jdbcRealm
  • JNDI: jdbc/nekurak
  • User table: uzivatel
  • User Name Column: prezdivka
  • Password Column: heslo
  • Group Table: uzivatel_role
  • Group Name Column: role
  • Digest Algorithm: SHA-512
  • Encoding: Hex

Většina parametrů asi nepotřebuje vysvětlení. Aplikační server se pokusí najít záznam pro danou přezdívku (uživatelské jméno) v tabulce uzivatel. Následně ověří heslo. A poté přiřadí úspěšně ověřenému uživateli role podle záznamů v tabulce uzivatel_role.

Hesla je samozřejmě možné uchovávat i v čistém tvaru (algoritmus: none), ale vhodnější bude je mít v databázi zahashovaná. Hashovací algoritmus si můžeme vybrat – MD5 (dosud je jako výchozí), nebo SHA-1, SHA-512 a další. Určíme kódování – jelikož výstupem hashovacího algoritmu je pole bajtů, převádějí se tyto bajty na text, který se snadněji ukládá do databáze. Kromě obvyklejšího hexadecimálního kódování můžeme použít i Base64.

Pokud zvolíme SHA-512 a Hex, budou hesla ve stejném tvaru, jaký můžeme získat v příkazové řádce pomocí:

echo -n "nějaké pěkné heslo" | sha512sum
7df16188f807fb…503e6e800fabeafc9bc1413d4dfe0a743e35ca -

Parametr -n je zde důležitý, protože příkaz echo jinak přidává nakonec řetězce znak konce řádku (hash by se tudíž neshodoval s tím, co očekává autentizační modul).

JNDI jméno odkazující na datový zdroj jsme použili stejné, jako používá aplikace. Ale není to pravidlem – např. můžeme chtít zabezpečit aplikaci tak, aby nemohla číst hesla uživatelů (ke své činnosti to totiž nepotřebuje), tudíž bychom si definovali dvě různá databázová spojení (pod různými DB uživateli), jedno pro autentizaci a druhé pro samotnou aplikaci.

Pokud nám standardní způsob ověřování nevyhovuje, můžeme si vytvořit vlastní autentizační modul a použít ho místo JDBCRealm. Např. bychom mohli uživatele ověřovat pomocí uložené databázové procedury (v tom případě by právo na SELECT hesel nemusel mít ani tento autentizační modul – pouze by zavolal proceduru s parametry jméno/heslo a ta by vrátila seznam rolí, kterými uživatel disponuje, nebo vyhodila výjimku při neúspěšné autentizaci). Výhodou je, že jakmile si jednou takový modul napíšeme, můžeme ho používat pro všechny své aplikace na daném serveru.

Zaujaly vás možnosti Javy a chcete se dozvědět o tomto jazyce víc? Akademie Root nabízí školení Základy programovacího jazyka Java a Pokročilejší kurz jazyka Java, na nichž se naučíte, jak tento multiplatformní objektově orientovaný jazyk používat.

Registrační formulář

Se záznamy uživatelů budeme v aplikaci pracovat stejně jako s jinými daty – nad datovou a EJB vrstvou si vytvoříme prezentační vrstvu (JavaBeany a JSP), formulář bude velice podobný tomu na přidávání podniků. Pouze doplníme ještě jeden krok:

  1. Uživatel vyplní registrační formulář a odešle ho.
  2. Zobrazíme mu zadané údaje a on je potvrdí.
  3. Po potvrzení teprve založíme účet.

Uživatel potvrzuje údaje kliknutím na odkaz na stránce. Neodesílá se podruhé formulář a místo toho odkaz obsahuje token (hash náhodného čísla). Data jsou mezitím uložena v objektu na straně serveru (se scope="session"). Jednak si uživatel může zkontrolovat vyplněné údaje a jednak to naši aplikaci ochrání před CSRF. V případě registračního formuláře je ochrana před tímto typem útoku možná zbytečná, ale až budeme psát funkci pro hlasování v anketách, bude se ochrana pomocí tokenu daleko víc hodit – Zabráníme tak útočníkovi ve zmanipulování ankety umístěním odkazu (např. obrázku) na nějakou hodně navštěvovanou stránku.

Pokud bychom k registračnímu formuláři doplnili CAPTCHu Mohli bychom hash v odkazu vypustit (CAPTCHa aplikaci ochrání i před CSRF).

V souboru registrovatUzivatele.jsp si inicializujeme tři objekty:

<jsp:useBean id="uzivatel" class="cz.frantovo.nekurak.dto.Uzivatel" scope="request"/>
<jsp:useBean id="uzivatelPredRegistraci" class="cz.frantovo.nekurak.web.UzivatelPredRegistraci" scope="request"/>
<jsp:useBean id="registraceUzivatele" class="cz.frantovo.nekurak.web.RegistraceUzivatele" scope="session"/>

Objekt uzivatel je uživatelský účet, který bude nakonec uložen do databáze. uzivatelPredRegistraci je instance pomocné třídy, která ho obaluje a stará se o generování náhodného kontrolního tokenu. Tyto dva objekty mají nastavenou platnost jen pro daný HTTP požadavek ( scope="request"). Třetí objekt RegistraceUzivatele je platný pro celou relaci uživatele ( scope="session") a to proto, aby udržel stav (údaje vyplněné do formuláře a nastavené objektu uzivatel), mezi HTTP požadavky (kontrola údajů a následně potvrzení registrace). Uživatel by si totiž mohl otevřít několik registračních formulářů, odeslat je a pak jeden z nich potvrdit.

Za zmínku stojí, že pokud v JSP vyplňujete vlastnosti nějakého objektu (v tomto případě uživatelského účtu) a názvy proměnných v Javě se shodují s názvy POST/GET parametrů odeslaných přes HTTP, není potřeba vlastnosti nastavovat po jedné a lze to udělat hromadně:

<jsp:setProperty name="uzivatel" property="*"/>

Pokud se v budoucnu rozhodneme podmínit registraci ověřením e-mailové adresy (v současnosti není ani povinná, stačí vyplnit přezdívku a heslo), stačí, když nebudeme uživatelům přiřazovat roli bezny automaticky hned po registraci a uděláme to až když uživatel klikne na odkaz v e-mailu. Tento systém je poměrně flexibilní a umožňuje měnit procesy na základě aktuálních potřeb.

Závěr

V dnešním díle jsme si ukázali, jak lze prakticky bez zásahu do aplikace změnit způsob ověřování uživatelů – ověřování oproti SQL databázi místo souboru (který se hodí spíše pro prototypování). A přidali jsme registrační formulář pro nové uživatele. Příště je na řadě trochu teoretičtější díl – porovnání výhod Javy a PHP na webovém serveru.

Odkazy

Komentáře

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

Chtěl bych protestovat proti tokenu v URL. Pokud bude na té stránce nějaký odkaz ven, musíme si dávat bacha na ně, protože mohou token získat z refereru. V tomto případě to asi vadit nebude, protože token se po použití stává asi ihned k ničemu, ale přesto si myslím, že to není dobré takto učit.

v6ak

Zajímavý argument, nicméně bych ho radši viděl přímo ve článku.

v6ak

Proti tomuto nastavování beans bych také něco měl:
* Nerozlišuje to GET a POST.
* Nekontroluje to proti definici formuláře, takže je někdy možné podstrčit i něco navíc.

Druhý bod si lze představit třeba na editaci uživatelského profilu. Jakmile do uživatele přidáme nějaká oprávnění (dobře, na ty jsou spíš role) nebo informace typu VIP/non-VIP, může je uživatel se změnou profilu měnit také.

Právě proto se hodí nějaká knihovna pro formuláře a svázat to s ní.

v6ak

Neříkám, že GET/POST je důležitý bezpečnostní rozdíl (za určitých podmínek teda ano), ale už z principu to není dobré mixovat.

Formulářové knihovny jsou asi až ve frameworcích, že? Ta třída PozadavekNaRe­gistraci je sice možná, ale… …ale i tady je určitá duplikace – jednou je seznam povolených ve formuláři a jednou ve třídě PozadavekNaRe­gistraci. Nějaká formulářová knihovna takovéto data binding přece umožňuje, nebo ne?

kvr

Článek dobrý a autorovi posílám pochvalu za celou sérii.

V tomto dílu mě ale zarazila definice tabulek pro uzivatel_role – referencovat „prezdivka“ místo primárního klíče mi trochu skřípe, zvláště, když nevidím ani žádné zjevné výhody. Je to kvůli nějakému omezení, třeba v rámci definice JDBCRealm?

Tomáš J. Kouba

Děkuji za zajímavý článek. Dodám svůj názor na jeden detail. Osobně se domnívám, že tabulka Role je nesmyslná. Myslím, že je lepší seznam rolí uložit do aplikace, přímo do kódu nebo nastavení webu. Pokud se totiž změní požadavek na role, je asi vždy nutné změnit kód aplikace. Pak mám v databázi zbytečnou tabulku do které kladu zbytečné dotazy.

Mějte se hezky

Petr

Vypadá to hezky, ale zajímá mě, jak to bývá implementované na různých Java hostinzích.

Application-level nastavení bude asi v tom WAR souboru, ale nastavení toho realmu je už nastavení serveru.

Dovolují mi toto běžné Java hostingy taky nastavit ?

baboon

Díky za bezva rady ohledně realmu. Měl bych jeden dotaz, je možné nastavit realm z databáze tak, že v databázi mám pouze jednu tabulku „uzivatel“, který má atribut role? Nějak se mi to bohužel nedaří.

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.