【问题标题】:ViewExpiredException not thrown on ajax request if JSF page is protected by j_security_check如果 JSF 页面受 j_security_check 保护,则不会在 ajax 请求上引发 ViewExpiredException
【发布时间】:2023-04-02 04:40:02
【问题描述】:

我有一个不受j_security_check 保护的JSF 页面。我执行以下步骤:

  1. 在浏览器中打开 JSF 页面。
  2. 重新启动服务器。
  3. 单击 JSF 页面上的命令按钮以启动 ajax 调用。

Firebug 显示 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,我的页面卡住了,没有任何视觉反馈。

【问题讨论】:

    标签: ajax jsf-2 j-security-check viewexpiredexception


    【解决方案1】:

    我能够重现您的问题。这里发生的是容器调用RequestDispatcher#forward() 到安全约束中指定的登录页面。但是,如果登录页面本身也是一个 JSF 页面,那么在转发的请求上也会调用 FacesServlet。由于请求是转发的,这将简单地在转发的资源(登录页面)上创建一个新视图。但是,由于是ajax请求,没有render信息(整个POST请求在安全检查转发过程中基本被丢弃),只会返回视图状态。

    请注意,如果登录页面不是 JSF 页面(例如 JSP 或纯 HTML),则 ajax 请求将返回页面的整个 HTML 输出作为 ajax 响应,JSF ajax 无法解析并解释为“空" 回应。

    不幸的是,它“按设计”工作。我怀疑 JSF 规范对 ajax 请求的安全约束检查存在一些疏忽。原因毕竟是可以理解的,幸运的是很容易解决。只是,您实际上不想在此处显示错误页面,而只是完整地显示登录页面,就像在非 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,那么这个问题 fully transparently 已解决,您不需要为此自定义 PhaseListener

    【讨论】:

    • 谢谢 BalusC。拯救了我的一天。
    • 哦,还有一个问题,我想知道为什么会发生以下情况:如果我使用 context.getExternalContext().redirect(loginURL),它确实会将我重定向到登录页面。但是登录后,浏览器会显示一个以ViewState为内容的xml文件。 xml 文件与我在问题中发布的第二个 xml 文件完全相同。如果我使用 context.getExternalContext().redirect(homepageURL),一切正常。它会带我进入登录页面。登录后,将显示主页。
    • 你是对的,这是另一个令人讨厌的问题:容器管理的身份验证会记住所有请求参数(包括无效的 javax.faces.ViewState 和另一个指示它是 ajax 请求的请求参数)并在之后重新传递它成功登录,这将导致 ViewExpiredException 错误页面作为 ajax 响应。这可以通过重定向到转发 URI 来避免,而转发 URI 又应该通过普通的 GET 请求而不是 ajax POST 请求再次重新触发安全检查。我已经相应地更新了答案。
    • @BalusC OmniPartialViewContext 对我没有任何作用。 Ajax 发布到安全页面会导致 403 错误(而不是 viewExpiredException),jsf ajax 处理程序会忽略该错误,因此不会发生任何事情。在 JSF 中处理过期视图时,似乎不可能在服务器端做任何事情。没有过滤器、错误处理程序或任何东西被调用。有什么建议吗?
    • 感谢您的解决方案。需要检查 originalURL!=null 吗? request.getAttribute(RequestDispatcher.FORWARD_REQUEST_URI) 对于我的 Web 应用程序始终为空
    【解决方案2】:

    上面的 AjaxLoginListener 解决方案对我有用。有趣的是,我们使用的是 omnifaces 3.11.1,但 OmniPartialViewContext 在我的场景中不起作用。这是因为对 loginViewId 的检查与当前 viewId 不匹配,因为我的 web.xml 中有一个错误页面,用于 org.jboss.weld.contexts.NonexistentConversationException。请注意,当为我触发 AjaxLoginListener 时,它会在调用 context.getExternalContext().invalidateSession(); 时引发异常;所以它从不调用redirect()。所以我不确定我的场景是否与该线程中的原始场景完全相同。以下是我用来重新创建场景的步骤:

    1. 使用 ajax 命令按钮访问 xhtml 页面。
    2. 等待会话超时。
    3. 单击 ajax 命令按钮。
    4. 用户被重定向到映射到 web.xml 中的 NonexistentConversationException 的错误页面
    5. 单击该页面上请求安全 URL 的链接
    6. 系统显示登录页面 - 登录。
    7. 单击该链接将您带到包含第 1 步中的 ajax 命令按钮的 xhtml 页面。
    8. 系统显示包含 NonexistentConversationException 错误页面内容的部分响应。

    AjaxLoginListener 是否可以正常工作,因为它映射到 PhaseId.RESTORE_VIEW 而 OmniPartialViewContext 映射到 PhaseId.RENDER_RESPONSE?

    【讨论】:

      猜你喜欢
      • 2012-12-05
      • 2012-06-27
      • 2021-11-17
      • 1970-01-01
      • 1970-01-01
      • 2016-03-02
      • 2016-01-13
      • 2011-02-11
      相关资源
      最近更新 更多