Java-EE-Webapplikation absichern
Authentifizierung und Autorisierung mit Servlet 3.0 und JDBCRealm
In diesem Weblog möchte ich in aller Kürze 😉 das Thema “Anwendungssicherheit” in Java EE 6 erläutern. Als Applikationsserver habe ich Glassfish verwendet, Infos zum JBoss sind weiter unten verlinkt.
Anhand eines Beispiels gehen wir die einzelnen Schritte durch, um den Server abzusichern und um unsere Web-Anwendung vor unberechtigtem Zugriff zu schützen. Die Web-Anwendung wird drei voneinander abgegrenzte Bereiche (admin, employee, manager) haben, die nur ein Nutzer mit der richtigen Rolle betreten darf.
Ganz konkret werden wir an einer bestehenden Webapplikation folgende Dinge tun:
1. Wir sichern das Umfeld des Web-Servers ab. [Link]
2. Wir erzeugen eine Datenbanktabelle, die User, Passwort und Rollen enthält. [Link]
3. JSF Login: Wir bauen eine JSF-Login-Seite, die EJB nutzt, um Nutzer einzulassen oder auszusperren. Die Zugriffsberechtigungen liegen in einer SQL-Datenbank. [Link]
4. Deployment Deskriptor web.xml: Wir setzen deklarativ SecurityConstraints in der web.xml, um Seiten vor unberechtigtem Zugriff zu schützen. [Link]
5. EJBs und Servlets: Per Annotation setzen wir erlaubte und unerlaubte Rollen und können in EJBs sogar auf Methodenebene Nutzer ausschließen. [Link]
Unsere Web-Anwendung besteht aus folgenden Schichten:
JSF 2.0 und Servlets 3.0 (z.B. Glassfish 3, JBoss 6+, Tomcat 7)
EJB 3.1 (mind. JavaEE6)
MySQL-Datenbank
1. Server und Betriebssystem absichern
Bevor wir uns daranmachen, unsere Web-Anwendung zu sichern, müssen wir erstmal daran, den Server und das Umfeld zu sichern. Wir legen hier die Basis für die Sicherheit unserer Web-Anwendung. Was hilft es, wenn unsere Web-Anwendung perfekt geschützt ist, aber der Server und das Betriebssystem angreifbar sind?
In diesem Artikel möchte ich nur einige Stichpunkte geben, da das Thema zu weitreichend ist, um es hier auch nur annähernd vollständig behandeln zu können.
1.1 Absichern des Betriebssystems:
- User mit eingeschränkten Rechten einrichten, nicht als Administrator auf dem BS arbeiten
- Firewall, Virenscanner und gesunden Menschenverstand einschalten
- “Sichere”, d.h. aktuelle und gepatchte Software verwenden
- und vieles mehr
1.2 Absichern des (Java Application) Servers:
- Nur Ports öffnen, die vom Server genutzt werden (s. %serverdir%/config/server.xml auf den meisten Java-Applikationsservern und Servlet-Containern)
- Die Admin-Konsole per Passwort absichern. Hier einige Beispiele:
Tomcat (ungetestet): Die Datei%tomcat%/conf/tomcat-users.xml
muss angepasst werden
Glassfish:%glassfish%/bin/asadmin change-master-password
bzw.
%glassfish%/bin/asadmin change-admin-password
auf der Kommandozeile ausführen
JBoss 7 (ungetestet):%jboss7%/standalone/configuration/mgmt-users.properties
editieren - und vieles mehr
2. Authentifizierung mit einer Datenbanktabelle
Für die Login-Seite müssen wir ein Realm anlegen.
Was ist ein Realm? Lt. Definition im offiziellen Tutorial von Sun ist ein Realm eine Sicherheits-domäne, die für einen Webserver definiert wird. Den Satz verstehe ich selber nur zur Hälfte, deshalb habe ich ein Bild gemalt, das diesen Sachverhalt klarer macht:
In Worten: Wir haben in der Datenbank einen User ‘holger’, der in der Gruppe ‘manager’ ist, einen User ‘bernhard’, der in der Gruppe ’employee’ ist usw. Unsere Web-Anwendung definiert Rollen, die zufällig genauso heißen – aber auch anders heißen könnten. Nun müssen wir noch die in der Datenbank definierten Gruppen unserer Web-Anwendung bekannt machen: das geht über eine Mapping-Tabelle, die besagt, dass bspw. der Datenbankeintrag ‘manager’ auf die Rolle ‘manager’ unserer Web-Anwendung passt.
Ziel: Der “Manager” darf nur in den “Manager”-Bereich unserer Web-Anwendung und nur mit “manager” bezeichnete Geschäftslogik aufrufen, das Gleiche gilt für den Nutzer mit der Rolle “employee” und Nutzer der Rolle “admin”.
So, jetzt müssen wir die User anlegen, die unsere Web-Anwendung im Login erkennen soll. Hier will ich die Erzeugung von Usern auf zwei Wegen behandeln – per FileRealm und per JDBCRealm. FileRealm ist einfacher, da wir dafür keine Datenbanktabelle und kein Mapping brauchen, aber wir können damit nicht dynamisch User anlegen, wie wir es mit dem datenbankbasierten JDBCRealm können, sondern müssen händisch User anlegen.
- FileRealm (optional): wir legen die User mitsamt Gruppe per Hand auf dem Server (nicht per DB)
an. Wenn nur der JDBC-Realm interessant ist, der überspringt diesen Abschnitt und
klickt hier.
Der Server legt dafür eine Datei auf dem Server an, die Username, gehashtes Passwort
und Rollen enthält.
Glassfish: In der Web-Oberfläche gehen wir auf den Punkt
Konfigurationen -> server-config ->Sicherheit -> Bereiche -> file (s. Screenshot).
Hier im Beispiel habe ich die User ‘fileBernhard’ mit Gruppenzugehörigkeit ’employee’ angelegt, den ‘fileAdmin’ mit Gruppe ‘admin’ und ‘fileHolger’ mit Gruppe ‘manager’.
Wer das sofort mal testen will, kann den kommenden Abschnitt ‘JDBCRealm’
überspringen und geht gleich zum Login unserer Web-Applikation [Link].
JBoss: http://docs.jboss.org/jbossweb/3.0.x/realm-howto.html (ungetestet) - JDBCRealm: die Definition der User liegt in einer DB-Tabelle. Legen wir also eine
DB-Tabelle an:CREATE TABLE `myuser` ( `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY , `username` VARCHAR( 30 ) NOT NULL , `password` VARCHAR( 30 ) NOT NULL , `group` VARCHAR( 30 ) NOT NULL )
und legen ein paar Datensätze an:
INSERT INTO `myuser` ( `username` , `password` , `group`) VALUES ('admin', 'adminpw', 'admin'), ('bernhard', 'bernhardpw', 'employee'), ('holger', 'holgerpw', 'manager'), ('karlheinz', 'karlheinzpw', 'employee')
Ok, wir haben jetzt 4 Datensätze. Die müssen wir jetzt unserer Web-Anwendung bekannt
machen (s. Abb. “Realm”).Nächster Schritt: Im Glassfish müssen wir die Datei WEB-INF/glassfish-web.xml anlegen, die unsere “Gruppen” in unserer Datenbank-Tabelle auf die “Rollen” auf dem Server überträgt.
glassfish-web.xml
<security-role-mapping> <role-name>admin</role-name> <group-name>admin</group-name> </security-role-mapping> <security-role-mapping> <role-name>employee</role-name> <group-name>employee</group-name> </security-role-mapping> <security-role-mapping> <role-name>manager</role-name> <group-name>manager</group-name> </security-role-mapping>
Im JBoss gibt es ebenfalls die Security Roles mit leicht unterschiedlicher Notation:
JBoss-Docs zum MappingWir müssen die Datenbank dem Server über JNDI bekannt machen: wie das geht, hat Kollege Joachim beschrieben: [Link zu JNDI]
Jetzt legen wir das JDBCRealm an:
bei Glassfish geht das über die Weboberfläche,
bei JBoss über die Konfigurationsdatei$CATALINA_HOME/conf/server.xml [Link]
JBoss-Docs zu JDBCRealm
3. JSF Login-Seite
Wir nähern uns langsam dem ersten Erfolg. Erstellen wir also die Login-Seite als JSF-Facelet:
login.xhtml
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html"> <head> <title>Login</title> </head> <h:body> <h:form id="j_security_check"> <center> <h:messages errorClass="errorMessage" infoClass="infoMessage" warnClass="warnMessage"></h:messages> <h:panelGrid columns="2"> <h:outputText value="Username : "/> <h:inputText id="j_username" value="#{loginBean.username}"/> <h:outputText value="Password : "/> <h:inputSecret id="j_password" value="#{loginBean.password}"/> <h:commandButton value="Submit" action="#{loginBean.login}" type="submit"/> </h:panelGrid> </center> </h:form> </h:body> </html>
Und die logout.xhtml:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html"> <head> <title></title> </head> <h:body styleClass="body"> <h:messages></h:messages> <h:form> <h:commandLink value="Logout" action="#{loginBean.logout}"></h:commandLink> </h:form> </h:body> </html>
Wir man sieht, greift das Facelet auf eine loginBean
zu.
Diese wollen wir jetzt definieren:
LoginBean.java
package de.triona.ejb; import java.io.Serializable; import java.security.Principal; import java.util.logging.Logger; import javax.faces.application.FacesMessage; import javax.faces.bean.ManagedBean; import javax.faces.context.FacesContext; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; @ManagedBean public class LoginBean implements Serializable { private String username; private String password; public String login() { FacesContext fc = FacesContext.getCurrentInstance(); HttpServletRequest request = (HttpServletRequest) fc.getExternalContext().getRequest(); try { //Login per Servlet 3.0 request.login(username, password); // Der Principal entspricht dem Usernamen Principal principal = request.getUserPrincipal(); // Wir können hier nur abfragen, ob der User eine Rolle hat (isUserInRole('whatever')), // aber wir können NICHT die Rolle aktiv erfragen (z.B. mit getUserRole(...)) if (request.isUserInRole("admin")) { String msg = "User: " + principal.getName() + ", Role: admin"; fc.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, msg, null)); return "admin/startrek"; } else if (request.isUserInRole("manager")) { String msg = "User: " + principal.getName() + ", Role: manager"; fc.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, msg, null)); return "manager/timesheet"; } else if (request.isUserInRole("employee")) { String msg = "User: " + principal.getName() + ", Role: employee"; fc.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_INFO, msg, null)); return "employee/work"; } return "du_musst_die_rollen_noch_definieren"; // hier sollte etwas sinnvolles passieren ;-) } catch (ServletException e) { fc.addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, "An Error Occured: Login failed", null)); e.printStackTrace(); } return "loginFailed"; } public void logout() { FacesContext fc = FacesContext.getCurrentInstance(); HttpSession session = (HttpSession) fc.getExternalContext().getSession(false); if (session != null) { session.invalidate(); } fc.getApplication().getNavigationHandler().handleNavigation(fc, null, "/login.xhtml"); } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } }
Die Besonderheit an diesem Code ist die Methode HttpServletRequest.login(username,password)
aus der Servlet-Spezifikation 3.0 und HttpServletRequest.isUserInRole(String role)
.
Mit login(username,password)
wird username und password des Realms (hier: JDBCRealm oder FileRealm) validiert. Die Methode HttpServletRequest.isUserInRole(String role)
ist selbst-erklärend: hier wird die Rolle abgefragt, wir können aber aus Sicherheitsgründen nicht – und das ist Absicht der Java EE 6 Spezifikation – aktiv die Rolle abfragen.
Zwischenstand:
- wir haben eine DB-Tabelle myusers (mit Name, Passwort und Gruppe) angelegt und befüllt
- wir haben einen FileRealm auf dem Server angelegt (dieser Schritt ist optional)
- wir haben einen JDBCRealm angelegt
- wir haben eine JSF-Seite login.xhtml angelegt, die auf die EJB-Klasse LoginBean zugreift
- wir haben auf dem Glassfish die XML-Datei glassfish-web.xml bzw. auf dem JBoss die SecurityRoles
angelegt, die das Datenbankfeld myuser.group auf die Rollen in der Web-Applikation abbildet
Was fehlt noch?
4. web.xml bearbeiten
Wir müssen unserem Deployment-Deskriptor (WEB-INF/)web.xml mitteilen, das er eine Authentifizierung anhand unseres Formulars login.xhtml durchführt und wir müssen unsere Rollen definieren.
In der Netbeans-Oberfläche können wir die web.xml grafisch bearbeiten.
Wer direkt die web.xml bearbeiten will, kann das auch tun:
web.xml
<login-config> <auth-method>FORM</auth-method> <realm-name>loginRealm</realm-name> <form-login-config> <form-login-page>/login.xhtml</form-login-page> <form-error-page>/loginfailed.xhtml</form-error-page> </form-login-config> </login-config> <security-role> <description/> <role-name>manager</role-name> </security-role> <security-role> <description/> <role-name>admin</role-name> </security-role> <security-role> <description/> <role-name>employee</role-name> </security-role>
Probieren wir das Ganze aus: wir starten die login.xhtml im Browser.
Loggen wir uns erfolgreich als admin ein, dann sollten wir zur Seite /admin/startrek.xhtml weitergeleitet werden, der manager wird auf /manager/timesheet.xhtml und der employee auf /employee/work.xhtml weitergeleitet.
So weit, so gut: wir können uns einloggen und unsere Rolle ist dem Server bekannt.
Ein Problem haben wir noch: wer zufällig den Link für den Admin-Bereich kennt, kann diese Seiten aufrufen, dito für den Employee- und den Manager-Bereich.
Das können wir so ebenfalls in der web.xml fixen, indem wir URLs explizit für eine Rolle erlauben. Der Admin darf nur URLs mit dem Pattern /admin/* aufrufen, dito für employee und Manager:
Hier via Netbeans:
Und hier der XML-Codeausschnitt aus der web.xml:
<security-constraint> <display-name>AdminConstraint</display-name> <web-resource-collection> <web-resource-name>AdminArea</web-resource-name> <description/> <url-pattern>/admin/*</url-pattern> </web-resource-collection> <auth-constraint> <description/> <role-name>admin</role-name> </auth-constraint> <user-data-constraint> <description/> <transport-guarantee>CONFIDENTIAL</transport-guarantee> </user-data-constraint> </security-constraint> <security-constraint> <display-name>EmployeeConstraint</display-name> <web-resource-collection> <web-resource-name>EmployeeArea</web-resource-name> <description/> <url-pattern>/employee/*</url-pattern> </web-resource-collection> <auth-constraint> <description/> <role-name>employee</role-name> </auth-constraint> </security-constraint> <security-constraint> <display-name>ManagerConstraint</display-name> <web-resource-collection> <web-resource-name>ManagerArea</web-resource-name> <description/> <url-pattern>/manager/*</url-pattern> </web-resource-collection> <auth-constraint> <description/> <role-name>manager</role-name> </auth-constraint> </security-constraint>
In diesem Beispiel habe ich den Admin-Bereich via SSL-geschützt
<user-data-constraint><transport-guarantee>CONFIDENTIAL</transport-guarantee></user-data-constraint>
Der Browser leitet direkt von http auf https um.
Langsam geht es auf das Finale zu:
5. Annotationsbasierter Schutz von Servlets und EJBs
Wir haben das Schlimmste hinter uns. Jetzt folgt die Kür und die wunderbare Welt der Annotationen.
Fangen wir mit dem Servlet an. Wir wollen ein Servlet schreiben, das nur User mit den Rollen manager
und admin
besuchen können. Nichts leichter als das:
ManagerAndAdminServlet.java
@ServletSecurity(@HttpConstraint(rolesAllowed={"manager", "admin"})) public class ManagerAndAdminServlet extends HttpServlet { private static final long serialVersionUID = 1046L; protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //manager und admin dürfen rein, der employee bleibt draußen mit einer SecurityException } }
Mit der Annotation
@ServletSecurity(@HttpConstraint(rolesAllowed={"manager", "admin"}))
ist schon alles fertig.
Genauso einfach ist eine EJB vor unautorisiertem Zugriff geschützt:
WorkBean.java
@DeclareRoles({"manager", "employee", "admin"}) @Stateless @LocalBean public class WorkBean { /** Nur für Nutzer mit Rolle 'employee' */ @RolesAllowed("employee") public void doWork(String employee) { System.out.println("work"); } /** Nur für Nutzer mit Rolle 'manager' */ @RolesAllowed("manager") public void delegateWork() { System.out.println("delegateWork"); } /** Nur für den 'admin' */ @RolesAllowed("admin") public void administrateWhatever() { System.out.println("administrateWhatever"); } /** Alle Nutzer ('manager', 'employee' und 'admin') dürfen diese Methode aufrufen */ public void lookOutOfTheWindow() { System.out.println("it rains"); } }
Unser Web-Anwendung könnte bspw. jetzt so aussehen:
Fazit: Wir haben unsere Web-Anwendung deklarativ und programmativ schützen können.
Natürlich haben wir zum Thema Anwendungssicherheit nur die Spitze des Eisbergs gesehen.
Dies ist nur ein erster Einblick.
Liebe Leser, vielen Dank für eure Aufmerksamkeit.