使用Jersey的ExceptionMapper映射Shiro的AuthenticationException

时间:2015-08-05 15:32:21

标签: jersey guice shiro embedded-jetty

前言

首先,我最诚挚地对这个问题道歉,但我真的不知道如何缩短它,因为每个部分都是一个特例。不可否认,我可能会对此视而不见,因为我现在正撞在墙上几天,我开始变得绝望了。

我非常尊重和感谢所有读过它的人。

目标

我希望能够使用Shiro's AuthenticationExceptionJAX-RS Responses及其子类映射到Jersey ExceptionMappers,使用创建嵌入式的Guice 3.0 Injector进行设置码头。

环境

  • Guice 3.0
  • Jetty 9.2.12.v20150709
  • 泽西岛1.19.1
  • Shiro 1.2.4

设置

使用Guice Injector

创建嵌入式Jetty
// 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服务器的提供程序:

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以及更多内容,我留下来确保没有&#34;副作用&#34;可能是隐藏的:

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过滤器时,我注意到.NETExceptions被静默吞下,用户再次被重定向到登录页面。

所以我将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中。{/ p>

这两种方法都会导致同样的问题:不是由定义的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来处理的?< / p>

2 个答案:

答案 0 :(得分:4)

这个问题对我来说太复杂了,但是我发现了一个问题,我认为是答案,如果我结果是这样的话,我会删除这个答案。错。

你这样做:

@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()方法中的注释,这意味着当您使用删除注释时绑定&#34;它只是为了确保&#34;。尝试删除该行代码,看看是否能解决您的问题。如果它没有解决问题,那么无论如何都要将其删除,因为我确信它至少是问题的一部分 - 该语句会删除那些必要的注释。

答案 1 :(得分:0)

我也没有办法做到这一点。在身份验证期间,看起来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控制器上下文中抛出的异常。