Szoftverfejlesztés J2EE platformon - Security
Ez az oldal a korábbi SCH wikiről lett áthozva.
Ha úgy érzed, hogy bármilyen formázási vagy tartalmi probléma van vele, akkor, kérlek, javíts rajta egy rövid szerkesztéssel!
Ha nem tudod, hogyan indulj el, olvasd el a migrálási útmutatót.
Gondolom, elég sokan vagyunk, akik úgy képzelünk el egy webes bejelentkezést, hogy a felhasználónak ki kell töltenie valamilyen űrlapot a nevével és a jelszavával, a háttérben erre a program kiszedi a megfelelő sort az adatbázisból, és ennek alapján eldönti, hogy jó-e a jelszó, avagy rossz, admin-e a felhasználó vagy sem, stb. Ez a folyamat az autentikáció. Ezután a felhasználó meg akar nézni valamilyen lapot a biztonságos területről. A rendszernek döntenie kell, hogy beengedje-e, avagy sem. Ez az autorizáció. Aki csinált már valamilyen webes bejelentkező felülettel rendelkező cuccot PHP-ban, az megszokta, hogy mindezt saját magának kell leprogramoznia. Erre természetesen Java EE-ben is van lehetőség (a PHP-hoz képest kisebb csinosításokkal, úgymint filterek beiktatása, entity beanek használata sima query-k helyett, stb.), szükség azonban nincs rá. Tudvalevő, hogy a Java EE biztonsági szolgáltatása a programozó kezébe ad egy JAAS-alapú (Java Authentication & Authorization Service) módszert az autentikáció és az autorizáció (a továbbiakban AA) kezelésére. Hogy miért jó ez?
- Valakik már megírták helyetted a kódot. Valószínű, hogy az évek során sikerült egy csomó rést kiküszöbölni (lásd pl. SQL-injektálhatóság), úgyhogy feltehetően jobban jársz egy ilyennel.
- Ha deklaratívan kezeled az AA-t, akkor a jogosultságok nincsenek bedrótozva a programba: egy-két telepítésleíró-bejegyzés beiktatásával vagy elvételével bővíthető vagy kurtítható egy-egy szerepkör felhasználói tábora. További hatalmas előny, hogy az AA adatokat tartalmazó erőforrás jellegétől (Egy fájl? Egy adatbázis? Egy...?) is független maradhatsz.
A Java EE biztonsági szolgáltatásában ún. realmek (ejtsd "relm", nem pedig "rílm") használatosak egy alkalmazás felhasználói táborának azonosítására. Autentikálják a felhasználót, aki egy biztonsági kontextust kap, benne a felhasználói azonosítójával és a csoportazonosítójával (esetleg több ilyen is lehet bizonyos realmeknél). Az autorizáció ezután a konténer feladata, a telepítésleírók alapján.
A továbbiak kizárólag Netbeans 5.5-re, és a hozzácsomagolt AS-re vonatkoznak. (Ezt használom, ezt ismerem...) Az életszerűség kedvéért hagyom a fenébe a konfigfájlalapú (file realm) azonosítást, helyette az adatbázis-alapú azonosítást (JDBCRealm) fogom bemutatni.
Mik egy biztonságos program fejlesztésének lépései, ha Java EE Securityben gondolkozunk?
- Adatbázistáblák létrehozása a felhasználónév, a jelszó és a csoportazonosító tárolására
- Adatok beírása az adatbázisba ;)
- Alkalmazásszerver elindítása, Admin console megnyitása, bejelentkezés (alapértelmezés: admin/adminadmin)
- Ámulás-bámulás: a Configuration -> Security (a baloldali menüben a kis nyílra kattints!) -> Realms alatt csak olyasmik látszanak, mint a file realm (bármilyen komolyabb célra használhatatlan) és a certificate realm (ennek a valós életben lehet értelme, egy házi feladatban már kevésbé). Hozzá kell adni a listához a JDBCRealmet. A New... gombot megnyomva egy űrlapot kapunk. Az adatok:
- Realm: JDBCRealm
- Class name: com.sun.enterprise.security.auth.realm.jdbc.JDBCRealm
- Hozzá kell továbbá adni egy vagon propertyt az Add Property gombbal. Ezek a következők:
- datasource-jndi (az adatbázis kapcsolat JNDI neve)
- user-table (a felhasználói neveket és jelszavakat tároló tábla)
- user-name-column (a felhasználói név oszlopa)
- password-column (a jelszó oszlopa)
- group-table (a csoport táblája, mely akár azonos is lehet a user-table-ben megadottal)
- group-name-column (a csoportnév oszlopa)
- jaas-context (kötelezően jdbcRealm, pontosan ilyen betűnagyságokkal)
- digest-algorithm (a jelszóhoz tartozó digest előállításának algoritmusa - ha ilyet nem akarsz, ez a property none legyen).
- Ok.
- A group-table tábla group-name-column mezejéből kivett csoportnevet össze kell rendelni a konténer által azonosítható biztonsági szerepkörökkel (role). Ennek módja a telepítésleíró (sun-application.xml) megbuherálása. Először is meg kell adni neki a fasza kis GUI-n, hogy a JDBCRealmet használja, majd Edit as XML. Be kell szúrni a kívánt mennyiségű összerendelést a következőképp:
<security-role-mapping> <role-name>client</role-name> <group-name>client</group-name> </security-role-mapping>
Asszem nemigen szükséges magyarázat, hogy hova mit írj (a csoport és a szerepkör neve lehet azonos, de ez korántsem kötelező, valamint egy szerepkörhöz több csoport is lehet rendelve).
- Pl. web réteges AA beállításához security constrainteket kell beállítani a telepítésleíróban (EJB rétegben is lehet használni deklaratív jellegű AA-t, de ott annotációk szükségesek. Ennek sajnos nem néztem utána, majd valaki más... :) ). Nyisd ki a web.xml-t, és menj a Security fülre! Az autentikáció típusa legyen Form, a realm neve JDBCRealm, a Form Login Page és a Form Error Page értelemszerűen kitöltendő egy-egy magunk készítette lap címével. Most a Security Roles rész jön. Ide kell beszúrni az alkalmazásban használni kívánt biztonsági szerekörök nevét. (Nem árt, ha a csoport-szerepkör összerendelésnél mindkét fél létezik...) Végül a Security Constraints részben definiálni kell a jogosultsági korlátokat a megfelelő URL mintákra. Fontos, hogy az Enable Authentication Constraint be legyen ikszelve, és hozzá legyen adva az Edit gombbal az engedélyezni kívánt szerepkör. Az sem árt továbbá, ha a Form Login Page nem a védett területen belül van. A bejelentkeztetést végző űrlap szerkezete kötött:
- a form tag action paramétere mindenképp j_security_check
- a felhasználónév egy text típusú, j_username nevű szövegmező
- a jelszó egy password típusú, j_password nevű szövegmező
No, nagyjából ennyi: ha ezt mind végigcsináltad, kaptál egy működő AA-t a web rétegben, egyetlen sor Java kód beírása nélkül. Ugye, hogy nem rossz? :)
Amit még érdemes tudni:
- A form HTML-tagnek ne legyen ilyen attribútuma: enctype="multipart/form-data".
- A login form csak akkor fog feljönni, ha authentikációhoz kötött oldalt szeretnél elérni. Direkt meghívni nem szerencsés (nem fog működni). Ha jól adod meg a felhasználónevet és a jelszót, akkor az elküld gomb után megkapod a kért oldalt.
Források:
- Imre Gábor diái :)
- http://blogs.sun.com/swchan/entry/jdbcrealm_in_glassfish
- http://www.developinjava.com/readarticle.php?article_id=5
Programmatic Login
Ha minden oldalra szeretnél kirakni egy egyedi login form-ot, akkor a _ProgrammaticLogin_-ra lesz szükséged. A ProgrammaticLogin osztály használatához szükség lesz a Glassfish lib könyvtárában lévő lévő appserver-rt.jar fájlra. NetBeansben: a pojectben a Libraries mappára jobb klikk, Add JAR/Folder...
Ezzel megoldható az is, hogy egy JSF-es űrlaphoz tartozó backing bean kezelje a bejelentkezést, illetve a kijelentkezést is. Hátránya, hogy appszerverfüggő. Íme egy Glassfish-es megoldás vázlata:
import com.sun.appserv.security.ProgrammaticLogin; ... ProgrammaticLogin programmaticLogin; programmaticLogin = new ProgrammaticLogin(); .... programmaticLogin.login(username, password, "JDBCRealm", getRequest(), getResponse(), true); ... programmaticLogin.logout(getRequest(), getResponse());
A HttpServletRequest és HttpServletResponse objektumokat így tudod megszerezni backing beanből:
FacesContext ctx = FacesContext.getCurrentInstance(); ExternalContext ectx = ctx.getExternalContext(); this.req = (HttpServletRequest) ectx.getRequest(); this.resp = (HttpServletResponse) ectx.getResponse();
Ezekre azért van szükség, mert sikeres authentikáció után ide kerül be a session azonosító. Persze ha csak meghívsz egy metódust a jogosultságokkal, és rögtön ki is lépsz (ugyanabban a backing bean metódusban), akkor elég lehet a HttpServlet* paraméterek nélkül login() és logout() metódus is (bár ezt nem próbáltam).
Egy példa login form:
<f:view> <h:form> <table border="1" cellpadding="1"> <tr> <th colspan="2">Login form</th> </tr> <tr> <th><h:outputLabel for="UsernameField" value="Username:" /></th> <td> <h:inputText id="UsernameField" value="#{LoginManagedBean.username}" required="true" /> </td> <td><h:message for="UsernameField" /></td> </tr> <tr> <th><h:outputLabel for="PasswordField" value="Password:" /></th> <td> <h:inputText id="PasswordField" value="#{LoginManagedBean.password}" required="true" /> </td> <td><h:message for="PasswordField" /></td> </tr> <tr> <td> <h:commandButton type="submit" value="Login" action="#{LoginManagedBean.login}" /> </td> <td colspan="2"> <h:messages globalOnly="true" /> </td> </tr> </table> </h:form> </f:view>
EJB-security
Ha JAAS-el authentikálod magad, akkor azt a webréteg továbbterjeszti az EJB-réter felé is. Session bean-ben így tudod lekérdezni a bejelentkezett felhasználó nevét:
@Resource SessionContext sessionContext; ... String username = sessionContext.getCallerPrincipal().getName();