You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

JSF 2.3会话过期与ViewExpired异常问题求助

问题背景

在WildFly 17环境下,使用JSF 2.3(Mojarra)和PrimeFaces 7开发时,遇到HTTP会话过期相关异常,参考资料后未解决。为验证会话过期假设,在web.xml中添加以下配置:

<session-config>
    <cookie-config>
        <http-only>true</http-only>
    </cookie-config>
    <session-timeout>1</session-timeout>
</session-config>
测试场景

部署应用后,在不同浏览器中测试两个页面:

1. 需要登录的页面

登录成功后打开该页面,使用ViewScoped Bean。等待1分钟会话过期后,点击页面中的按钮:

<p:commandButton icon="ui-icon-check" value="....." action="#{aveAngelBean.aveAngelegtOK}" update="@form" />

现象:仅显示加载动画,无其他响应,浏览器控制台无错误,再次点击仍无效,服务器端无代码执行。

2. 无需登录的页面

通过带GET参数的链接打开页面,使用ViewScoped Bean。会话过期后点击页面中的按钮:

<p:commandButton  icon = "ui-icon-check" value="#{msg['immobilien.ave.go.forward']}" action="#{persDatenBean.weiterUnterlagen}" update="@form">
    <f:param name="validateOnWeiter" value="true"/>
</p:commandButton>

现象:后台Bean的@PostConstruct方法执行(服务器尝试恢复视图),但因初始打开页面时URL中的GET参数?linkId=c91cc387-45f4-4c22-8829-4e3fd266c311_abd45a61-0e4e-4e7b-aa48-e9d858a5dbb9丢失而报错。

问题
  1. 针对第一个页面,如何在会话过期后自动跳转至登录页,而非无响应状态?
  2. 针对第二个页面,点击按钮时如何携带初始的GET参数重新加载页面?为何该参数未被识别?

附完整web.xml配置:

<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
                    http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
                    version="3.0">

    <context-param>
        <param-name>javax.faces.STATE_SAVING_METHOD</param-name>
        <param-value>server</param-value>
    </context-param>
    <context-param>
        <param-name>javax.faces.PROJECT_STAGE</param-name>
        <param-value>Development</param-value>
    </context-param>
    <context-param>
        <param-name>primefaces.THEME</param-name>
        <param-value>sapphire-#{guestPreferences.theme}</param-value>
    </context-param>
    <context-param>
        <param-name>primefaces.FONT_AWESOME</param-name>
        <param-value>true</param-value>
    </context-param>
    <context-param>
        <param-name>javax.faces.FACELETS_LIBRARIES</param-name>
        <param-value>/WEB-INF/primefaces-sapphire.taglib.xml</param-value>
    </context-param>
<!-- @see http://jsfcorner.blogspot.co.at/2014/01/client-side-validation-example-of.html  -->
    <context-param>
        <param-name>primefaces.CLIENT_SIDE_VALIDATION</param-name>
        <param-value>true</param-value>
    </context-param>

    <context-param>
        <param-name>primefaces.FONT_AWESOME</param-name>
        <param-value>true</param-value>
    </context-param>  
    
    <listener>
        <listener-class>com.sun.faces.config.ConfigureListener</listener-class>
    </listener>
    <filter>
        <filter-name>Character Encoding Filter</filter-name>
        <filter-class>org.primefaces.sapphire.filter.CharacterEncodingFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>Character Encoding Filter</filter-name>
        <servlet-name>Faces Servlet</servlet-name>
    </filter-mapping>
    
    <!-- JSF mapping -->
    <servlet>
        <servlet-name>Faces Servlet</servlet-name>
        <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet>
        <servlet-name>Job Repair Servlet</servlet-name>
        <servlet-class>at.home.digest.web.ave.JobRepairServlet</servlet-class>
        <load-on-startup>0</load-on-startup>
    </servlet>
    <servlet>
        <servlet-name>InitServlet Servlet</servlet-name>
        <servlet-class>at.home.digest.web.ave.InitServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet>
        <servlet-name>Download Servlet</servlet-name>
        <servlet-class>at.home.digest.web.DownloadServlet</servlet-class>
        <load-on-startup>0</load-on-startup>
    </servlet>
    
    <!-- Map these files with JSF -->
    <servlet-mapping>
        <servlet-name>Faces Servlet</servlet-name>
        <url-pattern>*.jsf</url-pattern>
    </servlet-mapping>
    <servlet-mapping>
        <servlet-name>Faces Servlet</servlet-name>
        <url-pattern>*.faces</url-pattern>
    </servlet-mapping>
    <servlet-mapping>
        <servlet-name>Faces Servlet</servlet-name>
        <url-pattern>*/faces/*</url-pattern>
    </servlet-mapping>
    <servlet-mapping>
        <servlet-name>Faces Servlet</servlet-name>
        <url-pattern>*.xhtml</url-pattern>
    </servlet-mapping>
    <servlet-mapping>
        <servlet-name>Job Repair Servlet</servlet-name>
        <url-pattern>/ave/makler/admin/jobRep</url-pattern>
    </servlet-mapping>
    <servlet-mapping>
        <servlet-name>Download Servlet</servlet-name>
        <url-pattern>/heimdi/ave/download</url-pattern>
    </servlet-mapping>
    <servlet-mapping>
        <servlet-name>Download Servlet</servlet-name>
        <url-pattern>/ave/download</url-pattern>
    </servlet-mapping>
    
    <security-constraint>
        <web-resource-collection>
            <web-resource-name>All resources in /ave/makler/*</web-resource-name>
            <description>All resources in /ave/makler/*</description>
            <url-pattern>/ave/makler/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>makler</role-name>
            <role-name>maklerChef</role-name>
        </auth-constraint>
    </security-constraint>

    <login-config>
        <auth-method>FORM</auth-method>
        <realm-name>jBossJaasHeimdiRealm</realm-name>
        <form-login-config>
            <form-login-page>/login/login.jsf</form-login-page>
            <form-error-page>/login/loginFailed.jsf</form-error-page>
        </form-login-config>
    </login-config>

    <security-role>
        <role-name>makler</role-name>
    </security-role>
    
    <security-role>
        <role-name>maklerChef</role-name>
    </security-role>
    
    <security-role>
        <role-name>kaufer</role-name>
    </security-role>
    
     <session-config>
        <cookie-config>
            <http-only>true</http-only>  <!-- https://owasp.org/www-community/HttpOnly , @see also:  https://www.cookiepro.com/knowledge/httponly-cookie/  -->
            <!-- <secure>true</secure>   --> <!-- https://owasp.org/www-community/controls/SecureCookieAttribute  -->
        </cookie-config>
        <session-timeout>1</session-timeout>
    </session-config>
    
    <error-page>
      <exception-type>javax.servlet.ServletException</exception-type>
      <location>/error.faces</location>
   </error-page>
   <error-page>
      <exception-type>javax.faces.FacesException</exception-type>
      <location>/error.faces</location>
   </error-page>
   <error-page>
       <exception-type>java.lang.Exception</exception-type>
       <location>/error.faces</location>
   </error-page>
   <!-- 
   <error-page>
        <exception-type>javax.faces.application.ViewExpiredException</exception-type>
        <location>/error.faces</location>
   </error-page>
    -->
 </web-app>
解决方案

针对问题1:会话过期后跳转至登录页

方法1:启用ViewExpiredException错误页面映射

取消web.xml中注释的ViewExpiredException错误页面配置,修改为跳转至登录页:

<error-page>
    <exception-type>javax.faces.application.ViewExpiredException</exception-type>
    <location>/login/login.jsf</location>
</error-page>

当会话过期触发ViewExpiredException时,容器会自动跳转至指定的登录页面。

方法2:PrimeFaces全局AJAX错误处理

在页面中添加全局AJAX错误处理脚本,捕获会话过期异常并跳转:

PrimeFaces.onAjaxError(function(xhr, status, error) {
    if (xhr.status === 403 || xhr.responseText.indexOf('ViewExpiredException') !== -1) {
        window.location.href = '/login/login.jsf';
    }
});

该脚本会监听所有PrimeFaces AJAX请求的错误,判断是否为会话过期相关异常,若则跳转至登录页。

方法3:自定义Servlet Filter

实现一个Filter,拦截所有请求,检查会话有效性:

public class SessionValidationFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse res = (HttpServletResponse) response;
        
        // 排除登录页面和静态资源
        String path = req.getRequestURI();
        if (!path.contains("/login/") && req.getSession(false) == null) {
            res.sendRedirect(req.getContextPath() + "/login/login.jsf");
            return;
        }
        
        chain.doFilter(request, response);
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {}

    @Override
    public void destroy() {}
}

在web.xml中注册该Filter,映射到需要登录的资源路径:

<filter>
    <filter-name>SessionValidationFilter</filter-name>
    <filter-class>your.package.SessionValidationFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>SessionValidationFilter</filter-name>
    <url-pattern>/ave/makler/*</url-pattern>
</filter-mapping>

针对问题2:携带初始GET参数重新加载页面

原因分析

会话过期后,服务器尝试恢复视图,但初始的GET参数linkId并未存储在视图状态中,因此恢复视图时无法获取该参数。PrimeFaces的p:commandButton默认仅携带视图状态和按钮上的f:param参数,不会自动保留初始URL中的GET参数。

解决方法

方法1:将linkId存储到ViewScoped Bean中

在页面初始化时,将linkId参数存入ViewScoped Bean:

@ViewScoped
@ManagedBean
public class PersDatenBean implements Serializable {
    private String linkId;

    @PostConstruct
    public void init() {
        FacesContext fc = FacesContext.getCurrentInstance();
        linkId = fc.getExternalContext().getRequestParameterMap().get("linkId");
    }

    // getter和setter方法
    public String getLinkId() {
        return linkId;
    }

    public void setLinkId(String linkId) {
        this.linkId = linkId;
    }
}

然后在p:commandButton中添加f:param传递该参数:

<p:commandButton icon="ui-icon-check" value="#{msg['immobilien.ave.go.forward']}" action="#{persDatenBean.weiterUnterlagen}" update="@form">
    <f:param name="validateOnWeiter" value="true"/>
    <f:param name="linkId" value="#{persDatenBean.linkId}"/>
</p:commandButton>

这样点击按钮时,linkId会被重新传递到服务器。

方法2:使用includeViewParams="true"

在页面的<h:form>标签中添加includeViewParams="true",自动包含当前视图的所有参数:

<h:form includeViewParams="true">
    <!-- 按钮代码 -->
</h:form>

该属性会自动将初始URL中的GET参数添加到表单提交的请求中,无需手动添加f:param。

方法3:JavaScript手动获取参数

在按钮的onclick事件中,手动获取URL中的linkId参数并添加到请求中:

<p:commandButton icon="ui-icon-check" value="#{msg['immobilien.ave.go.forward']}" action="#{persDatenBean.weiterUnterlagen}" update="@form"
                 onclick="this.getJQ().data('params').push({name: 'linkId', value: getUrlParameter('linkId')});">
    <f:param name="validateOnWeiter" value="true"/>
</p:commandButton>

添加对应的JavaScript函数:

function getUrlParameter(name) {
    name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
    var regex = new RegExp('[\\?&]' + name + '=([^&#]*)');
    var results = regex.exec(location.search);
    return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
}

内容的提问来源于stack exchange,提问作者Alex Mi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.21 10:06:26