How to register DispatcherServlets and override onStartup when extending SpringBootServletInitializer - spring

I have a spring boot application that is being deployed to an external instance of Tomcat. The main class extends SpringBootServletInitializer and overrides the methods configure and onStartup. In the method configure, a custom environment is initialized to provide an encryptor for decrypting user passwords for activemq because jasypt is not initialized until after activemq. This I got from the jasypt-spring-boot documentation.
#Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
try {
StandardEncryptableServletEnvironment env = StandardEncryptableServletEnvironment.builder().encryptor(encryptor).build();
return application.environment(env).sources(AxleServer.class);
} catch (FileNotFoundException e) {
logger.error("Could not load encryptable environment", e);
return application.sources(AxleServer.class);
}
}
In the method onStartup I am configuring DispatcherServlets:
#Override
public void onStartup(ServletContext servletContext) throws ServletException {
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(RemotingConfig.class);
servletContext.addListener(new ContextLoaderListener(context));
ServletRegistration.Dynamic restDispatcher = servletContext.addServlet("rest-dispatcher", new DispatcherServlet(context));
restDispatcher.setLoadOnStartup(1);
restDispatcher.addMapping("/rest/*");
ServletRegistration.Dynamic remotingDispatcher = servletContext.addServlet("remoting-dispatcher", new DispatcherServlet(context));
remotingDispatcher.setLoadOnStartup(2);
remotingDispatcher.addMapping("/remoting/*");
}
The problem is that the configure method is never called because it is called from the super implementation of onStartup. I tried calling the super implementation super.onStartup(servletContext); but then I get an error that a root context already exists when deploying the app.
What is the correct way to register the DispatcherServlets and to override the onStartup method? Is it required to call the super implementation of onStartup?

I was able to solve this issue by first calling the super.onStartup and then getting the root context from ServletContext attributes
#Override
public void onStartup(ServletContext servletContext) throws ServletException {
super.onStartup(servletContext);
Object obj = servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
if(obj instanceof AnnotationConfigServletWebServerApplicationContext) {
AnnotationConfigServletWebServerApplicationContext context = (AnnotationConfigServletWebServerApplicationContext) obj;
context.register(RemotingConfig.class);
ServletRegistration.Dynamic restDispatcher = servletContext.addServlet("rest-dispatcher", new DispatcherServlet(context));
restDispatcher.setLoadOnStartup(1);
restDispatcher.addMapping("/rest/*");
ServletRegistration.Dynamic remotingDispatcher = servletContext.addServlet("remoting-dispatcher", new DispatcherServlet(context));
remotingDispatcher.setLoadOnStartup(2);
remotingDispatcher.addMapping("/remoting/*");
}
}

Related

How to set equivalent of web.xml JNDI <env-entry> in a Spring Boot project?

Referring to this SO answer, I'd like to setup the equivalent of this web.xml configuration in a JSF / JoinFaces / SpringBoot application (that doesn't have web.xml).
<env-entry>
<env-entry-name>jsf/ClientSideSecretKey</env-entry-name>
<env-entry-type>java.lang.String</env-entry-type>
<env-entry-value>[AES key in Base64 format]</env-entry-value>
</env-entry>
Any pointers?
If you are using spring boot and embedded tomcat server, you can add <env-entry> programmatically with the following configuration.
#SpringBootApplication
public class DemoApplication extends SpringBootServletInitializer {
#Bean
public TomcatServletWebServerFactory tomcatFactory() {
return new TomcatServletWebServerFactory() {
#Override
protected TomcatWebServer getTomcatWebServer(org.apache.catalina.startup.Tomcat tomcat) {
tomcat.enableNaming();
return super.getTomcatWebServer(tomcat);
}
#Override
protected void postProcessContext(Context context) {
// adding <resource-ref>
ContextResource resource = new ContextResource();
resource.setName("jdbc/myJndiResource");
resource.setType(DataSource.class.getName());
resource.setProperty("driverClassName", "org.postgresql.Driver");
resource.setProperty("url", "jdbc:postgresql://hostname:port/dbname");
resource.setProperty("username", "username");
resource.setProperty("password", "password");
context.getNamingResources()
.addResource(resource);
// adding <env-entry>
ContextEnvironment ce = new ContextEnvironment();
ce.setName("jsf/ClientSideSecretKey");
ce.setType(String.class.getName());
ce.setValue("[AES key in Base64 format]");
context.getNamingResources().addEnvironment(ce);
}
};
}
public static void main(String[] args) throws NamingException {
SpringApplication.run(DemoApplication.class, args);
}
}
Once defined the jndi naming resources they can be accessed in your application using JndiTemplate of InitialContext.
JndiTemplate jndiTemplate = new JndiTemplate();
String str = (String) jndiTemplate.lookup("java:comp/env/jsf/ClientSideSecretKey");
Hope this helps you in resolving your problem.
Essentially <env-entry> declares a web application context attribute.
You can initialize your servlet context and provide the equivalent servlet context attributes in your Spring Boot application.
For that purpose, you can register a bean that implements the ServletContextInitializer interface (or WebApplicationInitializer if your app has to be deployed in a traditional servlet container). For example:
public class JsfServletContextInitializer implements ServletContextInitializer {
#Override
public void onStartup(ServletContext servletContext) throws ServletException {
servletContext.setAttribute("jsf/ClientSideSecretKey", "[AES key in Base64 format]");
}
}
Do not forget to register it as a bean in your configuration.

How to register the Spring MVC dispatcher servlet with an embedded tomcat without spring-boot?

My question is similar to this one Embedded Tomcat Integrated With Spring. I want to run a Spring MVC Dispatcher Servlet on an embedded Tomcat. But I always end up with an exception saying that the WebApplicationObjectSupport instance does not run within a ServletContext.
My example has just the two classes:
class Application {
public static void main(String[] args) throws LifecycleException, ServletException {
try (AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext()) {
context.registerShutdownHook();
context.register(WebConfig.class);
context.refresh();
Tomcat tomcat = new Tomcat();
tomcat.setPort(9090);
File base = new File("");
System.out.println(base.getAbsolutePath());
Context rootCtx = tomcat.addWebapp("", base.getAbsolutePath());
DispatcherServlet dispatcher = new DispatcherServlet(context);
Tomcat.addServlet(rootCtx, "SpringMVC", dispatcher);
rootCtx.addServletMapping("/*", "SpringMVC");
tomcat.start();
tomcat.getServer().await();
}
}
}
#Configuration
#EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
#Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations("classpath:/assets/");
}
#Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("redirect:index.html");
}
}
How do I have to define the servlet context differently then by calling the tomcat.addWebApp(..) method? Does anybody have an example how the Spring MVC dispatcher can be used with an embedded tomcat but without boot?
You can create a ServletContextInitializer and sneak it in via #Configuration:
#Configuration
class WebAppInitConfig implements ServletContextInitializer {
#Override
void onStartup(ServletContext context) {
AnnotationConfigWebApplicationContext webAppContext = new AnnotationConfigWebApplicationContext()
webAppContext.register(RootConfig)
webAppContext.registerShutdownHook()
ServletRegistration.Dynamic dispatcher = context.addServlet("dispatcher", new DispatcherServlet(webAppContext))
dispatcher.loadOnStartup = 1
dispatcher.addMapping("/*")
// Whatever else you need
}
}

SpringMVC, using two contexts and two dispatchers for Rest Service and normal contents

In my SpringMVC project configuration, I have a RootContextConfiguration and other two context configuration files for rest services and normal requests namely; RestServletContextConfiguration and WebServletContextConfiguration.
And, I'm bootstrapping the application as in the following code.
public class Bootstrap implements WebApplicationInitializer
{
#Override
public void onStartup(ServletContext container) throws ServletException
{
container.getServletRegistration("default").addMapping("/resource/*");
AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
rootContext.register(RootContextConfiguration.class);
container.addListener(new ContextLoaderListener(rootContext));
AnnotationConfigWebApplicationContext restContext = new AnnotationConfigWebApplicationContext();
restContext.register(RestServletContextConfiguration.class);
DispatcherServlet restServlet = new DispatcherServlet(restContext);
restServlet.setDispatchOptionsRequest(true);
ServletRegistration.Dynamic springRestDispatcher = container.addServlet("springRestDispatcher", restServlet);
springRestDispatcher.setLoadOnStartup(1);
springRestDispatcher.addMapping("/api/*");
AnnotationConfigWebApplicationContext webContext = new AnnotationConfigWebApplicationContext();
webContext.register(WebServletContextConfiguration.class);
DispatcherServlet webServlet = new DispatcherServlet(webContext);
ServletRegistration.Dynamic springWebDispatcher = container.addServlet("springWebDispatcher", webServlet );
springWebDispatcher.setLoadOnStartup(2);
springWebDispatcher.setMultipartConfig(new MultipartConfigElement(null, 20_971_520L, 41_943_040L, 512_000));
springWebDispatcher.addMapping("/*");
}
}
I need /api/cars to resolve to
#RestController
#RequestMapping("/cars")
class CarRestController{}
And /cars to resolve to
#Controller
#RequestMapping("/cars")
class CarController{}
However, deployment fails because of ambiguous mapping. If I change the mapping of CarRestController to #RequestMapping('/api/cars') then I can access that controller with the path /api/api/cars (Note the double api prefix). But what I want is to be able to access the CarRestController with /api/cars.
What should I do to achieve my goal?. Highly appreciate your help.
Why you dont put mapping #RequestMapping on your method ?
For example
#RestController
public class YourClass {
#RequestMapping("/car")
public String yourMethod() { }
}

Multiple servlet mappings in Spring Boot

Is there any way to set via property 'context-path' many mappings for a same Spring Boot MVC application? My goal is to avoid creating many 'Dispatcherservlet' for the uri mapping.
For example:
servlet.context-path =/, /context1, context2
You can create #Bean annotated method which returns ServletRegistrationBean , and add multiple mappings there. This is more preferable way, as Spring Boot encourage Java configuration rather than config files:
#Bean
public ServletRegistrationBean myServletRegistration()
{
String urlMapping1 = "/mySuperApp/service1/*";
String urlMapping2 = "/mySuperApp/service2/*";
ServletRegistrationBean registration = new ServletRegistrationBean(new MyBeautifulServlet(), urlMapping1, urlMapping2);
//registration.set... other properties may be here
return registration;
}
On application startup you'll be able to see in logs:
INFO | localhost | org.springframework.boot.web.servlet.ServletRegistrationBean | Mapping servlet: 'MyBeautifulServlet' to [/mySuperApp/service1/*, /mySuperApp/service2/*]
You only need a single Dispatcherservlet with a root context path set to what you want (could be / or mySuperApp).
By declaring multiple #RequestMaping, you will be able to serve different URI with the same DispatcherServlet.
Here is an example. Setting the DispatcherServlet to /mySuperApp with #RequestMapping("/service1") and #RequestMapping("/service2") would exposed the following endpoints :
/mySuperApp/service1
/mySuperApp/service2
Having multiple context for a single servlet is not part of the Servlet specification. A single servlet cannot serve from multiple context.
What you can do is map multiple values to your requesting mappings.
#RequestMapping({"/context1/service1}", {"/context2/service1}")
I don't see any other way around it.
You can use 'server.contextPath' property placeholder to set context path for the entire spring boot application. (e.g. server.contextPath=/live/path1)
Also, you can set class level context path that will be applied to all the methods e.g.:
#RestController
#RequestMapping(value = "/testResource", produces = MediaType.APPLICATION_JSON_VALUE)
public class TestResource{
#RequestMapping(method = RequestMethod.POST, value="/test", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<TestDto> save(#RequestBody TestDto testDto) {
...
With this structure, you can use /live/path1/testResource/test to execute save method.
None of the answers to this sort of question seem to mention that you'd normally solve this problem by configuring a reverse proxy in front of the application (eg nginx/apache httpd) to rewrite the request.
However if you must do it in the application then this method works (with Spring Boot 2.6.2 at least) : https://www.broadleafcommerce.com/blog/configuring-a-dynamic-context-path-in-spring-boot.
It describes creating a filter, putting it early in the filter chain and basically re-writing the URL (like a reverse proxy might) so that requests all go to the same place (ie the actual servlet.context-path).
I've found an alternative to using a filter described in https://www.broadleafcommerce.com/blog/configuring-a-dynamic-context-path-in-spring-boot that requires less code.
This uses RewriteValve (https://tomcat.apache.org/tomcat-9.0-doc/rewrite.html) to rewrite urls outside of the context path e.g. if the real context path is "context1" then it will map /context2/* to /context1/*
#Component
public class LegacyUrlWebServerFactoryCustomizer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
private static final List<String> LEGACY_PATHS = List.of("context2", "context3");
#Override
public void customize(TomcatServletWebServerFactory factory) {
RewriteValve rewrite = new RewriteValve() {
#Override
protected void initInternal() throws LifecycleException {
super.initInternal();
try {
String config = LEGACY_PATHS.stream() //
.map(p -> String.format("RewriteRule ^/%s(/.*)$ %s$1", p, factory.getContextPath())) //
.collect(Collectors.joining("\n"));
setConfiguration(config);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
};
factory.addEngineValves(rewrite);
}
}
If you need to use HTTP redirects instead then there is a little bit more required (to avoid a NullPointerException in sendRedirect):
#Component
public class LegacyUrlWebServerFactoryCustomizer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
private static final List<String> LEGACY_PATHS = List.of("context2", "context3");
#Override
public void customize(TomcatServletWebServerFactory factory) {
RewriteValve rewrite = new RewriteValve() {
#Override
protected void initInternal() throws LifecycleException {
super.initInternal();
try {
String config = LEGACY_PATHS.stream() //
.map(p -> String.format("RewriteRule ^/%s(/.*)$ %s$1 R=permanent", p, factory.getContextPath())) //
.collect(Collectors.joining("\n"));
setConfiguration(config);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
#Override
public void invoke(Request request, Response response) throws IOException, ServletException {
if (request.getContext() == null) {
String[] s = request.getRequestURI().split("/");
if (s.length > 1 && LEGACY_PATHS.contains(s[1])) {
request.getMappingData().context = new FailedContext();
}
}
super.invoke(request, response);
}
};
factory.addEngineValves(rewrite);
}
}
I use this approach:
import javax.servlet.ServletContext;
import javax.servlet.ServletRegistration;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
#Configuration
public class WebAppInitializer implements WebApplicationInitializer {
#Override
public void onStartup(ServletContext servletContext) {
AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
rootContext.register(AppConfig.class);
rootContext.setServletContext(servletContext);
ServletRegistration.Dynamic dispatcher = servletContext.addServlet("dispatcher", new DispatcherServlet(rootContext));
dispatcher.setLoadOnStartup(1);
dispatcher.addMapping("/mapping1/*");
dispatcher.addMapping("/mapping2/*");
servletContext.addListener(new ContextLoaderListener(rootContext));
}
}

Map Shiro's AuthenticationException with Jersey's ExceptionMapper

Preface
First of all, my sincerest apologies for this question being extremely long, but I honestly have no idea on how to shorten it, since each part is kind of a special case. Admittedly, I may be blind on this since I am banging my head against the wall for a couple of days now and I am starting to get desperate.
My utmost respect and thankfulness to all of you who read through it.
The aim
I would like to be able to map Shiro's AuthenticationException and it's subclasses to JAX-RS Responses by using Jersey ExceptionMappers, set up using a Guice 3.0 Injector which creates an embedded Jetty.
The environment
Guice 3.0
Jetty 9.2.12.v20150709
Jersey 1.19.1
Shiro 1.2.4
The setup
The embedded Jetty is created using a Guice Injector
// imports omitted for brevity
public class Bootstrap {
public static void main(String[] args) throws Exception {
/*
* The ShiroWebModule is passed as a class
* since it needs a ServletContext to be initialized
*/
Injector injector = Guice.createInjector(new ServerModule(MyShiroWebModule.class));
Server server = injector.getInstance(Server.class);
server.start();
server.join();
}
}
The ServerModule binds a Provider for the Jetty Server:
public class ServerModule extends AbstractModule {
Class<? extends ShiroWebModule> clazz;
public ServerModule(Class <?extends ShiroWebModule> clazz) {
this.clazz = clazz;
}
#Override
protected void configure() {
bind(Server.class)
.toProvider(JettyProvider.withShiroWebModule(clazz))
.in(Singleton.class);
}
}
The JettyProvider sets up a Jetty WebApplicationContext, registers the ServletContextListener necessary for Guice and a few things more, which I left in to make sure no "side effects" may be hidden:
public class JettyProvider implements Provider<Server>{
#Inject
Injector injector;
#Inject
#Named("server.Port")
Integer port;
#Inject
#Named("server.Host")
String host;
private Class<? extends ShiroWebModule> clazz;
private static Server server;
private JettyProvider(Class<? extends ShiroWebModule> clazz){
this.clazz = clazz;
}
public static JettyProvider withShiroWebModule(Class<? extends ShiroWebModule> clazz){
return new JettyProvider(clazz);
}
public Server get() {
WebAppContext webAppContext = new WebAppContext();
webAppContext.setContextPath("/");
// Set during testing only
webAppContext.setResourceBase("src/main/webapp/");
webAppContext.setParentLoaderPriority(true);
webAppContext.addEventListener(
new MyServletContextListener(injector,clazz)
);
webAppContext.addFilter(
GuiceFilter.class, "/*",
EnumSet.allOf(DispatcherType.class)
);
webAppContext.setThrowUnavailableOnStartupException(true);
QueuedThreadPool threadPool = new QueuedThreadPool(500, 10);
server = new Server(threadPool);
ServerConnector connector = new ServerConnector(server);
connector.setHost(this.host);
connector.setPort(this.port);
RequestLogHandler requestLogHandler = new RequestLogHandler();
requestLogHandler.setRequestLog(new NCSARequestLog());
HandlerCollection handlers = new HandlerCollection(true);
handlers.addHandler(webAppContext);
handlers.addHandler(requestLogHandler);
server.addConnector(connector);
server.setStopAtShutdown(true);
server.setHandler(handlers);
return server;
}
}
In MyServletContextListener, I created a child injector, which gets initialized with the JerseyServletModule:
public class MyServletContextListener extends GuiceServletContextListener {
private ServletContext servletContext;
private Injector injector;
private Class<? extends ShiroWebModule> shiroModuleClass;
private ShiroWebModule module;
public ServletContextListener(Injector injector,
Class<? extends ShiroWebModule> clazz) {
this.injector = injector;
this.shiroModuleClass = clazz;
}
#Override
public void contextInitialized(ServletContextEvent servletContextEvent) {
this.servletContext = servletContextEvent.getServletContext();
super.contextInitialized(servletContextEvent);
}
#Override
protected Injector getInjector() {
/*
* Since we finally have our ServletContext
* we can now instantiate our ShiroWebModule
*/
try {
module = shiroModuleClass.getConstructor(ServletContext.class)
.newInstance(this.servletContext);
} catch (InstantiationException | IllegalAccessException
| IllegalArgumentException | InvocationTargetException
| NoSuchMethodException | SecurityException e) {
e.printStackTrace();
}
/*
* Now, we create a child injector with the JerseyModule
*/
Injector child = injector.createChildInjector(module,
new JerseyModule());
SecurityManager securityManager = child
.getInstance(SecurityManager.class);
SecurityUtils.setSecurityManager(securityManager);
return child;
}
}
The JerseyModule, a subclass of JerseyServletModule now put everything together:
public class JerseyModule extends JerseyServletModule {
#Override
protected void configureServlets() {
bindings();
filters();
}
private void bindings() {
bind(DefaultServlet.class).asEagerSingleton();
bind(GuiceContainer.class).asEagerSingleton();
serve("/*").with(DefaultServlet.class);
}
private void filters() {
Map<String, String> params = new HashMap<String, String>();
// Make sure Jersey scans the package
params.put("com.sun.jersey.config.property.packages",
"com.example.webapp");
params.put("com.sun.jersey.config.feature.Trace", "true");
filter("/*").through(GuiceShiroFilter.class,params);
filter("/*").through(GuiceContainer.class, params);
/*
* Although the ExceptionHandler is already found by Jersey
* I bound it manually to be sure
*/
bind(ExceptionHandler.class);
bind(MyService.class);
}
}
The ExceptionHandler is extremely straightforward and looks like this:
#Provider
#Singleton
public class ExceptionHandler implements
ExceptionMapper<AuthenticationException> {
public Response toResponse(AuthenticationException exception) {
return Response
.status(Status.UNAUTHORIZED)
.entity("auth exception handled")
.build();
}
}
The problem
Now everything works fine when I want to access a restricted resource and enter correct principal/credential combinations. But as soon as enter a non-existing user or a wrong password, I want an AuthenticationException to be thrown by Shiro and I want it to be handled by the above ExceptionHandler.
Utilizing the default AUTHC filter provided by Shiro in the beginning, I noticed that AuthenticationExceptions are silently swallowed and the user is redirected to the login page again.
So I subclassed Shiro's FormAuthenticationFilter to throw an AuthenticationException if there is one:
public class MyFormAutheticationFilter extends FormAuthenticationFilter {
#Override
protected boolean onLoginFailure(AuthenticationToken token,
AuthenticationException e, ServletRequest request,
ServletResponse response) {
if(e != null){
throw e;
}
return super.onLoginFailure(token, e, request, response);
}
}
And I also tried it with throwing the exception e wrapped in a MappableContainerException.
Both approaches cause the same problem: Instead of the exception being handled by the defined ExceptionHandler, a javax.servlet.ServletException is thrown:
javax.servlet.ServletException: org.apache.shiro.authc.AuthenticationException: Unknown Account!
at org.apache.shiro.web.servlet.AdviceFilter.cleanup(AdviceFilter.java:196)
at org.apache.shiro.web.filter.authc.AuthenticatingFilter.cleanup(AuthenticatingFilter.java:155)
at org.apache.shiro.web.servlet.AdviceFilter.doFilterInternal(AdviceFilter.java:148)
at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
at org.apache.shiro.guice.web.SimpleFilterChain.doFilter(SimpleFilterChain.java:41)
at org.apache.shiro.web.servlet.AbstractShiroFilter.executeChain(AbstractShiroFilter.java:449)
at org.apache.shiro.web.servlet.AbstractShiroFilter$1.call(AbstractShiroFilter.java:365)
at org.apache.shiro.subject.support.SubjectCallable.doCall(SubjectCallable.java:90)
at org.apache.shiro.subject.support.SubjectCallable.call(SubjectCallable.java:83)
at org.apache.shiro.subject.support.DelegatingSubject.execute(DelegatingSubject.java:383)
at org.apache.shiro.web.servlet.AbstractShiroFilter.doFilterInternal(AbstractShiroFilter.java:362)
at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125)
at com.google.inject.servlet.FilterDefinition.doFilter(FilterDefinition.java:163)
at com.google.inject.servlet.FilterChainInvocation.doFilter(FilterChainInvocation.java:58)
at com.google.inject.servlet.ManagedFilterPipeline.dispatch(ManagedFilterPipeline.java:118)
at com.google.inject.servlet.GuiceFilter.doFilter(GuiceFilter.java:113)
at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1652)
at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:585)
at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:143)
at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:577)
at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:223)
at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1127)
at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:515)
at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:185)
at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1061)
at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141)
at org.eclipse.jetty.server.handler.HandlerCollection.handle(HandlerCollection.java:110)
at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:97)
at org.eclipse.jetty.server.Server.handle(Server.java:499)
at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:310)
at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:257)
at org.eclipse.jetty.io.AbstractConnection$2.run(AbstractConnection.java:540)
at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:635)
at org.eclipse.jetty.util.thread.QueuedThreadPool$3.run(QueuedThreadPool.java:555)
at java.lang.Thread.run(Thread.java:744)
Caused by: org.apache.shiro.authc.AuthenticationException: Unknown Account!
at com.example.webapp.security.MyAuthorizingRealm.doGetAuthenticationInfo(MyAuthorizingRealm.java:27)
at org.apache.shiro.realm.AuthenticatingRealm.getAuthenticationInfo(AuthenticatingRealm.java:568)
at org.apache.shiro.authc.pam.ModularRealmAuthenticator.doSingleRealmAuthentication(ModularRealmAuthenticator.java:180)
at org.apache.shiro.authc.pam.ModularRealmAuthenticator.doAuthenticate(ModularRealmAuthenticator.java:267)
at org.apache.shiro.authc.AbstractAuthenticator.authenticate(AbstractAuthenticator.java:198)
at org.apache.shiro.mgt.AuthenticatingSecurityManager.authenticate(AuthenticatingSecurityManager.java:106)
at org.apache.shiro.mgt.DefaultSecurityManager.login(DefaultSecurityManager.java:270)
at org.apache.shiro.subject.support.DelegatingSubject.login(DelegatingSubject.java:256)
at org.apache.shiro.web.filter.authc.AuthenticatingFilter.executeLogin(AuthenticatingFilter.java:53)
at org.apache.shiro.web.filter.authc.FormAuthenticationFilter.onAccessDenied(FormAuthenticationFilter.java:154)
at org.apache.shiro.web.filter.AccessControlFilter.onAccessDenied(AccessControlFilter.java:133)
at org.apache.shiro.web.filter.AccessControlFilter.onPreHandle(AccessControlFilter.java:162)
at org.apache.shiro.web.filter.PathMatchingFilter.isFilterChainContinued(PathMatchingFilter.java:203)
at org.apache.shiro.web.filter.PathMatchingFilter.preHandle(PathMatchingFilter.java:178)
at org.apache.shiro.web.servlet.AdviceFilter.doFilterInternal(AdviceFilter.java:131)
... 32 more
The question, after all
Given that the environment can't be changed, how can I achieve that a server instance still can be requested via Guice, while Shiro's exceptions are handled with Jersey's auto discovered ExceptionMappers?
This question is much too complicated for me to reproduce on my side, but I saw a problem that I think is the answer and I'll delete this answer if I turn out to be wrong.
You do this:
#Provider
#Singleton
public class ExceptionHandler implements
ExceptionMapper<AuthenticationException> {
Which is correct, you are supposed to bind with both of those annotations as in this question. However, what you do differently is this:
/*
* Although the ExceptionHandler is already found by Jersey
* I bound it manually to be sure
*/
bind(ExceptionHandler.class);
The annotations in a class definition have lower priority than that in a module's configure() method, meaning you are erasing the annotations when you bind "it manually just to be sure". Try erasing that line of code and see if that fixes your problem. If it doesn't fix the problem, leave it deleted anyway, because I am certain that it is at least part of the problem - that statement erases those essential annotations.
I've not found a way to do this either. It looks like the Jersey filters/handlers aren't active on the Shiro servlet stack during authentication. As a work-around specifically for the AuthenticationException I opted to override the AdviceFilter::cleanup(...) method on my AuthenticatingFilter and return a custom message directly.
public class MyTokenAuthenticatingFilter extends AuthenticatingFilter {
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
// regular auth/token creation
}
#Override
protected void cleanup(ServletRequest request, ServletResponse response, Exception existing) throws ServletException, IOException {
HttpServletResponse httpResponse = (HttpServletResponse)response;
if ( null != existing ) {
httpResponse.setContentType(MediaType.APPLICATION_JSON);
httpResponse.getOutputStream().write(String.format("{\"error\":\"%s\"}", existing.getMessage()).getBytes());
httpResponse.setStatus(Response.Status.FORBIDDEN.getStatusCode());
existing = null; // prevent Shiro from tossing a ServletException
}
super.cleanup(request, httpResponse, existing);
}
}
When authentication is successful the ExceptionMappers work fine for exceptions thrown within the context of the Jersey controllers.

Resources