ViewExpiredException не выбрасывается на запрос ajax, если страница JSF защищена j_security_check

У меня есть страница JSF, которая не защищена j_security_check. Я выполняю следующие шаги:

  • Откройте страницу JSF в браузере.
  • Перезагрузите сервер.
  • Нажмите кнопку командной строки на странице JSF, чтобы инициировать вызов ajax.

Firebug показывает, что a ViewExpiredException поднят, как и ожидалось.

Сообщение:

javax.faces.ViewState=8887124636062606698:-1513851009188353364

Ответ:

<partial-response>
<error>
<error-name>class javax.faces.application.ViewExpiredException</error-name>
<error-message>viewId:/viewer.xhtml - View /viewer.xhtml could not be restored.</error-message>
</error>
</partial-response>

Однако, как только я сконфигурирую страницу, которая будет защищена j_security_check, и выполните те же действия, перечисленные выше, странно (для меня) ViewExpiredException больше не будет поднят. Вместо этого ответ - это просто новое состояние представления.

Сообщение:

javax.faces.ViewState=-4873187770744721574:8069938124611303615

Ответ:

<partial-response>
<changes>
<update id="javax.faces.ViewState">234065619769382809:-4498953143834600826</update>
</changes>
</partial-response>

Может кто-нибудь помочь мне понять это? Я ожидаю, что это вызовет исключение, поэтому я могу обработать это исключение и показать страницу с ошибкой. Теперь он просто отвечает новым ViewState, моя страница просто застряла без какой-либо визуальной обратной связи.

Ответ 1

Я смог воспроизвести вашу проблему. Что здесь происходит, так это то, что контейнер вызывает RequestDispatcher#forward() на странице входа, как указано в ограничении безопасности. Однако, если страница входа сама по себе является страницей JSF, тогда также будет вызываться FacesServlet по пересылаемому запросу. Поскольку запрос является переадресацией, это просто создаст новое представление для перенаправленного ресурса (страница входа). Однако, поскольку это ajax-запрос и там нет render информации (весь запрос POST в основном отбрасывается во время проверки безопасности вперед), возвращается только состояние представления.

Обратите внимание, что если страница входа в систему не была страницей JSF (например, JSP или простой HTML), тогда запрос ajax вернул бы весь вывод HTML страницы как ajax-ответ, который не поддается JSF ajax и интерпретируется как "пустой" ответ.

Это, к сожалению, работает "как запроектировано". Я подозреваю, что существует некоторый надзор в спецификации JSF для проверки ограничений безопасности на запросы ajax. Причина в конце концов понятна и, к счастью, легко решить. Только, вы действительно не хотите показывать страницу с ошибкой здесь, а вместо этого просто страницу входа полностью, точно так же, как это произошло во время запроса без аякса. Вам просто нужно проверить, является ли текущий запрос ajax-запросом и был отправлен на страницу входа в систему, тогда вам нужно отправить специальный ответ "перенаправление" ajax, чтобы весь вид был изменен.

Вы можете достичь этого с помощью PhaseListener следующим образом:

public class AjaxLoginListener implements PhaseListener {

    @Override
    public PhaseId getPhaseId() {
        return PhaseId.RESTORE_VIEW;
    }

    @Override
    public void beforePhase(PhaseEvent event) {
        // NOOP.
    }

    @Override
    public void afterPhase(PhaseEvent event) {
        FacesContext context = event.getFacesContext();
        HttpServletRequest request = (HttpServletRequest) context.getExternalContext().getRequest();
        String originalURL = (String) request.getAttribute(RequestDispatcher.FORWARD_REQUEST_URI);
        String loginURL = request.getContextPath() + "/login.xhtml";

        if (context.getPartialViewContext().isAjaxRequest()
            && originalURL != null
            && loginURL.equals(request.getRequestURI()))
        {
            try {
                context.getExternalContext().invalidateSession();
                context.getExternalContext().redirect(originalURL);
            } catch (IOException e) {
                throw new FacesException(e);
            }
        }
    }
}

Обновить это решение с тех пор, как OmniFaces 1.2 был встроен в OmniPartialViewContext. Поэтому, если вы уже используете OmniFaces, эта проблема будет полностью прозрачно решена, и для этого вам не нужен пользовательский PhaseListener.