Pracujete-li s programovacím jazykem Java na straně serveru, zjistíte, že v některých situacích nelze použít standardní autentizační mechanismy serveru TomCat. Jedná se o situace, kdy je nutné uživatele přihlásit programově. Pokusil jsem se tedy vytvořit a popsat balík tříd, který umožní vyměnit autentizaci založenou na JDBCRealmu za autentizaci pomocí filtrů, bez nutnosti úprav zdrojového textu aplikace.

Představme si situaci, kdy vytváříme webové stránky, které nabízejí nějakou službu. Služba je jen pro registrované uživatele. Registrace je zdarma a zaregistrovat se může každý sám vyplněním registračních údajů. Mezi registračními údaji bude mimo jiné uživatelské jméno a heslo, kterými se bude uživatel po registraci přihlašovat. Uživatel se jednou zaregistruje a poté se kdykoli na naše stránky vrátí, přihlásí se a začne využívat službu. Může se jednat například o free-webhosting, free-email (případně free-cokoli), možná také nějaký internetový obchod, nebo i obyčejný chat či diskusní fórum jen pro registrované.

Pozn. aut.: Při používání TomCatu a Javy na straně serveru (servlety, JSP) můžeme používat k autentizaci uživatelů standardní prostředky. Na Intervalu vychází série Marka Branického Java Servlets, ve které je několik článků věnováno autentizaci. Nebudu se proto standardními prostředky TomCatu zabývat.

Proč ne JDBCRealm?

Má-li aplikace běžet nad WWW serverem TomCat a být vytvářena pomocí servletů nebo JSP (JSP je vlastně jen jiný způsob psaní servletů), asi každého hned napadne použít standardní prostředky TomCatu. V souboru web.xml nakonfigurovat chráněné oblasti a ověřování uživatele provádět pomocí JDBCRealmu proti databázovým tabulkám. Registrační formulář by v chráněné zóně nebyl a servlet zpracovávající data z registračního formuláře by zapsal potřebné údaje do databázových tabulek, které JDBCRealm používá k autentizaci. Takto registrovaný uživatel by se mohl dále přihlašovat.

Je zde ale jeden drobný problém. Uživatel bezprostředně po registraci není přihlášen. Podívejme se na to právě z pohledu návštěvníka. Dostane se na registrační formulář, vyplní registrační údaje (třeba i v několika krocích) a úspěšně se zaregistruje. Nyní chce využívat službu, pro kterou se registroval, ale při přechodu do chráněné zóny se musí přihlásit. Logicky se zeptá, proč má vyplňovat přihlašovací jméno a heslo znovu, když je zadával již při registraci. Po úspěšné registraci přece měl být již přihlášen. Nebo se takto ani ptát nemusí. Může se jednat o úplného laika, který je zmaten, neví co má dělat (je to nějaké divné, když musí zase zadávat něco, co už tam je) a proto odchází.

Protože pohled uživatele by měl být nejdůležitější, je nutné zajistit, aby po registraci došlo k přihlášení uživatele. Tedy najít možnost, jak programově uživatele přihlásit nebo jak v programu „nasimulovat“ přihlášení uživatele. A tím jsme vlastně popsali celý problém. Nic takového udělat nejde.

Zní to dost neuvěřitelně a taky jsem tomu dlouho uvěřit nemohl. Dlouho jsem hledal řešení na internetu a účastnil se různých našich i zahraničních internetových diskuzí. Nakonec jsem došel k závěru, že opravdu programově přihlásit uživatele nelze.

Jak situaci řešit?

Nezbývá nám, než si vytvořit vlastní mechanizmus pro autentizaci uživatele. Při procházení diskusních fór můžeme najít řešení, které nabízí Victor R. Cardona. K řešení problému použil filtry. V příspěvku diskusního fóra je také příloha s balíčkem tříd. Na první pohled by se mohlo zdát, že vše řeší. Na druhý pohled už bohužel ne. Jednak v balíku jedna třída schází (není to zase taková tragédie, protože se jedná o třídu, ve které jsou pravděpodobně jen konstanty), ale hlavně metoda AuthenticationFilter.doFilter mi připadá napsána dost lehkovážně. A navíc neumožňuje připouštět uživatele do chráněných oblastí podle rolí.

Nechme se ale tímto nápadem inspirovat a vytvořme autentizační mechanizmus, který bude splňovat následující požadavky:

  • Bude nezávislý na aplikaci – námi napsané filtry a pomocné třídy by měly být použitelné kdekoliv, kde jde použít JDBCRealm. (Pod pojmem aplikace mám na mysli to, co se obvykle myslí jako objekt aplikace v JSP. Tedy instanci třídy ServletContext.)
  • Aplikace nepozná, že pracuje s našimi třídami místo s třídami JDBCRealmu – bez změny zdrojových textů aplikace půjde vyměnit JDBCRealm za naše řešení a naopak. Rozdíl bude pouze v tom, že při použití JDBCRealmu nebude po registraci uživatel přihlášen. Vše se bude pouze konfigurovat ve web.xml nebo jiných konfiguračních souborech.
  • Naše řešení bude mít stejné možnosti konfigurace jako JDBCRealm – tedy ve web.xml sami rozhodneme, jaké použít databázové tabulky, jaké sloupce v tabulkách, jaký přihlašovací řetězec, které oblasti budou příslušet kterým rolím a podobně.

Je zřejmé, že řešení z emailového diskusního fóra uvedené výše nesplňuje ani jeden bod.

Při výměně našeho řešení za JDBCRealm a naopak budeme muset ve skutečnosti pár velmi drobných úprav provést. Upozorním na ně v dalších článcích. Bude se jednat o úpravu servletu nebo JSP zpracovávající registrační formulář (vložíme řádky zdrojového textu pro přihlášení) a přihlašovací formulář (upravíme atribut „action“ přihlašovacího formuláře.

Princip řešení

Používáme-li standardní autentizační mechanizmus serveru TomCat, všechny servlety v chráněné zóně jsou přístupné jen autentizovaným uživatelům a ve svých metodách pro zpracovávání požadavků dostanou jako parametr instanci typu HttpServletRequest, jejíž metody getAuthType, getRemoteUser, getUserPrincipal a isUserInRole při zavolání vrací informaci o způsobu autentizace nebo informace o uživateli. Nejprve musíme zajistit, aby tyto metody pomocí našeho mechanizmu autentizace vracely správné hodnoty.

Vše bude postaveno na filtrech. Informace o přihlášeném uživateli budeme ukládat v session. Každý příchozí požadavek projde filtrem, který zjistí, jestli pro danou session je uživatel přihlášený. Jestliže ne, přesměruje uživatele na přihlašovací formulář. Dále zkontroluje, jestli uživatel náleží roli, která má přístup k požadovanému dokumentu. Jestliže uživatel není asociován s rolí, která má k dokumentu přístup, vrátí uživateli v odpovědi HTTP kód 403. Další filtr vytvoří novou instanci typu HttpServletRequest (obalí existující požadavek), jejíž metody getAuthType, getRemoteUser, getUserPrincipal a isUserInRole budou vracet informace o přihlášeném uživateli, které požadujeme a očekáváme. Tím sami odfiltrujeme uživatele, kteří nemají k požadovanému dokumentu přístup, a navíc „ošidíme“ cílový servlet tím, že mu předáme HTTP požadavek tvářící se jako požadavek uživatele, který byl autentizován standardními prostředky TomCatu.

Při zpracovávání dat z přihlašovacího formuláře zapíšeme potřebné informace o přihlášeném uživateli do session. Tím bude uživatel přihlášen. Toto „přihlášení“ můžeme provést i po úspěšné registraci.

Principal

Instance typu Principal podle dokumentace reprezentuje nějakou entitu, například jednotlivce, který je přihlášen. Napíšeme třídu implementující rozhraní Principal, která bude uchovávat přihlašovací jméno uživatele a názvy rolí, kterým je uživatel přiřazen. Povinná metoda getName bude vracet přihlašovací jméno uživatele. Metody addRole, setRoles, getRoles a isUserInRole nám umožní pracovat s rolemi. Role budeme uchovávat v množině implementované hash tabulkou (třída HashSet).

Instance naší třídy bude po přihlášení uživatele uložená v session. Metoda getUserPrincipal modifikované instance typu HttpServletRequest, která bude předána servletu, bude vracet právě instanci naší třídy, která je uložená v session.

package authentication;
import java.security.Principal;
import java.util.HashSet;
public class MyPrincipal implements Principal
{
private java.lang.String name;
private java.util.Set roles;
public MyPrincipal()
{
  super();
  roles = new HashSet();
}
public void addRole(String newRole)
{
  roles.add(newRole);
}
public boolean equals(Object obj)
{
  try
  {
    return getName().equals(((Principal)obj).getName());
  }
  catch (ClassCastException ex)
  {
    return false;
  }
  catch (NullPointerException ex)
  {
    return getName() == ((MyPrincipal)obj).getName();
  }
}
public java.lang.String getName()
{
  return name;
}
public java.util.Set getRoles()
{
  return roles;
}
public boolean isUserInRole(String role)
{
  try
  {
    return getRoles().contains(role);
  }
  catch(NullPointerException ex)
  {
    return false;
  }
}
public void setName(java.lang.String newName)
{
  name = newName;
}
public void setRoles(java.util.Set newRoles)
{
  roles = newRoles;
}
}

Nyní musíme zajistit, aby cílovému servletu byla předána instance HttpServletRequest, která má modifikovány některé metody, což si ukážeme v následujících článcích.

Starší komentáře ke článku

Pokud máte zájem o starší komentáře k tomuto článku, naleznete je zde.

Žádný příspěvek v diskuzi

Odpovědět