Jakarta EE 8 Sicherheits-API

1. Übersicht

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

In diesem Artikel werden die drei Hauptfunktionen der API beschrieben:

  1. HTTP-Authentifizierungsmechanismus
  2. Identitätsspeicher
  3. Sicherheitskontext

Wir werden zuerst verstehen, wie die bereitgestellten Implementierungen konfiguriert werden und dann, wie eine benutzerdefinierte implementiert wird.

2. Maven-Abhängigkeiten

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

2.1. Verwenden der Server-Implementierung

Jakarta EE 8-kompatible Server bieten bereits eine Implementierung für die Jakarta EE 8-Sicherheits-API. Daher benötigen wir nur das Maven-Artefakt der Jakarta EE-Webprofil-API:

  javax javaee-web-api 8.0 provided  

2.2. Verwenden einer expliziten Implementierung

Zunächst geben wir das Maven-Artefakt für die Jakarta EE 8-Sicherheits-API an:

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

Und dann fügen wir eine Implementierung hinzu, zum Beispiel Soteria - die Referenzimplementierung:

  org.glassfish.soteria javax.security.enterprise 1.0  

3. HTTP-Authentifizierungsmechanismus

Vor Jakarta EE 8 haben wir Authentifizierungsmechanismen deklarativ über die Datei web.xml konfiguriert .

In dieser Version hat die Jakarta EE 8-Sicherheits-API die neue HttpAuthenticationMechanism- Schnittstelle als Ersatz entwickelt. Daher können Webanwendungen jetzt Authentifizierungsmechanismen konfigurieren, indem sie Implementierungen dieser Schnittstelle bereitstellen.

Glücklicherweise bietet der Container bereits eine Implementierung für jede der drei Authentifizierungsmethoden, die in der Servlet-Spezifikation definiert sind: Grundlegende 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 mithilfe der Annotation @BasicAuthenticationMechanismDefinition auf einer CDI-Bean konfigurieren :

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

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

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

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 Header der Autorisierungsanforderung :

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

Beachten Sie, dass das Dialogfeld zum Bereitstellen von Anmeldeinformationen vom Browser und nicht vom Server stammt.

3.2. Formularbasierte HTTP-Authentifizierung

Die Annotation @FormAuthenticationMechanismDefinition löst eine formularbasierte Authentifizierung aus, wie in der Servlet-Spezifikation definiert.

Dann haben wir die Möglichkeit, die Anmelde- und Fehlerseiten anzugeben oder die vernünftigen Standardeinstellungen / login und / login-error zu verwenden :

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

Als Ergebnis des Aufrufs von loginPage sollte der Server das Formular an den Client senden:

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

3.3. Benutzerdefinierte formularbasierte HTTP-Authentifizierung

Eine Webanwendung kann die benutzerdefinierte formularbasierte Authentifizierungsimplementierung mithilfe der Anmerkung @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 die SecurityContext.authenticate () -Methode als Hintergrundauthentifizierungsprozess auf.

Schauen wir uns auch die Backing LoginBean an, die die Login-Logik enthält:

@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)); // ... } // ... }

As a result of invoking the custom login.xhtml page, the client submits the received form to the LoginBean's login() method:

//... 

3.4. Custom Authentication Mechanism

The HttpAuthenticationMechanism interface defines three methods. The most important is the validateRequest() which we must provide an implementation.

The default behavior for the other two methods, secureResponse() and cleanSubject(), is sufficient in most cases.

Let's have a look at an example implementation:

@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(); } //... }

Here, the implementation provides the business logic of the validation process, but in practice, it's recommended to delegate to the IdentityStore through the IdentityStoreHandler by invoking validate.

We've also annotated the implementation with @ApplicationScoped annotation as we need to make it CDI-enabled.

After a valid verification of the credential, and an eventual retrieving of user roles, the implementation should notify the container then:

HttpMessageContext.notifyContainerAboutLogin(Principal principal, Set groups)

3.5. Enforcing Servlet Security

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 { }

This annotation has two attributes – httpMethodConstraints and value; httpMethodConstraints is used to specify one or more constraints, each one representing an access control to an HTTP method by a list of allowed roles.

The container will then check, for every url-pattern and HTTP method, if the connected user has the suitable role for accessing the resource.

4. Identity Store

This feature is abstracted by the IdentityStore interface, and it's used to validate credentials and eventually retrieve group membership. In other words, it can provide capabilities for authentication, authorization or both.

IdentityStore is intended and encouraged to be used by the HttpAuthenticationMecanism through a called IdentityStoreHandler interface. A default implementation of the IdentityStoreHandler is provided by the Servletcontainer.

An application can provide its implementation of the IdentityStore or uses one of the two built-in implementations provided by the container for Database and LDAP.

4.1. Built-in Identity Stores

The Jakarta EE compliant server should provide implementations for the two Identity Stores: Database and LDAP.

The database IdentityStore implementation is initialized by passing a configuration data to the @DataBaseIdentityStoreDefinition annotation:

@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 { }

As a configuration data, we need a JNDI data source to an external database, two JDBC statements for checking caller and his groups and finally a priority parameter which is used in case of multiples store are configured.

IdentityStore with high priority is processed later by the IdentityStoreHandler.

Like the database, LDAP IdentityStore implementation is initialized through the @LdapIdentityStoreDefinition by passing configuration data:

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

Here we need the URL of an external LDAP server, how to search the caller in the LDAP directory, and how to retrieve his groups.

4.2. Implementing a Custom IdentityStore

The IdentityStore interface defines four default methods:

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

The priority() method returns a value for the order of iteration this implementation is processed by IdentityStoreHandler. An IdentityStore with lower priority is treated first.

By default, an IdentityStore processes both credentials validation (ValidationType.VALIDATE) and group retrieval(ValidationType.PROVIDE_GROUPS). We can override this behavior so that it can provide only one capability.

Thus, we can configure the IdentityStore to be used only for credentials validation:

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

In this case, we should provide an implementation for the validate() method:

@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; } }

Or we can choose to configure the IdentityStore so that it can be used only for group retrieval:

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

We should then provide an implementation for the getCallerGroups() methods:

@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()); } }

Because IdentityStoreHandler expects the implementation to be a CDI bean, we decorate it with ApplicationScoped annotation.

5. Security Context API

The Jakarta EE 8 Security API provides an access point to programmatic security through the SecurityContext interface. It's an alternative when the declarative security model enforced by the container isn't sufficient.

A default implementation of the SecurityContext interface should be provided at runtime as a CDI bean, and therefore we need to inject it:

@Inject SecurityContext securityContext;

At this point, we can authenticate the user, retrieve an authenticated one, check his role membership and grant or deny access to web resource through the five available methods.

5.1. Retrieving Caller Data

In previous versions of Jakarta EE, we'd retrieve the Principal or check the role membership differently in each container.

While we use the getUserPrincipal() and isUserInRole() methods of the HttpServletRequest in a servlet container, a similar methods getCallerPrincipal() and isCallerInRole() methods of the EJBContext are used in EJB Container.

The new Jakarta EE 8 Security API has standardized this by providing a similar method through the SecurityContext interface:

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

The getCallerPrincipal() method returns a container specific representation of the authenticated caller while the getPrincipalsByType() method retrieves all principals of a given type.

It can be useful in case the application specific caller is different from the container one.

5.2. Testing for Web Resource Access

First, we need to configure a protected resource:

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

And then, to check access to this protected resource we should invoke the hasAccessToWebResource() method:

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

In this case, the method returns true if the user is in role USER_ROLE.

5.3. Authenticating the Caller Programmatically

An application can programmatically trigger the authentication process by invoking authenticate():

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

The container is then notified and will, in turn, invoke the authentication mechanism configured for the application. AuthenticationParameters parameter provides a credential to HttpAuthenticationMechanism:

withParams().credential(credential)

The SUCCESS and SEND_FAILURE values of the AuthenticationStatus design a successful and failed authentication while SEND_CONTINUE signals an in progress status of the authentication process.

6. Running the Examples

For highlighting these examples, we've used the latest development build of the Open Liberty Server which supports Jakarta EE 8. This is downloaded and installed thanks to the liberty-maven-plugin which can also deploy the application and start the server.

To run the examples, just access to the corresponding module and invoke this command:

mvn clean package liberty:run

As a result, Maven will download the server, build, deploy, and run the application.

7. Conclusion

In diesem Artikel haben wir die Konfiguration und Implementierung der Hauptfunktionen der neuen Jakarta EE 8-Sicherheits-API behandelt.

Zunächst wurde gezeigt, wie die integrierten Standardauthentifizierungsmechanismen konfiguriert und ein benutzerdefinierter implementiert werden. Später haben wir gesehen, wie der integrierte Identitätsspeicher konfiguriert und ein benutzerdefinierter implementiert wird. Und schließlich haben wir gesehen, wie Methoden des SecurityContext aufgerufen werden .

Wie immer sind die Codebeispiele für diesen Artikel auf GitHub verfügbar.