2010. január 27., szerda

Kedvenc Java library-k

Ebben a post-ban szeretném felsorolni azon Java library-ket, keretrendszereket, melyeket legszívesebben használok különböző webes alkalmazások fejlesztése közben. Ezekkel rengeteg tapasztalat gyűlt fel, és mindenkinek bátran ajánlhatom őket. Érdekes lesz visszanézni egy-két év múlva, hogy hol tartanak. Remélem ti is merítetek belőle hasznos ötleteket, és a véleményeteket is várom, hogy milyen eszközöket használtok, az általam használt stack mely elemeit lenne érdemes kicserélni Az eszközöket tetszési sorrendben írom, a kedvenceim elől, azok, melyekkel kényelmes fejleszteni, amelyekben keveset csalódtam.

Spring: Kvázi szabvány keretrendszer dependency injection-nel, inversion of control-lal. A J2EE kiváltására találták ki, annak bonyolultságának mellőzésével. Annak szolgáltatásait nagyrészt biztosítja (talán néha a lokális transzparencia hiányzik egyedül). Ugyan a Java EE 5 már sokkal egyszerűbb, a Spring-ből rengeteg elemet át is emelt, mégis a lassú szabványosítási folyamat miatt a Spring már elhaladt mellette. Legnagyobb előnye, hogy apró lépésekben is bevezethető (pl. több éves projektben pár nap alatt vezettük be a perzisztencia rétegbe), és nem igényel alkalmazásszervert, melyekkel rengeteg rossz tapasztalatom volt. Számomra az egyik legfontosabb szempont fejlesztés közben a nagyon rövid fejlesztési ciklus, azaz hogy egy módosítás max. 30 másodpercen belül kipróbálható legyen. JDBC esetén nagyon hasznos a JDBC abstraction layer, JPA estén inkább a standart JPA API-t használjuk.

Spring MVC: Webes keretrendszer, mely elég magas szintű, értelmes default konfigurációkkal rendelkező, annotációkkal konfigurálható a gyors haladás érdekében, de elég alacsony szintű is, ha kell, pl. a request, session, stb. objektumok is könnyen elérhetőek. Összevetve a JSF és Wicket keretrendszerekkel talán kevésbé objektumorientált, kevésbé komponens alapú, de sajnos olyan tapasztalataink voltak az eddigi megrendelőkkel, hogy képesek a keretrendszerek határait feszegetni. A Spring MVC-vel mindent egyszerűen meg lehet csinálni, amire az amúgy silány HTTP protokoll és HTML formátum, és társai lehetőséget biztosítanak. E mellett a Struts-ot is szeretem, de ez jobban illeszkedik a Spring-hez. Ami külön tetszik, hogy sokszor voltam úgy, hogy dokumentáció nélkül elgondoltam, hogy így kéne működnie, kipróbáltam, és tényleg.

Apache Log4J: Apró és megbízható segítség a naplózáshoz. Ugyan jönnek a trónkövetelők, mint a Logback, SLF4J, azonban nem tudok elképzelni olyan funkciót, amiért váltanék, és még nem is annyira elterjedtek. Erről cikket is írtam.

Apache Velocity: Sablonozásra használjuk. Régebben ez volt a view réteg, de most inkább a JSP, mely jobban megköti az ember kezét. Nem csak alkalmazásokban használjuk, hanem pl. ügyfél számára interfész prototípus, sőt dokumentáció generálására is. Okosabb a FreeMarker, de nem volt még szükség a funkcióira. Erről szóló cikk.

Apache Lucene: Most már mindegyik webes alkalmazásban szükség van a tartalom hatékony keresésére, erre tökéletes. Erről cikket is írtam.

Spring Security: Autorizációra és autentikációra kizárólagosan ezt használjuk. Erről nemrég írtam a Spring Security post-omban.

JSP, JSTL: Szabványos view réteg, aminek ugyan vannak hiányosságai, de rákényszerít a helyes MVC használatra, és az IDE-k is ezt támogatják a legjobban.

Display tag library: JSP tag library táblázatok megjelenítésére. Ami kellett, azt még mind tudta. Akár AJAX bővítménye is van. Van régebbi post erről is.

JUnit: Unit test-ek fejlesztésére, az elterjedtsége miatt. A TestNG is rendkívül szimpatikus, de egyelőre nincs olyan funkció, melyért váltanék, kockáztatva a támogatottságot.

Direct Web Remoting: Kellően alacsony szintű bridge a Java és a JavaScript világ között, AJAX-os funkciók megvalósítására.

jQuery: Bár alapjában véve gyűlölöm a JavaScript-et, ez a library nagyon hasznos segítségnek bizonyult. A Prototype annyira nem hatott meg.

JasperReports és iReport: Riport generálásra használatos library és NetBeans alapú riport tervező eszköz. No ez már jó pár napom megkeserítette, de még mindig ez a legszimpatikusabb. Az XSL-FO általában ágyúval verébre, és iszonyatosan lassú és erőforrás igényes. A másik versenyző a BIRT, amivel nem sok tapasztalatom van, és amúgy is az Eclipse ökoszisztéma tagja.

Hibernate: Az egyik legelterjedtebb ORM megvalósítás. Történelmi okok miatt nem váltottunk az EclipseLink-re, de nem vagyok elkötelezett híve, mert sok nehéz pillanatot szerzett. Kizárólag JPA provider-ként használom.

A listából is látható, hogy olyan eszközöket igyekszem választani, melyek vagy szabványosak, vagy kvázi szabványosak. Egy konkrét problémát célozzanak meg, és elég egyszerűek ahhoz, hogy probléma esetén akár a forrását tanulmányozva, vagy debug-olva előrébb lehessen jutni (az nyílt forráskódú szoftverek legnagyobb előnye, ha már a dokumentációjuk hagy némi kívánnivalót maga után). Jelentős forrásanyag (tutorial, projekt reports, dokumentáció, példakódok, cikkek, könyvek), felhasználótábor (hírek, fórum, levelezési lista, issue tracker) legyen körülötte. Külön fontos, hogy nagyon egyszerűen, lépésekben bevezethető, könnyen tanulható legyen, hogy hamar sikerélményt biztosítson. És szép legyen a weboldala.

A következő post-ban az általam leggyakrabban használt Java-s tool-okat, eszközöket fogom bemutatni.

Ti milyen library-ket, keretrendszereket javasoltok? Főleg olyanok érdekelnek, melyeket éles projektben használtatok, beváltak, újabb projektekben is bevetnétek, és több hónap után is szívesen nyúltok hozzá vissza.

2010. január 18., hétfő

Struts felviteli es szerkesztő képernyő

A legtöbb projektünkben ugyan Spring MVC-t használunk, de van egy-két régebbi alkalmazás, melyet Struts-ban (még nem Struts 2-ben) fejlesztünk. Gyakran vissza kell hozzá nyúlni, és mindig keresgélnem kell, hogy hogyan is kell a képernyőket felépíteni, ígyhát inkább leírom ide, hátha másnak is hasznára válik.

Ezen probléma ismerős lehet más webes keretrendszereknél is, és lehetséges, mint pl. a Spring MVC esetén, hogy sokkal szofisztikáltabb megoldást adnak rá, mégis érdemes az alapelvekkel tisztában lenni. A bonyolultabb, komponens alapú keretrendszereknél, mint JSF vagy Wicket, már kevésbé kell ennyire a kérésekkel/válaszokkal foglalkozni.

Egy CRUD (Create-Read-Update-Delete) alkalmazást tipikus képernyőfolyama, mikor a felhasználó a listázó képernyőről indul. Itt vagy új tételt vesz fel, vagy kiválaszt egy tételt módosításra, vagy töröl egy tételt. Az első két esetben mindenképp megjelenik egy űrlap (form), az első esetben üresen, módosítás esetén már feltöltve adatokkal. Az űrlapot elküldve megtörténik a mentés, visszakerül a felhasználó a listázó képernyőre, valamilyen üzenettel.

A szakirodalom ezt DataEntryForm-nak nevezi. Két fázisból áll, ahol az első fázis az űrlap megjelenítése (render/output/setup phase), a második fázis az elküldött űrlap feldolgozása (post/submit phase). Az első fázis az alapján dönti el, hogy új felvitel, vagy módosítás van, hogy URL paraméterben kapott-e valamilyen tétel azonosítót (id). Nézzük a felviteli/módosító képernyőt, hogy hogyan érdemes megvalósítani.

A következő követelményeket sorolhatjuk fel:

  • Az oldal http GET-re, ha nem kap id-t, adja vissza az űrlapot
  • Az oldal http GET-re, ha id-t kap, kitöltve visszaadja az űrlapot
  • Mindkét esetben a megjelenítendő, űrlapot tartalmazó oldalt fel kell készíteni, feltölteni dinamikus adatokkal
  • Az oldal http POST-ra (az űrlap elküldésekor, POST metódust használunk, hiszen a szerver oldalon történik üzleti adat változtatás) lefuttatja az ellenőrzéseket
  • Amennyiben az ellenőrzés nem sikerül, újra kapjuk vissza az űrlapot, és különösen fontos, hogy a kitöltött értékekkel, a hibásakat megjelölve
  • Amennyiben az ellenőrzés sikerül, a redirect after post elv szerint átirányítás történjen egy másik oldalra, amin meg kell jeleníteni a művelet eredményét (sikeres mentés)

Sajnos a Struts nem ad arra tanácsot, hogy hogyan lehetne ezen összefüggő oldalakat egyszerűen megvalósítani, inkább az oldalakat külön egységnek tekinti.

A Struts belső logikáját és osztályait kevésbé ismerve a feladatot megoldhatjuk úgy, hogy külön Action-t veszünk fel az űrlap alapértékekkel való feltöltésére, és külön Action-t veszünk fel, ami a POST-ot feldolgozza, két külön URL-len. Akár mindegyikhez külön ActionForm-ot is készíthetünk.

Ennek a megközelítésnek több problémája is van:

  • Két Action osztály, a logika szétszórva, két külön URL, két bejegyzés a struts-config.xml-ben.
  • A legtöbb probléma az adatok ellenőrzésénél van. Ha az ellenőrzés elbukik, és a Struts autovalidation van bekapcsolva, az csak JSP oldalra tud vinni. Ebben az esetben viszont nem hívódik meg az a kód, ami az űrlap előkészítésekor az első Action-ben van. Ilyenek pl. azon elemek feltöltése, melyek nem szerepelnek a Form-ban. Az űrlap az első fázisban más URL-en szerepel, mint hibás esetben.

Ahhoz, hogy hatékonyabb megoldást találjunk, a következőket érdemes megfogadni:

  • Használjuk az EventActionDispatcher osztályt. Ezt használva egy Action-be több metódust is implementálhatunk, és a Dispatcher a struts-config.xml-ben lévő parameter attribútum értéke alapján a megfelelő metódust hívja meg. Pontosan ez úgy történik, hogy definiálhatjuk, hogy amennyiben egy paraméter szerepel a kérésben, vagy egy megadott paraméter a megadott értéket veszi fel, mely metódus kerüljön meghívásra. Különösen több gomb esetén érdemes használni.
  • Az űrlap kizárólag az ActionForm adatai alapján töltődjön fel, nem érdemes mindenféle request vagy session attribútumokat használni. Használhatjuk a ActionForm-ot az első fázis paramétereinek értelmezésére is, pl. a tétel azonosítójának tárolására. Minden esetben a Struts példányosítja az ActionForm-ot, ha a struts-config.xml-ben szerepel a name attribútum. Ezt a példányosított ActionForm-ot tölthetjük fel utána értékkel.
  • Az autovalidation-t ajánlott kikapcsolni, és manuálisan hívni az ellenőrzést. Az autovalidation során az irányítás kicsúszik a kezünkből, és hiba esetén a struts-config.xml-ben megadott input attribútumban definiált JSP-re kerülhet csak a vezérlés. Ha mi végezzük az ellenőrzést, akkor dönthetünk másképp is, a legtöbb esetben nem elég a JSP, a hozzá tartozó Action-t is le kell futtatnunk.
  • Amennyiben az ellenőrzés elbukik, vissza kell irányítani a felhasználót az űrlapra. Ajánlott az Action-re, és nem a view-ra (JSP), így az Action elő tudja készíteni az űrlapot tartalmazó oldalt. Az Action a getErrors metódussal tudja eldönteni, hogy a hiba ágon van-e épp a vezérlés. Az átirányítás történhet forward-dal, de történhet redirect-tel is. Az előbbi esetén a felhasználó ha frissít a böngészőjében, az megerősítést fog kérin, az újra elküldésnél. Ez szerintem belefér, hiszen nem szokás egy ellenőrzés eredményét újratölteni. Ha mégis, használhatunk redirect-et is, de itt az ellenőrzés eredményét vagy URL-ben, vagy session-ben át kell adni. Ezeket a technikákat action reloading-nak nevezik.
  • Sikeres esetben mindig egy Action-re redirect-áljunk. A redirect after post elv miatt is hasznos (a böngészőben való frissítés esetén így nem küldi el újra az űrlapot, és a böngésző sem tesz fel kérdéseket, hogy újra akarjuk-e küldeni). Ezen kívül érdemes betartani az alapszabályt, hogy mindegyik view-hoz (JSP) tartozzon saját, dedikált Action, de egy Action-höz több view is tartozhat. Így egy 1:n kapcsolat alakul ki, nem érdemes egy view-hoz több Action-t is rendelni, hiszen az m:n kapcsolat már jóval átláthatatlanabb, és az Action-ökben kódduplikáláshoz vezethet.
  • Az előző következménye, hogy valahogy át kell a művelet eredményét adni a redirect során. Ez történhet URL paraméterrel (ActionForward használatával), vagy pl. a Struts képes az ActionMessages objektumot session-be is menteni, majd a standard módon, tag-gel onnan elővenni.

Figyeljünk arra, hogy a törlés is POST legyen, hiszen üzleti adatokat módosít.

Nézzünk is meg egy példát, amikor van egy Employee osztályunk, és egy olyan Action-t szeretnénk írni, mely képes vagy új Employee felvitelére, vagy létező Employee módosítására.

Nézzük az Employee és ActionForm osztályokat:

public class Employee {
private Long id;

private String name;

// Getter és setter metódusok
}

public class EmployeeForm extends ActionForm {
private Employee employee = new Employee();

private String saveButton;

@Override
public ActionErrors validate(ActionMapping mapping, HttpServletRequest request) {
   ActionErrors errors = new ActionErrors();
 if (StringUtils.isEmpty(employee.getName())) {
  errors.add("employee.name", new ActionMessage("empty_name"));
 }
 return errors;
}

// Getter és setter metódusok
}

Itt azt a trükköt érdemes megfigyelni, hogy az Employee osztályt tartalmazó EmployeeForm-ot hoztunk létre, és ilyenkor az űrlapban a employe.id és employe.name nevekkel hivatkozhatunk annak mezőire. A validate metódus hibát ad vissza, ha a név üres.

Majd nézzük az Action osztályunkat:

public class EmployeeAction extends Action {
private final static String VIEW = "view";
private final static String ERROR = "error";
private final static String SUCCESS = "success";

@Resource
private EmployeeService employeeService;

private ActionDispatcher dispatcher =
       new EventActionDispatcher(this);
 
@Override
public ActionForward execute(ActionMapping mapping, ActionForm form,
       HttpServletRequest request, HttpServletResponse response)
       throws Exception {
   if (getErrors(request) != null && !getErrors(request).isEmpty()) {
       return view(mapping, form, request, response);
   }
 else {
  return dispatcher.execute(mapping, form, request, response);
 }
}

public ActionForward view(ActionMapping mapping,
  ActionForm form,
       HttpServletRequest request,
       HttpServletResponse response)
       throws IOException, ServletException {
 if (((getErrors(request) == null) || getErrors(request).isEmpty()) &&  ((EmployeeForm) form).getEmployee().getId() != null) {
  ((EmployeeForm) form).setEmployee(employeeService.findEmployeeById());
 }
 return mapping.findForward(VIEW);
}

public ActionForward save(ActionMapping mapping,
       ActionForm form,
       HttpServletRequest request,
       HttpServletResponse response)
       throws IOException, ServletException {
 ActionErrors errors = form.validate(mapping, request);
 if (!errors.isEmpty()) {
       saveErrors(request, errors);
       return mapping.findForward(ERROR);
   }
 else {
  employeeService.saveEmployee(((EmployeeForm) form).getEmployee());
  ActionMessages messages = new ActionMessages();
  messages.add(null, new ActionMessage("successful_save"));
  saveMessages(request.getSession(), messages);
  return mapping.findForward(SUCCESS);
 }
}
}

Az EmployeeAction az Action leszármazottja, így implementálnia kell az execute metódust. Ennek az első sora megvizsgálja, hogy hiba ágon kerültünk-e erre az Action-re. Ha igen, a view() metódust hívja. A továbbiakban használja az ActionDispatcher-t, hogy eldöntse, hogy melyik metódust kell meghívni, ha nem hiba ágon vagyunk. A hiba ágat azért nem bízhatjuk rá, mert a Form itt ki lesz töltve, hiszen az ellenőrzés ezen futott, és ezért a save() metódusra vinne.

A view() metódus egyszerűen megvizsgálja, hogy a hiba ágon van-e. Ha nem a hiba ágon van, és kapott paraméterben azonosítót, akkor betölt egy Employee példányt, és beteszi az ActionForm-ba.

A save() metódus meghívja manuálisan az ellenőrzést, hiszen az autovalidate ki van kapcsolva, és ha hibát talál, akkor az "error" nevű átirányításra visz (ez nem a JSP, hanem az Action lesz a view ágon). Ha nem talál hibát, elmenti az ActionFormba lévő Employee-t, és a session-be tesz egy üzenetet a mentés sikerességéről.

Az Action-höz tartozó struts-config.xml részlet:

<form-bean name="EmployeeForm" type="jtechlog.EmployeeForm" />
...
<action path = "/employee"
name = "EmployeeForm"
type = "jtechlog.EmployeeAction"
parameter = "saveButton=save, default=view"
validate = "false"
scope = "request"
>
<forward name="view" path="/WEB-INF/jsp/employee.jsp" />
<forward name="error" path="/employee.do" />
<forward name="success" path="/listEmployees.do" redirect="true" />
</action>

Ez a konfiguráció definiálja az EmployeeAction Action-t, mely a /employee.do címen érhető el új felvitel esetén, és /employee.do?id=1 címen módosítás esetén. Amennyiben a lekérés esetén a saveButton paraméter ki van töltve, az EventActionDispatcher a save metódusra fog vinni, egyébként a view metódusra. (Ide akár bármennyi metódust felsorolhatnánk.) Az autovalidation ki van kapcsolva. Az ActionForm-ot elegendő a request-be tenni, így erőforráskímélő is, mert nem kell a session-be tenni.

Az employee.jsp állítja elő az űrlapot. Hibás esetben önmagát hívja meg újra az Action. Sikeres esetben egy átirányítás történik a redirect after post alapján, mely az üzenetet is meg fogja jeleníteni.

És végül lássuk a JSP részletet:

<html:form method="post" action="employee.do">
<html:errors />

<html:hidden property="employee.id" />
<html:text property="employee.name" />
<html:submit property="saveButton">Save</html:submit>
</html>

A teljes hívási láncot a következő szekvenciadiagram mutatja, mely három részből áll. Első az űrlap lekérése (felvitel vagy módosítás), második az űrlap elküldése hibákkal és a harmadik az űrlap elküldése hibák nélkül.

Látható, hogy a Struts gyermekbetegséggekkel szenved, melyből sokat tanultak a későbbi keretrendszerek fejlesztői.

2010. január 10., vasárnap

Spring Security

Technológiák: Spring 3.0.6, Spring Security 3.0.7

A Spring Security egy Apache license alatt futó projekt Java alkalmazások autentikációjának és autorizációjának megvalósítására. Az előbbi azt jelenti, hogy a felhasználó tesz egy állítást, hogy ő kicsoda, és azt bizonyítja is. A legtöbbször ez felhasználónév és jelszó párossal történik, de lehet bonyolultabb megoldás, mint tanúsítvány (akár hardver token-en), ujjlenyomat, stb. Az utóbbi az erőforráshoz való hozzáféréskor ellenőrzi, hogy a felhasználónak van-e hozzá jogosultsága. A Spring Security független projektként indult Acegi Security néven, de azóta már a SpringSource tartja karban. Legkönnyebben Spring-es alkalmazásokkal integrálható, de nem kötelező a Spring használata. Persze az összes Spring-es technológiához illeszthető. Főleg webes alkalmazásoknál szokták használni, de működik vastag klienses környezetben is. Ez alapján egyszerűen beépíthető egy Spring + Spring MVC alkalmazásba, de használható többek között Struts-cal, Swing-gel, de gyakorlatilag bármilyen Java alkalmazásban.

Előnye, hogy nem függ a környezettől (pl. alkalmazásszerver), nem kell az üzleti logikát átfűzni a jogosultságkezelést végző kóddal (, hanem aspektusorientált módon adható meg). Egyszerű módon (XML-lel) konfigurálható, és a legtöbb beállításnak van alapértelmezett értéke is, mellyel működik a biztonság, de tetszőleges mértékben testre szabható, a legtöbb osztály akár saját implementációra is kicserélhető (plugin-elhetőség). Implementálva van benne hozzáférési listák kezelése (Access Control Lists).

Támogatja a HTTP BASIC, HTTP Digest és form alapú autentikációt, valamint az OpenID-t és a X.509 tanúsítványt.

A felhasználók és a hozzá kapcsolódó szerepkörök tárolhatóak properties vagy XML állományban, adatbázisban, LDAP-ban, de saját implementáció is megadható. Támogatja a jelszó kódolását pl. SHA vagy MD5 algoritmussal. A felhaszálóval kapcsolatos információkat képes cache-elni is. Különböző eseményekre eseménykezelőket lehet aggatni, pl. bejelentkezés, így könnyen megoldható pl. audit naplózás. Könnyen illeszthető a CAS single sign on megoldáshoz.

Kompatibilis a Servlet Security API-val, használhatóak vele az EJB 3 annotációi, valamint a WSS-hez(korábban WS-Security) is illeszhető. Képes a security propagation-re, azaz az alkalmazások különböző rétegei között átvinni a security context-et (pl. a vastag kliensről a szerverre).

Webes környezetben egy filter-t kell a web.xml-be betenni. Képes mindarra, amire a web.xml-ben definiálható biztonság, de azt rengeteg egyéb funkcióval egészíti ki, mint pl. a védett URL-eket nem csak a Servlet specifikációban megadott korlátozott URL mintákkal lehet megadni, hanem használható az Ant féle megadási mód is. Konfigurálható, hogy védet tartalmak esetén történjen https-re átirányítás. Alapból implementálva van benne két Remember-Me (Persistent Login) megoldás is, azaz a böngésző cookie-ban jegyezze meg a bejelentkezés tényét. A Spring Security tag library-t is biztosít funkcióinak elérésére JSP oldalból.

Ebben a post-ban egy egyszerű Spring MVC-s webes alkalmazásba illesztését fogom bemutatni. A posthoz egy példa projekt is tartozik, mely elérhető a GitHub-on, és a teljes forrás akár egy zip fájlban is letölthető. Egyszerű Spring MVC-s webes alkalmazás JPA perzisztens réteggel.

Első lépésként szerkesszük meg az web.xml állományt, és adjuk meg a Spring Security-t konfiguráló applicationContext-security.xml állományt (a jó elkülöníthetőség kedvéért konfigurálom külön állományban), valamint a filter-t, mely a http(s) kéréseket elkapja, és ellenőrzi.

<context-param>
<param-name>contextConfigLocation</param-name>
<param-value> 
 WEB-INF/applicationContext.xml
 WEB-INF/applicationContext-security.xml
</param-value>
</context-param>

<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>

<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

A következő lépésben írjuk meg az applicationContext-security.xml állományt.

<beans:beans xmlns="http://www.springframework.org/schema/security"
            xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
            http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.0.xsd">
 
    <http auto-config="true">
        <intercept-url pattern="/" access="ROLE_USER, ROLE_ADMIN" />
  <intercept-url pattern="/addUser.html" access="ROLE_ADMIN" />
        <logout />
    </http>
 
    <authentication-manager>
        <authentication-provider>
            <password-encoder hash="md5"/>
            <user-service>
                <user name="jtechlog" password="26b91b96e2e8adc37cd26cff6a6b2eba" authorities="ROLE_USER" />
            </user-service>
        </authentication-provider>     
    </authentication-manager>
</beans:beans>

Az auto-config tulajdonság egy rövidítés, a következő alapértelmezett beállításokat tartalmazza:

<http>
  <form-login />
  <http-basic />
  <logout />
</http>

Az authentication-provider elemben az XML szerepel egy "jtechlog" nevű felhasználó, akinek a jelszava MD5-tel kódolva szerepel ("jtechlog12"). Ezzel készen is van. Az alkalmazásunkat elindítva bármelyik URL-re egy (Spring Security által generált) bejelentkezési form jön be, hiszen deklarálva lett, hogy a / URL megtekintéséhez a felhasználónak rendelkeznie kell a ROLE_USER vagy ROLE_ADMIN szerepkörrel, a /addUser.html-hez ROLE_ADMIN szerepkörrel (lásd intercept-url elem). Az azonosítás form-on, jelszóval történik (form-login).

A security névtérban a következőkre adhatunk meg konfigurációkat:

  • Web/HTTP Security
  • Business Object (Method) Security
  • AuthenticationManager
  • AccessDecisionManager
  • AuthenticationProviders
  • UserDetailsService

Amennyiben kijelentkezést is meg akarunk valósítani, a JSP-ben csak helyezzük el a következő linket:

<a href="<c:url value='/j_spring_security_logout'/>">Kijelentkezés</a>

Következő lépésként implementáljuk magunk a felhasználó adatbázisból való betöltését, méghozzá pl. JPA segítségével. Ehhez kell egy User entitás, melynek különlegessége, hogy implementálnia kell a UserDetails interfészt, és annak több metódusát. Pl.:

@Entity
public class User implements UserDetails, Serializable {

   @Id
   @GeneratedValue
   private Long id;

   private String username;

   private String password;
   
   private String roles;

    @Override
    public Collection getAuthorities() {
        Collection authorities = new ArrayList();
        for (String s: roles.split(", ")) {
            authorities.add(new GrantedAuthorityImpl("ROLE_" + s.toUpperCase()));
        }
        return authorities;
    }    

   @Override
   public String getPassword() {
       return password;
   }

   @Override
   public String getUsername() {
       return username;
   }

   @Override
   public boolean isAccountNonExpired() {
       return true;
   }

   @Override
   public boolean isAccountNonLocked() {
       return true;
   }

   @Override
   public boolean isCredentialsNonExpired() {
       return true;
   }

   @Override
   public boolean isEnabled() {
       return true;
   }

   // Többi getter és setter metódus
   // ...
}

Valamint definiáljunk egy UserService nevű @Repository osztályt, és a trükk csak annyi, hogy implementálnia kell a UserDetailsService interfészt.

@Repository("userService")
@Transactional
public class DefaultUserService implements UserDetailsService {

   @PersistenceContext
   private EntityManager em;

   @Override
   public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException {
       try {
           return (UserDetails) em.createQuery("select u from User u where u.username = :username").setParameter("username", username).getSingleResult();
       }
       catch (EntityNotFoundException enfe) {
           throw new UsernameNotFoundException("A felhasznalo a megadott felhasznalonevvel nem talalhato: " + username, enfe);
       }
   }
   
   // Többi üzleti metódus
   // ...
}

Mivel az applicationContext.xml-ben context:annotation-config van beállítva, ami a @Repository annotáció miatt példányosítja a DefaultUserService osztályunkat.

A Spring Security-ben az AuthenticationProvider is cserélhető, és ebben az esetben a DaoAuthenticationProvider-t kell használnunk. Ennek megadhatunk egy userDetailsService tulajdonságot, melynek a UserDetailsService-t kell implementálnia, és ennek fogja meghívni a loadUserByUsername metódusát. Ezt egy rövidebb konfigurációval is megadhatjuk az applicationContext-security.xml állományban a következő módon:

<authentication-provider user-service-ref="userService" />

Egy authentication-manager-en belül több authentication-provider-t is megadhatunk. Ekkor sorban nézi végig a provider-eket, és ahol először sikerül az autentikáció, az nyer. Így előbb az XML-ben szereplő felhasználókat, majd az adatbázisban szereplő felhasználókat fogja alapul venni, a User entitásunk alapján. Ekkor a jelszó még plain text-ben kerül letárolásra, de ha mi MD5-öt szeretnénk, konfiguráljuk így:

<authentication-provider user-service-ref="userService">
   <password-encoder hash="md5"/>
</authentication-provider>

A Java kódból ezután a következőképpen kérhetjük le a bejelentkezés után a felhasználót:

SecurityContextHolder.getContext().getAuthentication().getPrincipal();

A Context ThreadLocal változó, így szálanként egyedi, webes környezetben nem kell session-ben eltárolni, ezt elvégzi helyettünk a Spring Security. A metódus visszatérési értékét kényszeríthetjük a saját User osztályunkra.

JSP-ben használhatunk tag library-t is, melynek definíciója:

<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags" %>

Az authentication tag visszaadja az Authentication objektumot, és annak tulajdonságait tudjuk lekérni:

<security:authentication property="principal.username" />

Valamint az authorize tag törzse csak a feltétel teljesítésekor jelenik meg. A feltételek a következők lehetnek: ifAllGranted - vesszővel megadott szerepkörök mindegyikével rendelkezik, ifAnyGranted - vesszővel megadott szerepkörök egyikével rendelkezik, ifNotGranted - vesszővel megadott szerepkörök egyikével sem rendelkezik.

<security:authorize ifAllGranted="ROLE_ADMIN">
       <!-- Felhasználók felvételére szolgáló form. -->
</security:authorize>

Ez esetben még mindig nem vagyunk megelégedve a Spring Security által biztosított alapértelmezett bejelentkező képernyővel, emiatt szabjuk azt testre. Az intercept-url-lel kell megadni a védendő URL-eket. Természetesen többet is megadhatunk, egy URL-hez több szerepkört is megadhatunk vesszővel elválasztva, valamint használhatunk Ant típusú mintákat. A Spring Security használatakor a leggyakoribb hiba, hogy a bejelentkezési képernyőt is letiltjuk, így végtelenciklus alakulhat ki. Erre a Spring Security egy üzenettel figyelmeztet is: org.springframework.security.config.FilterChainProxyPostProcessor: Anonymous access to the login page doesn't appear to be enabled. This is almost certainly an error. Please check your configuration allows unauthenticated access to the configured login page. (Simulated access was rejected: org.springframework.security.AccessDeniedException: Access is denied).

Ekkor be kell állítani, hogy a login.html oldalhoz ne kelljen bejelentkezés. Sikertelen bejelentkezés esetén történjen átirányítás a /login.htm?login_error=1 oldalra, sikeres bejelentkezés esetén a / oldalra. Kijelentkezés után ismét a /login.htm oldal jön be. A konfiguráció a következőképpen alakul:

<http auto-config="true">
 <intercept-url pattern="/login.html" access="IS_AUTHENTICATED_ANONYMOUSLY" />
 <intercept-url pattern="/" access="ROLE_ADMIN, ROLE_USER" />
 <intercept-url pattern="/addUser.html" access="ROLE_ADMIN" />
 <form-login login-page="/login.html" default-target-url="/" authentication-failure-url="/login.htm?login_error=1" />
 <logout logout-success-url="/login.html"/>
</http>

Majd nézzük a bejelentkező form-ot tartalmazó JSP részletet:

<c:if test="${not empty param.login_error}">
Sikertelen bejelentkezés
</c:if>

<form action="<c:url value='/j_spring_security_check'/>" method="POST">
<input type="text" name="j_username" value='<c:if test="${not empty param.login_error}"><c:out value="${SPRING_SECURITY_LAST_USERNAME}"/></c:if>'/>
<input type="password" name="j_password" value="" />
<input type="submit" value="Bejelentkezés"/>
</form>

A form-ot a j_spring_security_check címre kell post-olni, amit a filter fogad. Tartalmaznia kell egy j_username és j_password mezőt. Amennyiben nem sikerült a bejelentkezés, a session-ben két változó lesz: SPRING_SECURITY_LAST_EXCEPTION a kivételt tartalmazza, a SPRING_SECURITY_LAST_USERNAME pedig a beírt felhasználónevet.

Ezen kívül a Spring Security képes arra is, hogy különböző metódusok meghívása esetén is végezzen jogosultságellenőrzést. Ezt deklaratív módon, annotációval is meg lehet adni. Ekkor egyrész deklarálni kell, hogy metódus szintű hozzáférés ellenőrzést szeretnénk, ekkor a következőt kell elhelyezni a applicationContext-security.xml-ben:

<global-method-security pre-post-annotations="enabled" />

Valamint használjuk a @PreAuthorize annotációt a védendő metóduson:

@PreAuthorize("hasRole('ROLE_ADMIN')")
public void addUser(String name, String password, String roles) {
  // ...
}

Látható, hogy már minimális konfigurációval is működik a Spring Security, és minden elemét ki tudjuk cserélni saját implementációra is.

2010. január 3., vasárnap

Ant listener és logger osztályok

Az Ant (, a bejegyzés írásakor a legfrissebb verzió a 1.7.1) lehetőséget biztosít arra, hogy futását monitorozzuk, és a folyamat különböző eseményeihez különböző műveleteket rendeljünk. Ehhez a BuildListener és BuildLogger interfészeket kell implementálni, az abban definiált metódusokat megvalósítani, és az Ant-ot úgy indítani, hogy igénybe vegye ezeket. Ezekről a kézikönyvön kívül a Manning kiadónál megjelent Erik Hatcher, Steve Loughran: Java Development with Ant könyv 20.2 Listeners and loggers című fejezete is részletesen ír.

Ezekből léteznek az Ant-ban már implementációk, de írhatunk sajátokat is egyszerű időmérésre, saját naplózás megvalósítására, de akár bonyolultabb műveletekre is, mint pl. IDE fejlesztésekor a fejlesztőeszközzel való kapcsolattartásra, vagy ha az Ant-hoz grafikus felületet fejlesztünk, ezek adhatnak hírt a build folyamat pillanatnyi állásáról.

A BuildListener interfész leszármazottja a BuildLogger interfész, ahogy a következő UML osztálydiagram is mutatja.

A BuildListener interfészben a build folyamat különböző lépéseinek elindításához és elvégzéséhez is tartozik egy metódus, melyet az Ant hív meg. Így meghívja a build folyamat indításakor a buildStarted() metódust, és a végén a buildFinished() metódust. Ugyanígy vannak metódusok a target és a task futtatásához is. Mindegyik paramétere a BuildEvent osztály egy példánya, melytől le lehet kérni az éppen feldolgozás alatt álló projektet (BuildEvent.getProject()), target-et (BuildEvent.getTarget()) és task-ot (BuildEvent.getTask()). Természetesen aminek nincs értelme, null-t ad vissza, pl. a buildStarted() esemény esetén a target és a task még null. Külön megjegyzendő, hogy a build folyamat kezdetekor, mikor esemény generálódik (buildStarted() metódus hívásakor) még nem dolgozta fel a build.xml állományt, így a BuildEvent.getProject() is null-t fog visszaadni. Ezen események bekövetkeztekor a BuildEvent.getException() metódussal a kivételt is lekérdezhetjük. A messageLogged() metódus akkor hívódik meg, mikor az Ant üzenetet naplóz. Ekkor az üzenetet a BuildEvent.getMessage() metódussal tudjuk lekérni, és az üzenet prioritását a BuildEvent.getPriority() metódussal. Fontos, hogy a messageLogged() metódusban ne használjuk közvetlen System.out, vagy System.err stream-ekre írást, mivel az Ant úgy működik, hogy ezen stream-ek felett átveszi az irányítást, és ami ezekre kiírásra kerül, azt adja tovább a BuildListener-nek. Így ha ebből ezekre a stream-ekre írunk, végtelenciklus lesz a vége. Az inicializációs kódot javasolt a konstruktorban elhelyezni.

Írjunk is meg egy egyszerű BuildListener-t, mely azt méri, hogy mely target futása mennyi ideig tartott, névvel együtt.

package jtechlog.ant;

import org.apache.tools.ant.BuildEvent;
import org.apache.tools.ant.BuildListener;

public class MeasureBuildListener implements BuildListener {
private long startedAt;
public void buildStarted(BuildEvent be) {
}

public void buildFinished(BuildEvent be) {
}

public void targetStarted(BuildEvent be) {
  startedAt = System.currentTimeMillis();
}

public void targetFinished(BuildEvent be) {
  System.out.println("A " + be.getTarget().getName() + " target futási ideje: " + (System.currentTimeMillis() - startedAt) + " ms");
}

public void taskFinished(BuildEvent be) {
}

public void taskStarted(BuildEvent be) {
}

public void messageLogged(BuildEvent be) {
} 
}

Ahhoz, hogy ezt le is futtassuk, a saját osztályunkat el kell helyezni az Ant classpath-jában, melyre a legegyszerűbb megoldás a -lib kapcsoló használata. Ezen kívül a saját osztályunkat meg kell adni indítási paraméterként a -listener kapcsolóval. Azaz a parancssor, ha az osztályunk a lib/jtechlog-listeners.jar fájlban van, indítsuk így az Ant-ot:

ant -lib lib -listener jtechlog.ant.MeasureBuildListener

Ekkor a classpath-hoz a lib könyvtárban található összes jar állományt hozzá fogja adni, és így már megtalálja a jtechlog.ant.MeasureBuildListener osztályt is.

A BuildLogger interfész annyival egészíti ki a BuildListener-t, hogy képes hozzáférni a standard output-hoz, valamint error-hoz. Ezen kívül megkapja a naplózás szintjét, valamint a emacs módot. Indítani a -logger kapcsolóval lehet. Ha ilyent nem adunk meg, a BuildLogger interfészt megvalósító DefaultLogger osztály fog elindulni. Ez egyrészt a naplózás szintje alapján szűri az üzeneteket, valamint az emacs mód is használható, mely arra való, hogy az IDE-k ezt a naplóformátumot könnyen tudják feldolgozni. Egy Ant projekthez csak egy logger kapcsolható, hiszen direkt hozzáférése van az output és error stream-hez. A -emacs kapcsolóval állítható az emacs mód, és a naplózás szintje pedig a -quiet (kevés napló), -verbose (több napló) és -debug (még több napló) kapcsolókkal. Előfordulhat olyan eset is, mikor még a BuildLogger nem kapja meg az üzeneteket, pl. hibás inicializáció esetén, ha hiányzik a build.xml állomány. Ilyenkor az üzenet a konzolra vagy fájlba mehet.

Több beépített naplózó is van, érdemes ezeket is megvizsgálni, saját írása esetén ezek forráskódját is:

  • DefaultLogger: alapértelmezett naplózó
  • NoBannerLogger: nem írja ki a target-ek neveit
  • MailLogger: e-mail-t küld a build befejezésekor, a különböző beállítások property-kkel adhatóak meg
  • AnsiColorLogger: a különböző üzeneteket színkódokkal együtt írja ki, melyeket pl. az XTerm és a Win9x Console is tud értelmezni. A színkódokat felül is lehet definiálni egy properties fájlban, melynek helye property-ben adható meg.
  • Log4jListener (ant-apache-log4j.jar állományban): nagyon hasznos naplózó, képes a Log4J-t használni naplózáshoz (ha benne van a CLASSPATH-ban), és ekkor egy log4j.properties állománnyal konfigurálhatjuk, és kihasználható a Log4J teljes funkcionalitása, mint a Layout-ok, Appender-ek, stb.
  • XmlLogger: naplózás XML-be
  • TimestampedLogger: kiírja az időpontokat is
  • BigProjectLogger: nagy projekteknél alkalmazható, pl. minden task nevénél kiírja a projekt nevét is. Ennek akkor van értelme, ha egy build.xml a subant task-kal egy másik build.xml állományt hív meg.
  • CommonsLoggingListener (ant-commons-logging.jar állományban): a Commons Logging-ot használja naplózáshoz, ami meg a Log4J-t, ha benne van a classpath-ban

Ezen naplózók is a -logger kapcsolóval használhatóak, pl.

ant -logger org.apache.tools.ant.NoBannerLogger

A naplózás szinkron, azaz a lassú naplózás lassíthatja a build folyamatot. Persze ez kivédhető Log4J esetén az AsyncAppender használatával.

Az itt említett property-k az ANT_OPTS környezeti változóban megadott -D kapcsolóval adhatóak meg, vagy a build.xml-ben az init target-ben megadott tag-gel.

A BuildListener-nek van egy SubBuildListener leszármazottja is, ami olyan metódusokat definiál, melyek akkor hívódnak meg, ha egy gyermek build folyamat elindul vagy befejeződik (pl. ant, subant, antcall task-kal).