JSF应用中HTTP会话属性意外为空问题排查求助
会话属性随机为空问题排查与修复
问题背景
基于JSF 2.0/PrimeFaces 8.0构建的大型Web应用部署在Linux环境Tomcat 8服务器上,存在以下异常:用户登录后活跃操作期间(未触发30分钟默认会话超时),会话属性username会随机变为null,导致Ajax请求被过滤器拦截并重定向至登录页,需多次登录才可能恢复正常。已排查session.invalidate()和getSession(true)的使用场景,未发现明显异常。
相关代码实现
Login Bean
@ManagedBean(name="login") @ViewScoped public class LoginBean implements Serializable { private String username; private String password; String baseURL; public LoginBean() { HttpSession ss = (HttpSession) FacesContext.getCurrentInstance().getExternalContext().getSession(false); if(ss!=null) { ss.invalidate(); } baseURL = // 应用部署的HTTPS地址 } public void login() { // 通过DAO验证登录信息 if(loginSuccess) { HttpSession ss = (HttpSession) FacesContext.getCurrentInstance().getExternalContext().getSession(false); if(ss!=null) { ss.invalidate(); } ss = (HttpSession) FacesContext.getCurrentInstance().getExternalContext().getSession(true); ss.setAttribute("username", username); PrimeFaces.current().executeScript("window.location.replace('"+baseURL+"homePage.xhtml');"); // 避免浏览器回退到登录页 } } }
HomeBean
@ManagedBean(name="home") @ViewScoped public class HomeBean implements Serializable { private String username; public HomeBean() { HttpSession ss = (HttpSession) FacesContext.getCurrentInstance().getExternalContext().getSession(false); username = (String) ss.getAttribute("username"); } }
过滤器(MyFilter)
public class MyFilter implements Filter { String userName=""; @Override public void destroy() {} @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req=(HttpServletRequest) request; HttpServletResponse res=(HttpServletResponse) response; HttpSession session = req.getSession(false); userName=(String) req.getSession().getAttribute("username"); String requestUrl = req.getRequestURI(); String loginURL = req.getContextPath() + "/login.xhtml"; String expiryURL = req.getContextPath() + "/expired.xhtml"; if (!requestUrl.contains(ResourceHandler.RESOURCE_IDENTIFIER)) { if(!requestUrl.contains("/login.xhtml") && !requestUrl.contains("/expired.xhtml")) { if(session == null) { if ("partial/ajax".equals(req.getHeader("Faces-Request"))) { res.setContentType("text/xml"); res.getWriter() .append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>") .printf("<partial-response><redirect url=\"%s\"></redirect></partial-response>", expiryURL); } else { res.sendRedirect(expiryURL); } } else { if(userName==null || userName.equals("") ) { session.invalidate(); if ("partial/ajax".equals(req.getHeader("Faces-Request"))) { res.setContentType("text/xml"); res.getWriter() .append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>") .printf("<partial-response><redirect url=\"%s\"></redirect></partial-response>", expiryURL); } else { res.sendRedirect(expiryURL); } } else { chain.doFilter(request, response); } } } else { if(requestUrl.contains("/expired.xhtml")) { if(session!=null) { session.invalidate(); } } chain.doFilter(request, response); } } else { chain.doFilter(request, response); } } @Override public void init(FilterConfig arg0) throws ServletException {} }
PrimeFaces Idle Monitor
<h:form> <p:idleMonitor timeout="600000"> <p:ajax event="active" listener="#{idleBean.expire()}" /> </p:idleMonitor> </h:form>
Idle Monitor Bean
@ManagedBean(name="idleBean") @ViewScoped public class IdleBean implements Serializable { String baseURL; // 需补充初始化逻辑 public void expire() throws IOException { HttpSession ss = (HttpSession) FacesContext.getCurrentInstance().getExternalContext().getSession(false); if(ss!=null) { ss.invalidate(); } FacesContext.getCurrentInstance().getExternalContext().redirect(baseURL+"/expired.xhtml"); } }
原因分析
- 过滤器线程安全问题:Filter是单例实例,成员变量
userName会被所有请求共享,并发场景下会出现值覆盖,导致部分请求获取到的userName为null。同时,调用req.getSession()会强制创建新会话,若原会话已失效,会导致会话状态不一致。 - IdleMonitor事件绑定错误:
p:idleMonitor的active事件是用户从空闲状态恢复活跃时触发,但当前绑定了销毁会话的expire()方法,会在用户刚操作就销毁会话,完全违背超时销毁的逻辑。 - LoginBean会话处理不当:构造函数中无条件销毁现有会话,若用户打开多个登录页标签,可能导致刚创建的有效会话被其他标签页的构造函数销毁。
- HomeBean未处理空会话:直接调用
ss.getAttribute("username")未检查会话是否为null,会抛出空指针异常,间接导致会话状态异常。 - 导航方式混杂:混合使用JS脚本跳转、JSF导航规则和
ExternalContext.redirect(),可能导致会话Cookie传递不一致,引发会话丢失或重复创建。
改进方案
1. 修复过滤器线程安全问题
将userName改为局部变量,避免共享;同时仅通过已获取的session对象读取属性,禁止调用req.getSession()强制创建会话:
public class MyFilter implements Filter { @Override public void destroy() {} @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse res = (HttpServletResponse) response; HttpSession session = req.getSession(false); // 改为局部变量,每次请求单独读取 String userName = (session != null) ? (String) session.getAttribute("username") : null; String requestUrl = req.getRequestURI(); String loginURL = req.getContextPath() + "/login.xhtml"; String expiryURL = req.getContextPath() + "/expired.xhtml"; if (!requestUrl.contains(ResourceHandler.RESOURCE_IDENTIFIER)) { if(!requestUrl.contains("/login.xhtml") && !requestUrl.contains("/expired.xhtml")) { if(session == null) { handleAjaxOrRedirect(req, res, expiryURL); } else { if(userName == null || userName.isEmpty()) { session.invalidate(); handleAjaxOrRedirect(req, res, expiryURL); } else { chain.doFilter(request, response); } } } else { if(requestUrl.contains("/expired.xhtml") && session != null) { session.invalidate(); } chain.doFilter(request, response); } } else { chain.doFilter(request, response); } } // 提取重复逻辑,简化代码 private void handleAjaxOrRedirect(HttpServletRequest req, HttpServletResponse res, String redirectUrl) throws IOException { if ("partial/ajax".equals(req.getHeader("Faces-Request"))) { res.setContentType("text/xml"); res.getWriter() .append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>") .printf("<partial-response><redirect url=\"%s\"></redirect></partial-response>", redirectUrl); } else { res.sendRedirect(redirectUrl); } } @Override public void init(FilterConfig arg0) throws ServletException {} }
2. 修正IdleMonitor事件绑定
将active事件改为idle事件,确保用户超时空闲时才销毁会话;同时补充baseURL初始化逻辑:
<h:form> <p:idleMonitor timeout="600000"> <p:ajax event="idle" listener="#{idleBean.expire()}" /> </p:idleMonitor> </h:form>
@ManagedBean(name="idleBean") @ViewScoped public class IdleBean implements Serializable { private String baseURL; @PostConstruct public void init() { // 动态获取应用BaseURL,避免硬编码 ExternalContext ec = FacesContext.getCurrentInstance().getExternalContext(); baseURL = ec.getRequestScheme() + "://" + ec.getRequestServerName() + ":" + ec.getRequestServerPort() + ec.getRequestContextPath(); } public void expire() throws IOException { HttpSession ss = (HttpSession) FacesContext.getCurrentInstance().getExternalContext().getSession(false); if(ss != null) { ss.invalidate(); } FacesContext.getCurrentInstance().getExternalContext().redirect(baseURL + "/expired.xhtml"); } }
3. 优化LoginBean会话处理
移除构造函数中无条件销毁会话的逻辑,仅在登录成功时清理旧会话;用ExternalContext.redirect()代替JS脚本跳转:
@ManagedBean(name="login") @ViewScoped public class LoginBean implements Serializable { private String username; private String password; private String baseURL; @PostConstruct public void init() { // 用@PostConstruct替代构造函数做初始化 ExternalContext ec = FacesContext.getCurrentInstance().getExternalContext(); baseURL = ec.getRequestScheme() + "://" + ec.getRequestServerName() + ":" + ec.getRequestServerPort() + ec.getRequestContextPath(); } public void login() { // DAO验证登录逻辑 if(loginSuccess) { // 仅登录成功时清理旧会话 HttpSession ss = (HttpSession) FacesContext.getCurrentInstance().getExternalContext().getSession(false); if(ss != null) { ss.invalidate(); } // 创建新会话并设置属性 ss = (HttpSession) FacesContext.getCurrentInstance().getExternalContext().getSession(true); ss.setAttribute("username", username); // 使用标准重定向,确保会话Cookie正确传递 FacesContext.getCurrentInstance().getExternalContext().redirect(baseURL + "/homePage.xhtml"); } } }
4. 修复HomeBean空指针问题
在初始化时检查会话是否为null,避免空指针;若username为空则重定向至登录页:
@ManagedBean(name="home") @ViewScoped public class HomeBean implements Serializable { private String username; @PostConstruct public void init() { HttpSession ss = (HttpSession) FacesContext.getCurrentInstance().getExternalContext().getSession(false); if(ss != null) { username = (String) ss.getAttribute("username"); } // 若会话失效或无用户名,重定向至登录页 if(username == null) { try { ExternalContext ec = FacesContext.getCurrentInstance().getExternalContext(); String baseURL = ec.getRequestScheme() + "://" + ec.getRequestServerName() + ":" + ec.getRequestServerPort() + ec.getRequestContextPath(); ec.redirect(baseURL + "/login.xhtml"); } catch (IOException e) { e.printStackTrace(); } } } }
5. 统一导航方式
- 替换所有JS脚本跳转(如
PrimeFaces.current().executeScript("window.location.replace(...)"))为ExternalContext.redirect() - 若使用JSF导航规则,统一添加
<redirect/>标记,确保跳转时携带会话信息 - 避免混合使用多种导航方式,减少会话状态不一致的风险
内容的提问来源于stack exchange,提问作者PsPro
相关产品推荐
相关产品推荐

