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

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");
    }
}

原因分析

  1. 过滤器线程安全问题:Filter是单例实例,成员变量userName会被所有请求共享,并发场景下会出现值覆盖,导致部分请求获取到的userName为null。同时,调用req.getSession()会强制创建新会话,若原会话已失效,会导致会话状态不一致。
  2. IdleMonitor事件绑定错误:p:idleMonitor的active事件是用户从空闲状态恢复活跃时触发,但当前绑定了销毁会话的expire()方法,会在用户刚操作就销毁会话,完全违背超时销毁的逻辑。
  3. LoginBean会话处理不当:构造函数中无条件销毁现有会话,若用户打开多个登录页标签,可能导致刚创建的有效会话被其他标签页的构造函数销毁。
  4. HomeBean未处理空会话:直接调用ss.getAttribute("username")未检查会话是否为null,会抛出空指针异常,间接导致会话状态异常。
  5. 导航方式混杂:混合使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 12:46:58