Java EE 8-Sicherheits-API

Java EE 8-Sicherheits-API

1. Überblick

Die Java EE 8-Sicherheits-API ist der neue Standard und eine tragbare Methode zur Behandlung von Sicherheitsbedenken in Java-Containern.

In diesem Artikel werdenwe’ll look at the three core features of the API:

  1. HTTP-Authentifizierungsmechanismus

  2. Identitätsspeicher

  3. Sicherheitskontext

Wir werden zunächst verstehen, wie Sie die bereitgestellten Implementierungen konfigurieren und dann eine benutzerdefinierte implementieren.

2. Maven-Abhängigkeiten

Zum Einrichten der Java EE 8-Sicherheits-API benötigen wir entweder eine vom Server bereitgestellte oder eine explizite Implementierung.

2.1. Verwenden der Serverimplementierung

Java EE 8-kompatible Server bieten bereits eine Implementierung für die Java EE 8-Sicherheits-API. Daher benötigen wir nur das Maven-ArtefaktJava EE Web Profile API:


    
        javax
        javaee-web-api
        8.0
        provided
    

2.2. Explizite Implementierung verwenden

Zunächst spezifizieren wir das Maven-Artefakt für Java EE 8Security API:


    
        javax.security.enterprise
        javax.security.enterprise-api
        1.0
    

Und dann fügen wir eine Implementierung hinzu, z. B.Soteria - die Referenzimplementierung:


    
        org.glassfish.soteria
        javax.security.enterprise
        1.0
    

3. HTTP-Authentifizierungsmechanismus

Vor Java EE 8 haben wir Authentifizierungsmechanismen deklarativ über dieweb.xml-Datei konfiguriert.

In dieser Version hat die Java EE 8-Sicherheits-API die neueHttpAuthenticationMechanism-Schnittstelle als Ersatz entworfen. Daher können Webanwendungen jetzt Authentifizierungsmechanismen konfigurieren, indem Implementierungen dieser Schnittstelle bereitgestellt werden.

Glücklicherweise bietet der Container bereits eine Implementierung für jede der drei in der Servlet-Spezifikation definierten Authentifizierungsmethoden: Standardmäßige HTTP-Authentifizierung, formularbasierte Authentifizierung und benutzerdefinierte formularbasierte Authentifizierung.

Es enthält auch eine Anmerkung zum Auslösen jeder Implementierung:

  1. @BasicAuthenticationMechanismDefinition

  2. @FormAuthenticationMechanismDefinition

  3. @CustomFormAuthenrticationMechanismDefinition

3.1. Grundlegende HTTP-Authentifizierung

Wie oben erwähnt, kann eine Webanwendung die grundlegende HTTP-Authentifizierung nur mit@BasicAuthenticationMechanismDefinition annotation on a CDI beankonfigurieren:

@BasicAuthenticationMechanismDefinition(
  realmName = "userRealm")
@ApplicationScoped
public class AppConfig{}

Zu diesem Zeitpunkt durchsucht und instanziiert der Servlet-Container die bereitgestellte Implementierung derHttpAuthenticationMechanism-Schnittstelle.

Nach Erhalt einer nicht autorisierten Anforderung fordert der Container den Client auf, geeignete Authentifizierungsinformationen über den Antwortheader vonWWW-Authenticatebereitzustellen.

WWW-Authenticate: Basic realm="userRealm"

Der Client sendet dann den Benutzernamen und das Kennwort, die durch einen Doppelpunkt ":" getrennt und in Base64 codiert sind, über den Anforderungsheader vonAuthorization:

//user=example, password=example
Authorization: Basic YmFlbGR1bmc6YmFlbGR1bmc=

Beachten Sie, dass das Dialogfeld zur Eingabe der Anmeldeinformationen vom Browser und nicht vom Server stammt.

3.2. Formularbasierte HTTP-Authentifizierung

The @FormAuthenticationMechanismDefinition annotation triggers a form-based authentication wie in der Servlet-Spezifikation definiert.

Dann haben wir die Möglichkeit, die Anmelde- und Fehlerseiten anzugeben oder die standardmäßigen angemessenen/login und/login-error zu verwenden:

@FormAuthenticationMechanismDefinition(
  loginToContinue = @LoginToContinue(
    loginPage = "/login.html",
    errorPage = "/login-error.html"))
@ApplicationScoped
public class AppConfig{}

Als Ergebnis des Aufrufs vonloginPage, sollte der Server das Formular an den Client senden:

Der Client sollte das Formular dann an einen vordefinierten Backing-Authentifizierungsprozess senden, der vom Container bereitgestellt wird.

3.3. Benutzerdefinierte formularbasierte HTTP-Authentifizierung

Eine Webanwendung kann die benutzerdefinierte formularbasierte Authentifizierungsimplementierung mithilfe der Annotation@CustomFormAuthenticationMechanismDefinition: auslösen

@CustomFormAuthenticationMechanismDefinition(
  loginToContinue = @LoginToContinue(loginPage = "/login.xhtml"))
@ApplicationScoped
public class AppConfig {
}

Im Gegensatz zur standardmäßigen formularbasierten Authentifizierung konfigurieren wir jedoch eine benutzerdefinierte Anmeldeseite und rufen dieSecurityContext.authenticate()-Methode als Hintergrundauthentifizierungsprozess auf.

Schauen wir uns auch die BackingLoginBean an, die die Anmeldelogik enthalten:

@Named
@RequestScoped
public class LoginBean {

    @Inject
    private SecurityContext securityContext;

    @NotNull private String username;

    @NotNull private String password;

    public void login() {
        Credential credential = new UsernamePasswordCredential(
          username, new Password(password));
        AuthenticationStatus status = securityContext
          .authenticate(
            getHttpRequestFromFacesContext(),
            getHttpResponseFromFacesContext(),
            withParams().credential(credential));
        // ...
    }

    // ...
}

Als Ergebnis des Aufrufs der benutzerdefinierten Seitelogin.xhtmlendet der Client das empfangene Formular an die MethodeLoginBean'slogin():

//...

3.4. Benutzerdefinierter Authentifizierungsmechanismus

DieHttpAuthenticationMechanism-Schnittstelle definiert drei Methoden. The most important is the validateRequest(), für die wir eine Implementierung bereitstellen müssen.

Das Standardverhalten für die beiden anderen MethodensecureResponse() undcleanSubject()*,* ist in den meisten Fällen ausreichend.

Schauen wir uns eine Beispielimplementierung an:

@ApplicationScoped
public class CustomAuthentication
  implements HttpAuthenticationMechanism {

    @Override
    public AuthenticationStatus validateRequest(
      HttpServletRequest request,
      HttpServletResponse response,
      HttpMessageContext httpMsgContext)
      throws AuthenticationException {

        String username = request.getParameter("username");
        String password = response.getParameter("password");
        // mocking UserDetail, but in real life, we can obtain it from a database
        UserDetail userDetail = findByUserNameAndPassword(username, password);
        if (userDetail != null) {
            return httpMsgContext.notifyContainerAboutLogin(
              new CustomPrincipal(userDetail),
              new HashSet<>(userDetail.getRoles()));
        }
        return httpMsgContext.responseUnauthorized();
    }
    //...
}

Hier stellt die Implementierung die Geschäftslogik des Validierungsprozesses bereit. In der Praxis wird jedoch empfohlen, anIdentityStore zu delegieren, indemIdentityStoreHandler byvalidate aufruft.

Wir haben die Implementierung auch mit@ApplicationScoped Annotation versehen, da wir sie CDI-fähig machen müssen.

Nach einer gültigen Überprüfung des Berechtigungsnachweises und einem eventuellen Abrufen der Benutzerrollen werdenthe implementation should notify the container then:

HttpMessageContext.notifyContainerAboutLogin(Principal principal, Set groups)

3.5. Erzwingen der Servlet-Sicherheit

A web application can enforce security constraints by using the @ServletSecurity annotation on a Servlet implementation:

@WebServlet("/secured")
@ServletSecurity(
  value = @HttpConstraint(rolesAllowed = {"admin_role"}),
  httpMethodConstraints = {
    @HttpMethodConstraint(
      value = "GET",
      rolesAllowed = {"user_role"}),
    @HttpMethodConstraint(
      value = "POST",
      rolesAllowed = {"admin_role"})
  })
public class SecuredServlet extends HttpServlet {
}

Diese Anmerkung hat zwei Attribute -httpMethodConstraints undvalue; httpMethodConstraints wird verwendet, um eine oder mehrere Einschränkungen anzugeben, von denen jede eine Zugriffssteuerung auf eine HTTP-Methode durch eine Liste zulässiger Rollen darstellt.

Der Container prüft dann für jedeurl-pattern- und HTTP-Methode, ob der verbundene Benutzer die geeignete Rolle für den Zugriff auf die Ressource hat.

4. Identitätsspeicher

Diese Funktion wird vonthe IdentityStore interface, and it’s used to validate credentials and eventually retrieve group membership.  abstrahiert. Mit anderen Worten, sie kann Funktionen zur Authentifizierung, Autorisierung oder für beide. bereitstellen

IdentityStore soll vonHttpAuthenticationMecanism über eine aufgerufeneIdentityStoreHandler-Schnittstelle verwendet werden. Eine Standardimplementierung vonIdentityStoreHandler wird vom Servlet -Scontainer bereitgestellt.

Eine Anwendung kann die Implementierung vonIdentityStore bereitstellen oder eine der beiden integrierten Implementierungen verwenden, die vom Container für Datenbank und LDAP bereitgestellt werden.

4.1. Integrierte Identitätsspeicher

Der Java EE-kompatible Server sollte Implementierungen fürthe two Identity Stores: Database and LDAP bereitstellen.

Die Implementierung der DatenbankIdentityStorewird initialisiert, indem Konfigurationsdaten an die Annotation@DataBaseIdentityStoreDefinitionübergeben werden:

@DatabaseIdentityStoreDefinition(
  dataSourceLookup = "java:comp/env/jdbc/securityDS",
  callerQuery = "select password from users where username = ?",
  groupsQuery = "select GROUPNAME from groups where username = ?",
  priority=30)
@ApplicationScoped
public class AppConfig {
}

Als Konfigurationsdaten werdenwe need a JNDI data source to an external database, zwei JDBC-Anweisungen zum Überprüfen des Anrufers und seiner Gruppen und schließlich ein Prioritätsparameter konfiguriert, der im Fall eines Mehrfachspeichers verwendet wird.

IdentityStore mit hoher Priorität werden später vonIdentityStoreHandler. verarbeitet

LDAP IdentityStore implementation is initialized through the @LdapIdentityStoreDefinition wie die Datenbank durch Übergabe von Konfigurationsdaten:

@LdapIdentityStoreDefinition(
  url = "ldap://localhost:10389",
  callerBaseDn = "ou=caller,dc=example,dc=com",
  groupSearchBase = "ou=group,dc=example,dc=com",
  groupSearchFilter = "(&(member=%s)(objectClass=groupOfNames))")
@ApplicationScoped
public class AppConfig {
}

Hier benötigen wir die URL eines externen LDAP-Servers, um den Anrufer im LDAP-Verzeichnis zu suchen und seine Gruppen abzurufen.

4.2. Implementieren eines benutzerdefiniertenIdentityStore

DieIdentityStore-Schnittstelle definiert vier Standardmethoden:

default CredentialValidationResult validate(
  Credential credential)
default Set getCallerGroups(
  CredentialValidationResult validationResult)
default int priority()
default Set validationTypes()

Diepriority() -Smethod gibt einen Wert für die Reihenfolge der Iteration zurück, in der diese Implementierung vonIdentityStoreHandler. verarbeitet wird. EinIdentityStore mit niedrigerer Priorität wird zuerst behandelt.

Standardmäßig verarbeitet einIdentityStoreowohl die Validierung der Anmeldeinformationen(ValidationType.VALIDATE)als auch den Gruppenabruf (ValidationType.PROVIDE_GROUPS). Dieses Verhalten kann außer Kraft gesetzt werden, sodass nur eine Funktion bereitgestellt werden kann.

Daher können wirIdentityStoreo konfigurieren, dass sie nur für die Überprüfung der Anmeldeinformationen verwendet werden:

@Override
public Set validationTypes() {
    return EnumSet.of(ValidationType.VALIDATE);
}

In diesem Fall sollten wir eine Implementierung für dievalidate()-Methode bereitstellen:

@ApplicationScoped
public class InMemoryIdentityStore implements IdentityStore {
    // init from a file or harcoded
    private Map users = new HashMap<>();

    @Override
    public int priority() {
        return 70;
    }

    @Override
    public Set validationTypes() {
        return EnumSet.of(ValidationType.VALIDATE);
    }

    public CredentialValidationResult validate(
      UsernamePasswordCredential credential) {

        UserDetails user = users.get(credential.getCaller());
        if (credential.compareTo(user.getLogin(), user.getPassword())) {
            return new CredentialValidationResult(user.getLogin());
        }
        return INVALID_RESULT;
    }
}

Oder wir könnenIdentityStoreo konfigurieren, dass sie nur zum Abrufen von Gruppen verwendet werden können:

@Override
public Set validationTypes() {
    return EnumSet.of(ValidationType.PROVIDE_GROUPS);
}

Wir sollten dann eine Implementierung für diegetCallerGroups()-Methoden bereitstellen:

@ApplicationScoped
public class InMemoryIdentityStore implements IdentityStore {
    // init from a file or harcoded
    private Map users = new HashMap<>();

    @Override
    public int priority() {
        return 90;
    }

    @Override
    public Set validationTypes() {
        return EnumSet.of(ValidationType.PROVIDE_GROUPS);
    }

    @Override
    public Set getCallerGroups(CredentialValidationResult validationResult) {
        UserDetails user = users.get(
          validationResult.getCallerPrincipal().getName());
        return new HashSet<>(user.getRoles());
    }
}

DaIdentityStoreHandler erwartet, dass die Implementierung eine CDI-Bean ist, dekorieren wir sie mit der AnnotationApplicationScoped.

5. Sicherheitskontext-API

Die Java EE 8-Sicherheits-API bietetan access point to programmatic security through the SecurityContext interface. Dies ist eine Alternative, wenn das vom Container erzwungene deklarative Sicherheitsmodell nicht ausreicht.

Eine Standardimplementierung derSecurityContext-Schnittstelle sollte zur Laufzeit als CDI-Bean bereitgestellt werden. Daher müssen wir sie einfügen:

@Inject
SecurityContext securityContext;

Zu diesem Zeitpunkt können wir den Benutzer authentifizieren, einen authentifizierten abrufen, seine Rollenmitgliedschaft überprüfen und den Zugriff auf Webressourcen über die fünf verfügbaren Methoden gewähren oder verweigern.

5.1. Anruferdaten abrufen

In früheren Versionen von Java EE haben wir diePrincipal abgerufen oder die Rollenmitgliedschaft in jedem Container unterschiedlich überprüft.

Während wir die MethodengetUserPrincipal() und isUserInRole() derHttpServletRequest in einem Servlet-Container verwenden, werden ähnliche MethodengetCallerPrincipal() andisCallerInRole() methods für dieEJBContext verwendet EJB Container.

Die neue Java EE 8-Sicherheits-API hat diesesby tandardisiert und eine ähnliche Methode über dieSecurityContext-Schnittstelle bereitgestellt:

Principal getCallerPrincipal();
boolean isCallerInRole(String role);
 Set getPrincipalsByType(Class type);

Die MethodegetCallerPrincipal() gibt eine container-spezifische Darstellung des authentifizierten Aufrufers zurück, während die MethodegetPrincipalsByType() alle Principals eines bestimmten Typs abruft.

Dies kann nützlich sein, wenn sich der anwendungsspezifische Aufrufer vom Container unterscheidet.

5.2. Testen des Zugriffs auf Webressourcen

Zuerst müssen wir eine geschützte Ressource konfigurieren:

@WebServlet("/protectedServlet")
@ServletSecurity(@HttpConstraint(rolesAllowed = "USER_ROLE"))
public class ProtectedServlet extends HttpServlet {
    //...
}

Um den Zugriff auf diese geschützte Ressource zu überprüfen, sollten wirhasAccessToWebResource() method: aufrufen

securityContext.hasAccessToWebResource("/protectedServlet", "GET");

In diesem Fall gibt die Methode true zurück, wenn sich der Benutzer in der RolleUSER_ROLE. befindet

5.3. Programmgesteuertes Authentifizieren des Anrufers

Eine Anwendung kann den Authentifizierungsprozess programmgesteuert auslösen, indem sieauthenticate() aufruft:

AuthenticationStatus authenticate(
  HttpServletRequest request,
  HttpServletResponse response,
  AuthenticationParameters parameters);

Der Container wird dann benachrichtigt und ruft seinerseits den für die Anwendung konfigurierten Authentifizierungsmechanismus auf. Der ParameterAuthenticationParameters gibt einen Berechtigungsnachweis fürHttpAuthenticationMechanism: an

withParams().credential(credential)

Die WerteSUCCESS undSEND_FAILURE vonAuthenticationStatus entwerfen eine erfolgreiche und fehlgeschlagene Authentifizierung, währendSEND_CONTINUE einen laufenden Status des Authentifizierungsprozesses signalisiert.

6. Ausführen der Beispiele

Zur Hervorhebung dieser Beispiele haben wir die neueste Entwicklungsversion desOpen Liberty-Servers verwendet, der Java EE 8 unterstützt. Dies wird heruntergeladen und installiert, dank derliberty-maven-plugin, die auch die Anwendung bereitstellen und den Server starten können.

Um die Beispiele auszuführen, greifen Sie einfach auf das entsprechende Modul zu und rufen Sie den folgenden Befehl auf:

mvn clean package liberty:run

Als Ergebnis wird Maven den Server herunterladen, die Anwendung erstellen, bereitstellen und ausführen.

7. Fazit

In diesem Artikel wurde die Konfiguration und Implementierung der Hauptfunktionen der neuen Java EE 8-Sicherheits-API behandelt.

Zunächst wurde gezeigt, wie die integrierten Standardauthentifizierungsmechanismen konfiguriert werden und wie ein benutzerdefinierter Mechanismus implementiert wird. Später haben wir gesehen, wie Sie den integrierten Identitätsspeicher konfigurieren und einen benutzerdefinierten implementieren. Und schließlich haben wir gesehen, wie man Methoden derSecurityContext. aufruft

Wie immer sind die Codebeispiele für diesen Artikelover on GitHub verfügbar.