使用 Jersey 的 ExceptionMapper 映射 Shiro 的 AuthenticationException
Posted
技术标签:
【中文标题】使用 Jersey 的 ExceptionMapper 映射 Shiro 的 AuthenticationException【英文标题】:Map Shiro's AuthenticationException with Jersey's ExceptionMapper 【发布时间】:2015-10-28 11:34:22 【问题描述】:前言
首先,对于这个问题非常长,我深表歉意,但老实说,我不知道如何缩短它,因为每个部分都是一种特殊情况。诚然,我可能对此视而不见,因为我已经用头撞墙了几天,而且我开始绝望了。
向所有阅读它的人致以最崇高的敬意和感谢。
目标
我希望能够使用Jersey ExceptionMappers 将Shiro's AuthenticationException 及其子类映射到JAX-RS Responses,使用Guice 3.0 Injector 进行设置,该Guice 3.0 Injector 创建嵌入式码头。
环境
Guice 3.0 码头 9.2.12.v20150709 球衣 1.19.1 Shiro 1.2.4设置
嵌入式 Jetty 是使用 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();
ServerModule
为 Jetty 服务器绑定了一个 Provider:
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);
JettyProvider
设置了一个 Jetty WebApplicationContext,注册了 Guice 所需的 ServletContextListener 和其他一些东西,我留下了这些东西以确保不会隐藏“副作用”:
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;
在MyServletContextListener
中,我创建了一个子注入器,它使用 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;
JerseyModule,JerseyServletModule
的子类现在将所有内容放在一起:
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);
ExceptionHandler
非常简单,看起来像这样:
@Provider
@Singleton
public class ExceptionHandler implements
ExceptionMapper<AuthenticationException>
public Response toResponse(AuthenticationException exception)
return Response
.status(Status.UNAUTHORIZED)
.entity("auth exception handled")
.build();
问题
现在,当我想访问受限资源并输入正确的主体/凭据组合时,一切正常。但是一旦输入不存在的用户或者密码错误,我希望Shiro抛出一个AuthenticationException
,我希望它由上面的ExceptionHandler
处理。
利用一开始 Shiro 提供的默认 AUTHC
过滤器,我注意到 AuthenticationExceptions 被默默吞下,用户再次被重定向到登录页面。
所以我继承了 Shiro 的 FormAuthenticationFilter 以抛出一个 AuthenticationException
(如果有的话):
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);
我还尝试抛出异常 e
包裹在 MappableContainerException 中。
两种方法都会导致相同的问题:不是由定义的ExceptionHandler
处理异常,而是抛出javax.servlet.ServletException
:
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
这个问题,毕竟
鉴于环境无法更改,我怎样才能实现仍然可以通过 Guice 请求服务器实例,而 Shiro 的异常由 Jersey 的自动发现的 ExceptionMappers 处理?
【问题讨论】:
我有一个使用简单应用程序的 POC。但是我无法启动并运行您的代码来测试您的配置。你介意创建一个完整的运行应用程序并将其发布在 github 上或其他地方,以便我可以测试它。 @peeskillet 太棒了!会尽快做的! @MarkusWMahlberg 你有没有上过 github? 【参考方案1】:我也没有找到这样做的方法。看起来 Jersey 过滤器/处理程序在身份验证期间在 Shiro servlet 堆栈上未处于活动状态。作为专门针对 AuthenticationException 的解决方法,我选择覆盖 AuthenticatingFilter 上的 AdviceFilter::cleanup(...) 方法并直接返回自定义消息。
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);
当身份验证成功时,ExceptionMappers 可以很好地处理 Jersey 控制器上下文中引发的异常。
【讨论】:
【参考方案2】:这个问题对我来说太复杂了,无法在我这边重现,但我看到了一个我认为是答案的问题,如果我发现我错了,我会删除这个答案。
你这样做:
@Provider
@Singleton
public class ExceptionHandler implements
ExceptionMapper<AuthenticationException>
这是正确的,您应该像this question 那样绑定这两个注释。但是,您的不同之处在于:
/*
* Although the ExceptionHandler is already found by Jersey
* I bound it manually to be sure
*/
bind(ExceptionHandler.class);
类定义中的注解优先级低于模块的configure()
方法中的优先级,这意味着当您手动绑定“它只是为了确定”。尝试擦除该行代码,看看是否能解决您的问题。如果它不能解决问题,无论如何都要删除它,因为我确信它至少是问题的一部分 - 该语句会删除那些重要的注释。
【讨论】:
首先感谢您的回答。遗憾的是,这并没有改变任何东西 ;) 跟踪日志文件,在我看来,通过扫描或绑定找到的@Providers
都在 Jersey 注册。据我尝试,这些方法之间似乎没有任何功能差异,即使使用这两种方法似乎也不会改变任何行为。我使用异常处理程序和服务对此进行了测试。
@MarkusWMahlberg 另一个愚蠢的、低垂的果实尝试:这可能是重复的吗? ***.com/questions/27982948/…以上是关于使用 Jersey 的 ExceptionMapper 映射 Shiro 的 AuthenticationException的主要内容,如果未能解决你的问题,请参考以下文章
Jersey com.sun.jersey.spi.container.servlet.ServletContainer 使用 MAVEN 时出错