使用 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的主要内容,如果未能解决你的问题,请参考以下文章

JDK1.6使用jersey多少版本

Jersey com.sun.jersey.spi.container.servlet.ServletContainer 使用 MAVEN 时出错

使用 Jersey 客户端的 PATCH 请求

为啥使用 JAX-RS / Jersey?

Spring Boot + Jersey

如何在 tomcat 7 中的 Jersey 2 中使用异步回调