• About
Triona Weblog

Software Development and much more

Sicherheit in Java-EE6-Webapplikationen

18.10.2011 by Anton Autor

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 Mapping

    Wir 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.

Posted in: Database, Development, Java, JEE Tagged: auth constraint, Java EE Sicherheit, JDBC Realm, JDBCRealm, JNDIRealm, JSF, JSF login, securing EJB, securing JSF, securing Servlets, Security, security constraint, Servlet 3.0 Login
October 2023
M T W T F S S
 1
2345678
9101112131415
16171819202122
23242526272829
3031  
« Nov    

Tags

API Architecture CDI Collections Comparable Comparator Database EA Eclipse EJB Enterprise Architect Excel Hessian HTML Iteration Java Java 8 JavaEE Java EE Java Enterprise Development javascript Javascript Canvas HTML5 JEE JEE 6 JPA jQuery JSF linux Makro Map MariaDB Maven Oracle Plugins Relation relationship Richfaces Service Service Facade Set SOA Subversion Tutorial VBA XML

Recent Posts

  • Domain Driven Design und Event Storming
  • NESTJS BEYOND „HELLO WORLD“
  • Jakarta EE 9 – An upheaval with difficulties
  • Jakarta EE 9 – Ein Umbruch mit Schwierigkeiten
  • Erste Schritte mit Hibernate Spatial

Meta

  • Log in
  • Entries feed
  • Comments feed
  • WordPress.org

Copyright © 2023 Triona Weblog.

Impressum | Datenschutzerklärung