Chapter 7. Jakarta Enterprise Beans Interceptors
Chapter 7. Jakarta Enterprise Beans Interceptors 7.1. Custom Interceptors JBoss EAP allows you to develop and manage custom Jakarta Enterprise Beans interceptors. You can create the following types of interceptors: Client interceptors Client interceptors run when JBoss EAP functions as a client. Server interceptors Server interceptors run when JBoss EAP functions as a server. These interceptors are configured globally for the server. Container Interceptors Container interceptors run when JBoss EAP functions as a server. These interceptors are configured in the Jakarta Enterprise Beans container. Custom interceptor classes should be added to a module and stored in the USDJBOSS_HOME/modules directory. 7.1.1. The Interceptor Chain Custom interceptors are executed at specific points in the interceptor chain. Container interceptors configured for an Jakarta Enterprise Beans are executed before interceptors provided by Wildfly, such as security interceptors or transaction management interceptors. Container interceptors can thus process or configure context data before invocation of Wildfly interceptors or global interceptors. Server and client interceptors are executed after Wildfly-specific interceptors. 7.1.2. Custom Client Interceptors Custom client interceptors implement the org.jboss.ejb.client.EJBClientInterceptor interface. The org.jboss.ejb.client.EJBClientInvocationContext interface should also be included. The following code illustrates an example client interceptor. Client interceptor code example package org.foo; import org.jboss.ejb.client.EJBClientInterceptor; import org.jboss.ejb.client.EJBClientInvocationContext; public class FooInterceptor implements EJBClientInterceptor { @Override public void handleInvocation(EJBClientInvocationContext context) throws Exception { context.sendRequest(); } @Override public Object handleInvocationResult(EJBClientInvocationContext context) throws Exception { return context.getResult(); } } 7.1.3. Custom Server Interceptors Server interceptors use the @javax.annotation.AroundInvoke annotation or the javax.interceptor.AroundTimeout annotation to mark the method that is invoked during the invocation on the bean. The following code illustrates an example server interceptor. Server Interceptor Code Example package org.testsuite.ejb.serverinterceptor; import javax.annotation.PostConstruct; import javax.interceptor.AroundInvoke; import javax.interceptor.InvocationContext; public class TestServerInterceptor { @AroundInvoke public Object aroundInvoke(final InvocationContext invocationContext) throws Exception { return invocationContext.proceed(); } } 7.1.4. Custom Container Interceptors Container interceptors use the @javax.annotation.AroundInvoke annotation or the javax.interceptor.AroundTimeout annotation to mark the method that is invoked during the invocation on the bean. Standard Jakarta EE interceptors, as defined by the Jakarta Enterprise Beans 3.2 specification, are expected to run after the container has completed security context propagation, transaction management, and other container provided invocation processing. The following code illustrates an interceptor class that marks the iAmAround method for invocation. Container Interceptor Code Example public class ClassLevelContainerInterceptor { @AroundInvoke private Object iAmAround(final InvocationContext invocationContext) throws Exception { return this.getClass().getName() + " " + invocationContext.proceed(); } } Differences Between the Container Interceptor and the Jakarta EE Interceptor API Although container interceptors are modeled to be similar to Jakarta EE interceptors, there are some differences in the semantics of the API. For example, it is illegal for container interceptors to invoke the javax.interceptor.InvocationContext.getTarget() method because these interceptors are invoked long before the Jakarta Enterprise Beans components are set up or instantiated. 7.1.5. Configuring a Container Interceptor Container interceptors use the standard Jakarta EE interceptor libraries. Thus they use the same XSD elements that are allowed in the ejb-jar.xml file for the 3.2 version of the ejb-jar deployment descriptor. Because they are based on the standard Jakarta EE interceptor libraries, container interceptors may only be configured using deployment descriptors. By design applications do not require any JBoss EAP-specific annotation or other library dependencies. To configure a container interceptor: Create a jboss-ejb3.xml file in the META-INF/` directory of the Jakarta Enterprise Beans deployment. Configure the container interceptor elements in the descriptor file. Use the urn:container-interceptors:1.0 namespace to specify configuration of container interceptor elements. Use the <container-interceptors> element to specify the container interceptors. Use the <interceptor-binding> elements to bind the container interceptor to the Jakarta Enterprise Beans. The interceptors can be bound in any of the following ways: Bind the interceptor to all the Jakarta Enterprise Beans in the deployment using a wildcard (*). Bind the interceptor at the individual bean level using the specific Jakarta Enterprise Beans name. Bind the interceptor at the specific method level for the Jakarta Enterprise Beans. Note These elements are configured using the Jakarta Enterprise Beans 3.2 XSD in the same way as Jakarta EE interceptors. The following example descriptor file illustrates configuration options. Container Interceptor jboss-ejb3.xml File Example <jboss xmlns="http://www.jboss.com/xml/ns/javaee" xmlns:jee="http://java.sun.com/xml/ns/javaee" xmlns:ci ="urn:container-interceptors:1.0"> <jee:assembly-descriptor> <ci:container-interceptors> <!-- Default interceptor --> <jee:interceptor-binding> <ejb-name>*</ejb-name> <interceptor-class>org.jboss.as.test.integration.ejb.container.interceptor.ContainerInterceptorOne</interceptor-class> </jee:interceptor-binding> <!-- Class level container-interceptor --> <jee:interceptor-binding> <ejb-name>AnotherFlowTrackingBean</ejb-name> <interceptor-class>org.jboss.as.test.integration.ejb.container.interceptor.ClassLevelContainerInterceptor</interceptor-class> </jee:interceptor-binding> <!-- Method specific container-interceptor --> <jee:interceptor-binding> <ejb-name>AnotherFlowTrackingBean</ejb-name> <interceptor-class>org.jboss.as.test.integration.ejb.container.interceptor.MethodSpecificContainerInterceptor</interceptor-class> <method> <method-name>echoWithMethodSpecificContainerInterceptor</method-name> </method> </jee:interceptor-binding> <!-- container interceptors in a specific order --> <jee:interceptor-binding> <ejb-name>AnotherFlowTrackingBean</ejb-name> <interceptor-order> <interceptor-class>org.jboss.as.test.integration.ejb.container.interceptor.ClassLevelContainerInterceptor</interceptor-class> <interceptor-class>org.jboss.as.test.integration.ejb.container.interceptor.MethodSpecificContainerInterceptor</interceptor-class> <interceptor-class>org.jboss.as.test.integration.ejb.container.interceptor.ContainerInterceptorOne</interceptor-class> </interceptor-order> <method> <method-name>echoInSpecificOrderOfContainerInterceptors</method-name> </method> </jee:interceptor-binding> </ci:container-interceptors> </jee:assembly-descriptor> </jboss> The allow-ejb-name-regex attribute allows you to use regular expressions in interceptor bindings and maps the interceptors to all the beans that match the specified regular expression. Use the following management CLI command to enable the allow-ejb-name-regex attribute of the ejb3 subsystem to true : The schema for the urn:container-interceptors:1.0 namespace is available at http://www.jboss.org/schema/jbossas/jboss-ejb-container-interceptors_1_0.xsd . 7.1.6. Server and Client Interceptor Configuration Server and client interceptors are added globally to the JBoss EAP configuration in the configuration file being used. Server interceptors are added to the <server-interceptors> element in the ejb3 subsystem configuration. Client interceptors are added to the <client-interceptors> element in the ejb3 subsystem configuration. The following example illustrates adding a server interceptor. The following example illustrates adding a client interceptor. Whenever a server interceptor or client interceptor is added or the configuration of an interceptor is changed, the server must be reloaded. 7.1.7. Changing the Security Context Identity Rather than open multiple client connections, you can give permission to the authenticated user to switch identities and execute a request on the existing connection as a different user. By default, when you make a remote call to an Jakarta Enterprise Beans that is deployed to the application server, the connection to the server is authenticated and any subsequent requests that use the connection are executed using the original authenticated identity. This is true for both client-to-server and server-to-server calls. If you need to use different identities from the same client, normally you must open multiple connections to the server so that each one is authenticated as a different identity. Instead, you can allow the authenticated user to change identities. To change the identity of the authenticated user: Implement the change of identity in the interceptor code. Client interceptors The interceptor must pass the requested identity through the context data map, which can be obtained by using a call to EJBClientInvocationContext.getContextData() . The following example code illustrates a client interceptor that switches identities. Client Interceptor Code Example public class ClientSecurityInterceptor implements EJBClientInterceptor { public void handleInvocation(EJBClientInvocationContext context) throws Exception { Principal currentPrincipal = SecurityActions.securityContextGetPrincipal(); if (currentPrincipal != null) { Map<String, Object> contextData = context.getContextData(); contextData.put(ServerSecurityInterceptor.DELEGATED_USER_KEY, currentPrincipal.getName()); } context.sendRequest(); } public Object handleInvocationResult(EJBClientInvocationContext context) throws Exception { return context.getResult(); } } Container and server interceptors These interceptors receive the InvocationContext containing the identity and make the request to switch to that new identity. The following code illustrates an abridged example for a container interceptor: Container Interceptor Code Example public class ServerSecurityInterceptor { private static final Logger logger = Logger.getLogger(ServerSecurityInterceptor.class); static final String DELEGATED_USER_KEY = ServerSecurityInterceptor.class.getName() + ".DelegationUser"; @AroundInvoke public Object aroundInvoke(final InvocationContext invocationContext) throws Exception { Principal desiredUser = null; UserPrincipal connectionUser = null; Map<String, Object> contextData = invocationContext.getContextData(); if (contextData.containsKey(DELEGATED_USER_KEY)) { desiredUser = new SimplePrincipal((String) contextData.get(DELEGATED_USER_KEY)); Collection<Principal> connectionPrincipals = SecurityActions.getConnectionPrincipals(); if (connectionPrincipals != null) { for (Principal current : connectionPrincipals) { if (current instanceof UserPrincipal) { connectionUser = (UserPrincipal) current; break; } } } else { throw new IllegalStateException("Delegation user requested but no user on connection found."); } } ContextStateCache stateCache = null; try { if (desiredUser != null && connectionUser != null && (desiredUser.getName().equals(connectionUser.getName()) == false)) { // The final part of this check is to verify that the change does actually indicate a change in user. try { // We have been requested to use an authentication token // so now we attempt the switch. stateCache = SecurityActions.pushIdentity(desiredUser, new OuterUserCredential(connectionUser)); } catch (Exception e) { logger.error("Failed to switch security context for user", e); // Don't propagate the exception stacktrace back to the client for security reasons throw new EJBAccessException("Unable to attempt switching of user."); } } return invocationContext.proceed(); } finally { // switch back to original context if (stateCache != null) { SecurityActions.popIdentity(stateCache);; } } } An application can insert a client interceptor into the EJBClientContext interceptor chain programmatically or by using the service loader mechanism. For instructions to configure a client interceptor, see Using a Client Interceptor in an Application . Create a Jakarta Authentication login module. The Jakarta Authentication LoginModule component is responsible for verifying that the user is allowed to execute requests as the requested identity. The following abridged code example shows the methods that perform the login and validation: LoginModule Code Example @SuppressWarnings("unchecked") @Override public boolean login() throws LoginException { if (super.login() == true) { log.debug("super.login()==true"); return true; } // Time to see if this is a delegation request. NameCallback ncb = new NameCallback("Username:"); ObjectCallback ocb = new ObjectCallback("Password:"); try { callbackHandler.handle(new Callback[] { ncb, ocb }); } catch (Exception e) { if (e instanceof RuntimeException) { throw (RuntimeException) e; } // If the CallbackHandler can not handle the required callbacks then no chance. return false; } String name = ncb.getName(); Object credential = ocb.getCredential(); if (credential instanceof OuterUserCredential) { // This credential type will only be seen for a delegation request, if not seen then the request is not for us. if (delegationAcceptable(name, (OuterUserCredential) credential)) { identity = new SimplePrincipal(name); if (getUseFirstPass()) { String userName = identity.getName(); if (log.isDebugEnabled()) log.debug("Storing username '" + userName + "' and empty password"); // Add the username and an empty password to the shared state map sharedState.put("javax.security.auth.login.name", identity); sharedState.put("javax.security.auth.login.password", ""); } loginOk = true; return true; } } return false; // Attempted login but not successful. } // Make a trust user to decide if the user switch is acceptable. protected boolean delegationAcceptable(String requestedUser, OuterUserCredential connectionUser) { if (delegationMappings == null) { return false; } String[] allowedMappings = loadPropertyValue(connectionUser.getName(), connectionUser.getRealm()); if (allowedMappings.length == 1 && "*".equals(allowedMappings[0])) { // A wild card mapping was found. return true; } for (String current : allowedMappings) { if (requestedUser.equals(current)) { return true; } } return false; } 7.1.8. Using a Client Interceptor in an Application An application can insert a client interceptor into the EJBClientContext interceptor chain programmatically, using the service loader mechanism, or using the ClientInterceptors annotation. Note An EJBClientInterceptor can request specific data from the server side invocation context by calling org.jboss.ejb.client.EJBClientInvocationContext#addReturnedContextDataKey( String key ) . If the requested data is present under the provided key in the context data map, it is sent to the client. 7.1.8.1. Inserting a Client Interceptor Programmatically After creating an EJBClientContext with the interceptor registered, insert the interceptor. The following code illustrates how to create an EJBClientContext with the interceptor registration: EJBClientContext ctxWithInterceptors = EJBClientContext.getCurrent().withAddedInterceptors(clientInterceptor); After creating the EJBClientContext , two options are available to insert the interceptor: You can run the following code with EJBClientContext applied using a Callable operation. Jakarta Enterprise Beans calls performed within the Callable operation will apply the client-side interceptors: ctxWithInterceptors.runCallable(() -> { // perform the calls which should use the interceptor }) Alternatively you can mark the newly created EJBClientContext as the new default: EJBClientContext.getContextManager().setThreadDefault(ctxWithInterceptors); 7.1.8.2. Inserting a Client Interceptor Using the Service Loader Mechanism Create a META-INF/services/org.jboss.ejb.client.EJBClientInterceptor file and place or package it in the class path of the client application. The rules for the file are dictated by the Java ServiceLoader Mechanism . This file is expected to contain a separate line for each fully qualified class name of the Jakarta Enterprise Beans client interceptor implementation. The Jakarta Enterprise Beans client interceptor classes must be available in the class path. Jakarta Enterprise Beans client interceptors that are added using the service loader mechanism are added in the order they are found in the class path and are added to the end of the client interceptor chain. 7.1.8.3. Inserting a Client Interceptor Using the ClientInterceptor Annotation The @org.jboss.ejb.client.annotation.ClientInterceptors annotation allows you to place the Jakarta Enterprise Beans interceptor in the client-side of the remote call. import org.jboss.ejb.client.annotation.ClientInterceptors; @ClientInterceptors({HelloClientInterceptor.class}) public interface HelloBeanRemote { public String hello(); }
package org.foo; import org.jboss.ejb.client.EJBClientInterceptor; import org.jboss.ejb.client.EJBClientInvocationContext; public class FooInterceptor implements EJBClientInterceptor { @Override public void handleInvocation(EJBClientInvocationContext context) throws Exception { context.sendRequest(); } @Override public Object handleInvocationResult(EJBClientInvocationContext context) throws Exception { return context.getResult(); } } package org.testsuite.ejb.serverinterceptor; import javax.annotation.PostConstruct; import javax.interceptor.AroundInvoke; import javax.interceptor.InvocationContext; public class TestServerInterceptor { @AroundInvoke public Object aroundInvoke(final InvocationContext invocationContext) throws Exception { return invocationContext.proceed(); } } public class ClassLevelContainerInterceptor { @AroundInvoke private Object iAmAround(final InvocationContext invocationContext) throws Exception { return this.getClass().getName() + " " + invocationContext.proceed(); } } <jboss xmlns="http://www.jboss.com/xml/ns/javaee" xmlns:jee="http://java.sun.com/xml/ns/javaee" xmlns:ci ="urn:container-interceptors:1.0"> <jee:assembly-descriptor> <ci:container-interceptors> <!-- Default interceptor --> <jee:interceptor-binding> <ejb-name>*</ejb-name> <interceptor-class>org.jboss.as.test.integration.ejb.container.interceptor.ContainerInterceptorOne</interceptor-class> </jee:interceptor-binding> <!-- Class level container-interceptor --> <jee:interceptor-binding> <ejb-name>AnotherFlowTrackingBean</ejb-name> <interceptor-class>org.jboss.as.test.integration.ejb.container.interceptor.ClassLevelContainerInterceptor</interceptor-class> </jee:interceptor-binding> <!-- Method specific container-interceptor --> <jee:interceptor-binding> <ejb-name>AnotherFlowTrackingBean</ejb-name> <interceptor-class>org.jboss.as.test.integration.ejb.container.interceptor.MethodSpecificContainerInterceptor</interceptor-class> <method> <method-name>echoWithMethodSpecificContainerInterceptor</method-name> </method> </jee:interceptor-binding> <!-- container interceptors in a specific order --> <jee:interceptor-binding> <ejb-name>AnotherFlowTrackingBean</ejb-name> <interceptor-order> <interceptor-class>org.jboss.as.test.integration.ejb.container.interceptor.ClassLevelContainerInterceptor</interceptor-class> <interceptor-class>org.jboss.as.test.integration.ejb.container.interceptor.MethodSpecificContainerInterceptor</interceptor-class> <interceptor-class>org.jboss.as.test.integration.ejb.container.interceptor.ContainerInterceptorOne</interceptor-class> </interceptor-order> <method> <method-name>echoInSpecificOrderOfContainerInterceptors</method-name> </method> </jee:interceptor-binding> </ci:container-interceptors> </jee:assembly-descriptor> </jboss> /subsystem=ejb3:write-attribute(name=allow-ejb-name-regex,value=true) /subsystem=ejb3:list-add(name=server-interceptors,value={module=org.abccorp:tracing-interceptors:1.0,class=org.abccorp.TracingInterceptor}) /subsystem=ejb3:list-add(name=client-interceptors,value={module=org.abccorp:clientInterceptor:1.0,class=org.abccorp.clientInterceptor}) public class ClientSecurityInterceptor implements EJBClientInterceptor { public void handleInvocation(EJBClientInvocationContext context) throws Exception { Principal currentPrincipal = SecurityActions.securityContextGetPrincipal(); if (currentPrincipal != null) { Map<String, Object> contextData = context.getContextData(); contextData.put(ServerSecurityInterceptor.DELEGATED_USER_KEY, currentPrincipal.getName()); } context.sendRequest(); } public Object handleInvocationResult(EJBClientInvocationContext context) throws Exception { return context.getResult(); } } public class ServerSecurityInterceptor { private static final Logger logger = Logger.getLogger(ServerSecurityInterceptor.class); static final String DELEGATED_USER_KEY = ServerSecurityInterceptor.class.getName() + ".DelegationUser"; @AroundInvoke public Object aroundInvoke(final InvocationContext invocationContext) throws Exception { Principal desiredUser = null; UserPrincipal connectionUser = null; Map<String, Object> contextData = invocationContext.getContextData(); if (contextData.containsKey(DELEGATED_USER_KEY)) { desiredUser = new SimplePrincipal((String) contextData.get(DELEGATED_USER_KEY)); Collection<Principal> connectionPrincipals = SecurityActions.getConnectionPrincipals(); if (connectionPrincipals != null) { for (Principal current : connectionPrincipals) { if (current instanceof UserPrincipal) { connectionUser = (UserPrincipal) current; break; } } } else { throw new IllegalStateException("Delegation user requested but no user on connection found."); } } ContextStateCache stateCache = null; try { if (desiredUser != null && connectionUser != null && (desiredUser.getName().equals(connectionUser.getName()) == false)) { // The final part of this check is to verify that the change does actually indicate a change in user. try { // We have been requested to use an authentication token // so now we attempt the switch. stateCache = SecurityActions.pushIdentity(desiredUser, new OuterUserCredential(connectionUser)); } catch (Exception e) { logger.error("Failed to switch security context for user", e); // Don't propagate the exception stacktrace back to the client for security reasons throw new EJBAccessException("Unable to attempt switching of user."); } } return invocationContext.proceed(); } finally { // switch back to original context if (stateCache != null) { SecurityActions.popIdentity(stateCache);; } } } @SuppressWarnings("unchecked") @Override public boolean login() throws LoginException { if (super.login() == true) { log.debug("super.login()==true"); return true; } // Time to see if this is a delegation request. NameCallback ncb = new NameCallback("Username:"); ObjectCallback ocb = new ObjectCallback("Password:"); try { callbackHandler.handle(new Callback[] { ncb, ocb }); } catch (Exception e) { if (e instanceof RuntimeException) { throw (RuntimeException) e; } // If the CallbackHandler can not handle the required callbacks then no chance. return false; } String name = ncb.getName(); Object credential = ocb.getCredential(); if (credential instanceof OuterUserCredential) { // This credential type will only be seen for a delegation request, if not seen then the request is not for us. if (delegationAcceptable(name, (OuterUserCredential) credential)) { identity = new SimplePrincipal(name); if (getUseFirstPass()) { String userName = identity.getName(); if (log.isDebugEnabled()) log.debug("Storing username '" + userName + "' and empty password"); // Add the username and an empty password to the shared state map sharedState.put("javax.security.auth.login.name", identity); sharedState.put("javax.security.auth.login.password", ""); } loginOk = true; return true; } } return false; // Attempted login but not successful. } // Make a trust user to decide if the user switch is acceptable. protected boolean delegationAcceptable(String requestedUser, OuterUserCredential connectionUser) { if (delegationMappings == null) { return false; } String[] allowedMappings = loadPropertyValue(connectionUser.getName(), connectionUser.getRealm()); if (allowedMappings.length == 1 && "*".equals(allowedMappings[0])) { // A wild card mapping was found. return true; } for (String current : allowedMappings) { if (requestedUser.equals(current)) { return true; } } return false; } EJBClientContext ctxWithInterceptors = EJBClientContext.getCurrent().withAddedInterceptors(clientInterceptor); ctxWithInterceptors.runCallable(() -> { // perform the calls which should use the interceptor }) EJBClientContext.getContextManager().setThreadDefault(ctxWithInterceptors); import org.jboss.ejb.client.annotation.ClientInterceptors; @ClientInterceptors({HelloClientInterceptor.class}) public interface HelloBeanRemote { public String hello(); }