Integrate 3rd party library with CDI, part 3: use CDI IoC instead of the library one
Let's take the Shiro web case: what do you need to have it running and usable?
- ShiroFilter in place to be able
- to secure URLs
- to initialize the context (Shiro is sadly ThreadLocal based)
- A SecurityManager available somehow to the shiro filter
Add the filter
Let's make this part simple: how to autoconfigure Shiro filter? The answer is easy since Servlet 3.0: a ServletContainerInitialier:
public class ShiroSetup implements ServletContainerInitializer {
@Override
public void onStartup(final Set<Class<?>> set, final ServletContext servletContext) throws ServletException {
final FilterRegistration.Dynamic filter = servletContext.addFilter("shiro", CdiShiroFilter.class);
filter.setAsyncSupported(true);
filter.addMappingForUrlPatterns(
EnumSet.allOf(DispatcherType.class), false,
ofNullable(servletContext.getInitParameter("shiro-cdi.mapping")).orElse("/*"));
}
}
This simple intiializer will just add a filter matching all urls (we support some overriding mapping through an init parameter in web.xml but that's a detail for that post).
Now we need this CdiShiroFilter.
CdiShiroFilter: bind Shiro to the requests
Before seeing how to integrate Shiro and CDI by itself, we need the filter to be active. Shiro already provides its own filter: ShiroFilter and expects the user to switch in web.xml the environment (the SecurityManager factory) to get the setup right. To decrease this configuration we'll create the SecurityManager in our CdiShiroFilter and reuse Shiro logic to ensure it still works. Concretely it means we need to bridge the EnvironmentLoader to our filter. If we don't do that then we'll need to declare a listener in our initializer (this works but makes the configuration even more complicated):
public class CdiShiroFilter extends EnvironmentLoader implements Filter {
private ShiroFilter filter;
private SecurityManager securityManager;
private ServletContext servletContext;
@Override
public void init(final FilterConfig filterConfig) throws ServletException {
filter = new ShiroFilter();
servletContext = filterConfig.getServletContext();
initEnvironment(servletContext);
filter.init(filterConfig);
}
@Override
public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException {
filter.doFilter(servletRequest, servletResponse, filterChain);
}
@Override
public void destroy() {
filter.destroy();
destroyEnvironment(servletContext);
}
}
This filter works (assuming you have a SecurityManager) but will likely not be valid for asynchronous requests. To make it possible to use AsyncContext from Servlet 3, we need to wrap the request.
Before sharing the code doing that, let's assume we have an AsyncContextWrapper just delegating to an instance of AsyncContext (no additional logic):
public class AsyncContextWrapper implements AsyncContext {
private final AsyncContext delegate;
public AsyncContextWrapper(final AsyncContext asyncContext) {
delegate = asyncContext;
}
@Override
public ServletRequest getRequest() {
return delegate.getRequest();
}
@Override
public ServletResponse getResponse() {
return delegate.getResponse();
}
@Override
public boolean hasOriginalRequestAndResponse() {
return delegate.hasOriginalRequestAndResponse();
}
@Override
public void dispatch() {
delegate.dispatch();
}
@Override
public void dispatch(final String s) {
delegate.dispatch(s);
}
@Override
public void dispatch(final ServletContext servletContext, final String s) {
delegate.dispatch(servletContext, s);
}
@Override
public void complete() {
delegate.complete();
}
@Override
public void start(final Runnable runnable) {
delegate.start(runnable);
}
@Override
public void addListener(final AsyncListener asyncListener) {
delegate.addListener(asyncListener);
}
@Override
public void addListener(final AsyncListener asyncListener, final ServletRequest servletRequest, final ServletResponse servletResponse) {
delegate.addListener(asyncListener, servletRequest, servletResponse);
}
@Override
public <T extends AsyncListener> T createListener(final Class<T> aClass) throws ServletException {
return delegate.createListener(aClass);
}
@Override
public void setTimeout(final long l) {
delegate.setTimeout(l);
}
@Override
public long getTimeout() {
return delegate.getTimeout();
}
}
The only method we need to override there is start(Runnable). This one will use a Servlet thread to execute the task but if you use a custom thread pool (EE concurrency utilities?) then we'll use an AsyncListener to propagate the security context which is the only goal there:
@Override
public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException {
filter.doFilter(new HttpServletRequestWrapper(HttpServletRequest.class.cast(servletRequest)) {
@Override
public AsyncContext startAsync() throws IllegalStateException {
return propagate(super.startAsync());
}
@Override
public AsyncContext startAsync(final ServletRequest servletRequest, final ServletResponse servletResponse) throws IllegalStateException {
return propagate(super.startAsync(servletRequest, servletResponse));
}
private AsyncContext propagate(final AsyncContext asyncContext) {
final Subject subject = ThreadContext.getSubject();
final SubjectThreadState state = new SubjectThreadState(subject);
asyncContext.addListener(new AsyncListener() {
@Override
public void onComplete(final AsyncEvent asyncEvent) throws IOException {
state.restore();
}
@Override
public void onTimeout(final AsyncEvent asyncEvent) throws IOException {
state.restore();
}
@Override
public void onError(final AsyncEvent asyncEvent) throws IOException {
state.restore();
}
@Override
public void onStartAsync(final AsyncEvent asyncEvent) throws IOException {
asyncEvent.getAsyncContext().addListener(this);
state.bind();
}
});
return new AsyncContextWrapper(asyncContext) {
@Override
public void start(final Runnable runnable) {
super.start(subject.associateWith(runnable));
}
};
}
}, servletResponse, filterChain);
}
With Shiro the usage of SubjectThreadState makes it quite smooth. You can also see in this snippet how we reused our AsyncContextWrapper to make the actual logic more obvious (we could have done it in the wrapper but it would have been harder to identify the Shiro related code).
Now we have all we need to run except our SecurityManager.
Create the SecurityManager with CDI
Creating the security manager is just a matter of getting a set of defined instances and wire them altogether. The challenge with CDI is to know when they are provided (the user created them) or we need to create a default or just let Shiro create a default.
Here the trick will be to inject all instances and select the present ones. There are two ways to do it:
- inject an Instance
- do a programmatic lookup from the BeanManager
We'll use the first one but note the second one is probably easier for a framework:
public class CdiShiroFilter extends EnvironmentLoader implements Filter {
private SecurityManager securityManager;
@Inject
private Instance<WebSecurityManager> manager;
@Inject
private Instance<Realm> realm;
@Inject
private Instance<Authenticator> authenticator;
@Inject
private Instance<Authorizer> authorizer;
@Inject
private Instance<CacheManager> cacheManager;
@Inject
private Instance<EventBus> eventBus;
@Inject
private Instance<SubjectDAO> subjectDAO;
@Inject
private Instance<SubjectFactory> subjectFactory;
@Inject
private Instance<SessionManager> sessionManager;
@Inject
private Instance<FilterChainResolver> filterChainResolver;
@Inject
private Instance<RememberMeManager> rememberMeManager;
@Inject
private Event<WebEnvironment> environmentEvent;
@Inject
private ShiroExtension extension;
@Override
protected WebEnvironment createEnvironment(final ServletContext sc) {
final DefaultWebEnvironment environment = new DefaultWebEnvironment();
securityManager = configureManager(!extension.isSecurityManager() ? extension.newSecurityManager() : manager.get());
environment.setSecurityManager(securityManager);
if (environment.getFilterChainResolver() == null && !filterChainResolver.isUnsatisfied()) {
environment.setFilterChainResolver(filterChainResolver.get());
}
environmentEvent.fire(environment); // to customize it
return environment;
}
// here we use that philosophy: if set it was configured in the security manager producer otherwise use the produced value if there
private SecurityManager configureManager(final SecurityManager manager) {
if (!DefaultWebSecurityManager.class.isInstance(manager)) {
return manager;
}
final DefaultWebSecurityManager mgr = DefaultWebSecurityManager.class.cast(manager);
if ((mgr.getRealms() == null || mgr.getRealms().isEmpty()) && !realm.isUnsatisfied()) {
mgr.setRealms(stream(realm.spliterator(), false).collect(toList()));
}
if (mgr.getAuthenticator() == null && !authenticator.isUnsatisfied()) {
mgr.setAuthenticator(authenticator.get());
}
if (mgr.getAuthorizer() == null && !authorizer.isUnsatisfied()) {
mgr.setAuthorizer(authorizer.get());
}
if (mgr.getCacheManager() == null && !cacheManager.isUnsatisfied()) {
mgr.setCacheManager(cacheManager.get());
}
if (mgr.getEventBus() == null && !eventBus.isUnsatisfied()) {
mgr.setEventBus(eventBus.get());
}
if (mgr.getSubjectDAO() == null && !subjectDAO.isUnsatisfied()) {
mgr.setSubjectDAO(subjectDAO.get());
}
if (mgr.getSubjectFactory() == null && !subjectFactory.isUnsatisfied()) {
mgr.setSubjectFactory(subjectFactory.get());
} else if (mgr.getSubjectFactory() == null) {
mgr.setSubjectFactory(new DefaultWebSubjectFactory());
}
if (mgr.getSessionManager() == null && !sessionManager.isUnsatisfied()) {
mgr.setSessionManager(sessionManager.get());
}
if (mgr.getRememberMeManager() == null && !rememberMeManager.isUnsatisfied()) {
mgr.setRememberMeManager(rememberMeManager.get());
}
return manager;
}
}
It probably looks a bit complicated but here is the atomic code:
@Inject
Instance<Something> something;
if (mgr.getSomething() == null && !something.isUnsatisfied()) {
mgr.setSomething(something.get());
}
- we inject the type we need
- if the value is not already set (we don't override what the user already did!) and we have a CDI bean matching this type then we set it
Note: you can see for realms we do exactly the same but since you can have multiple realms we convert Instance to a list since it is iterable.
This logic is great but it requires a root instance, to get the root instance we use the same trick but you probably saw we relied on an extension. Why not using Instance?
The answer is simple: we want to be able to inject the SecurityManager so if there is no security manager bean then we need to add a security manager bean (I'll not detail this part but you can do it easily with DeltaSpike BeanBuilder for instance). This part is done during CDI bootstrap so Instance will always have at least one Bean. To avoid to mess up user produced SecurityManager we capture in the extension if we have or not a user SecurityManager and if not we add a custom one through the extension. It looks like that:
public class ShiroExtension implements Extension {
private boolean securityManager;
private SecurityManagerBean bean;
void hasSecurityManager(@Observes final ProcessBean<SecurityManager> securityManagerProcessBean) {
securityManager = securityManager || !SecurityManagerBean.class.isInstance(securityManagerProcessBean.getBean());
}
void addSecurityManagerIfNeeded(@Observes final AfterBeanDiscovery afterBeanDiscovery) {
if (securityManager) {
return;
}
bean = new SecurityManagerBean();
afterBeanDiscovery.addBean(bean);
}
public boolean isSecurityManager() {
return securityManager;
}
public SecurityManager newSecurityManager() {
final WebSecurityManager manager = new DefaultWebSecurityManager();
bean.initSecurityManagerBean(manager);
return manager;
}
}
This way our initialization supports default and user managers and user can after that point use:
@Inject
private SecurityManager manager;
Just for completeness here is the bean implementation but it has nothing original:
import com.github.rmannibucau.shiro.literal.AnyLiteral;
import com.github.rmannibucau.shiro.literal.DefaultLiteral;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.web.mgt.WebSecurityManager;
import javax.enterprise.context.Dependent;
import javax.enterprise.context.spi.CreationalContext;
import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.InjectionPoint;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.HashSet;
import java.util.Set;
import static java.util.Arrays.asList;
import static java.util.Collections.emptySet;
public class SecurityManagerBean implements Bean<WebSecurityManager> {
private final Set<Type> types = new HashSet<>(asList(WebSecurityManager.class, SecurityManager.class, Object.class));
private final Set<Annotation> qualifiers = new HashSet<>(asList(new DefaultLiteral(), new AnyLiteral()));
private WebSecurityManager manager;
public void initSecurityManagerBean(final WebSecurityManager manager) {
this.manager = manager;
}
@Override
public Set<InjectionPoint> getInjectionPoints() {
return emptySet();
}
@Override
public Class<?> getBeanClass() {
return WebSecurityManager.class;
}
@Override
public boolean isNullable() {
return false;
}
@Override
public WebSecurityManager create(final CreationalContext<WebSecurityManager> context) {
return manager;
}
@Override
public void destroy(final WebSecurityManager instance, final CreationalContext<WebSecurityManager> context) {
// no-op
}
@Override
public Set<Type> getTypes() {
return types;
}
@Override
public Set<Annotation> getQualifiers() {
return qualifiers;
}
@Override // avoid proxies
public Class<? extends Annotation> getScope() {
return Dependent.class;
}
@Override
public String getName() {
return null;
}
@Override
public Set<Class<? extends Annotation>> getStereotypes() {
return emptySet();
}
@Override
public boolean isAlternative() {
return false;
}
}
Don't forget the cases you forgot
You can see if you analyze more the initialization that the createEnvironment() method fires an event with the environment (and therefore the security manager). This is not needed by itself but if you forgot one setter or if the framework evolves but not your library it allows end users to observe this event and still customize the instance.
Produce the instances with the right scope
The last challenge Shiro has is the Subject "producing". This instance is the entry point to login, logout, get user information etc...In CDI you likely want it this way:
@Inject
private Subject subject;
Issue is this instance is bound to a thread. In a standard web application @RequestScoped works but for all other cases it doesn't even if Shiro context is well initialized. To solve that the trick I used was to always reevaluate the Subject through a proxy:
@ApplicationScoped
public class SubjectProducer {
@Produces
public Subject subject(final SecurityManager manager) {
return Subject.class.cast(Proxy.newProxyInstance(
Thread.currentThread().getContextClassLoader(),
new Class<?>[]{WebSubject.class},
(proxy, method, args) -> {
try {
final Subject subject = ThreadContext.getSubject();
return method.invoke(subject, args);
} catch (final InvocationTargetException ite) {
throw ite.getCause();
}
}));
}
}
This code will return a global Subject (can be @ApplicationScoped too) but uses Shiro instance each time through ThreadContext accessor.
The limitation of such a code is to not loop, ie don't bind in Shiro ThreadContext the proxy otherwise you'll likely get a stackoverflow but since all this setup is hidden in the filter you don't need and it shouldn't happen. If you really need just use the ThreadContext instead of the injection (but it would be unlikely).
Conclusion
Integrating with CDI a library IoC leads to creating root entities of that framework relying on CDI. There are a lot of ways to do it and sometimes you need to mix them:
- just grab root entities and assume they are intitialized
- initialize well known nested instances from Instance or programmatic lookups after having created manually the root instance
- rarely needed but you can also have a kind of visitor pattern to initialize nested instances too (useful when internals are not CDI friendly by default)
It is also re-thinking the configuration to be less "custom" and in files but more "in the code" to ensure users fully control it and can override it as simply as a bean or CDI component.
Finally it is ensuring you can inject what you produced (the SecurityManager in our sample).
From the same author:
In the same category: