Подтвердить что ты не робот

Карта Широ Аутентификация Исключение с Джерси ExceptionMapper

Введение

Прежде всего, мои самые искренние извинения за этот вопрос очень длинные, но я честно не знаю, как его укоротить, так как каждая часть - это особый случай. По общему признанию, я могу быть слепым, потому что я стучу головой о стену на пару дней, и я начинаю отчаяться.

Мое уважение и благодарность всем, кто прочитал это.

Цель

Я хотел бы иметь возможность отображать Shiro AuthenticationException и его подклассы to Ответы JAX-RS с помощью Jersey ExceptionMappers, настроенный с помощью Guice 3.0 Injector, который создает встроенный Jetty.

Окружение

  • Guice 3.0
  • Jetty 9.2.12.v20150709
  • Джерси 1.19.1
  • Сиро 1.2.4

Настройка

Встроенный Jetty создается с помощью Injector Guice

// 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, регистрирует ServletContextListener, необходимый для Guice, и несколько вещей, которые я оставил, чтобы убедиться, что "побочные эффекты" могут быть скрыты:

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;
    }

}

The 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();
    }

}

Проблема

Теперь все работает нормально, когда я хочу получить доступ к ограниченному ресурсу и ввести правильные комбинации основных/учетных данных. Но как только введите несуществующего пользователя или неправильный пароль, я хочу, чтобы AuthenticationException был брошен Сиро, и я хочу, чтобы он обрабатывался выше ExceptionHandler.

Используя фильтр по умолчанию AUTHC, предоставленный Сиро в начале, я заметил, что Аутентификационные Исключения молча проглатываются, и пользователь снова перенаправляется на страницу входа.

Итак, я подклассифицировал 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, a 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 обрабатываются с помощью обнаруженных Джерси ExceptionMappers?

4b9b3361

Ответ 1

Этот вопрос слишком сложный для меня, чтобы воспроизвести на моей стороне, но я увидел проблему, которая, по моему мнению, является ответом, и я удалю этот ответ, если я ошибусь.

Вы делаете это:

@Provider
@Singleton
public class ExceptionHandler implements
        ExceptionMapper<AuthenticationException> {

Что правильно, вы должны связываться с обоими аннотациями, как в этом вопросе. Однако то, что вы делаете по-другому, следующее:

/* 
 * Although the ExceptionHandler is already found by Jersey
 * I bound it manually to be sure
 */
bind(ExceptionHandler.class);

Аннотации в определении класса имеют более низкий приоритет, чем метод в модуле configure(), что означает, что вы стираете аннотации при привязке "вручную быть уверенным". Попробуйте удалить эту строку кода и посмотреть, устраняет ли это вашу проблему. Если это не устранит проблему, оставьте ее в любом случае, потому что я уверен, что это хотя бы часть проблемы - это выражение стирает эти важные аннотации.

Ответ 2

Я тоже не нашел способ сделать это. Похоже, фильтры Джерси/обработчики не активны в стеке сервлетов Shiro во время аутентификации. Как правило, для исключения AuthenticationException я решил переопределить метод AdviceFilter:: cleanup (...) на моем AuthenticatingFilter и сразу же отправить собственное сообщение.

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 отлично работают для исключений, созданных в контексте контроллеров Джерси.