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丢失而报错。
- 针对第一个页面,如何在会话过期后自动跳转至登录页,而非无响应状态?
- 针对第二个页面,点击按钮时如何携带初始的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

