Spring-session 在 pageContext.include 之后从会话中清除属性

Spring-session clears attributes from session after pageContext.include

我想我在spring-session中遇到了一个错误,但我只想在这里问一下它是否真的是一个错误。在我忘记之前

https://github.com/paranoiabla/spring-session-issue.git

这里是重现该问题的 github 存储库。基本上我有 2 个控制器和 2 个 jsps,所以流程是这样的:

谢谢。

问题

问题是,当使用 MapSessionRepository 时,SessionRepositoryFilter 会自动将 HttpSession 同步到 Spring Session,这会覆盖 APIs 的显式使用。具体发生了以下情况:

  1. SessionRepositoryFilter 正在获取当前 Spring 会话。它将它缓存在 HttpServletRequest 中以确保 HttpServletRequest.getSession() 的每次调用都不会进行数据库调用。此 Spring 会话的缓存版本没有与之关联的属性。
  2. HomepageController 获取自己的 Spring Session 副本,对其进行修改,然后保存。
  3. JSP 刷新提交 HttpServletResponse 的响应。这意味着我们必须在设置刷新之前写出会话 cookie。我们还需要确保会话在此时保持不变,因为此后客户端可能会立即访问会话 ID 并能够发出另一个请求。这意味着来自 #1 的 Spring 会话保存时没有覆盖 #2 中保存的会话的属性。
  4. IncludeController 获取从#3 保存的 Spring 会话(没有属性)

解决方案

我认为有两个选项可以解决这个问题。

使用 HttpSession APIs

那么我该如何解决这个问题。最简单的方法是直接停止使用 Spring Session API。无论如何,这是首选,因为如果可能的话,我们不想将自己绑定到 Spring 会话 API。例如,而不是使用以下内容:

@Controller
public class HomepageController {

    @Resource(name = "sessionRepository")
    private SessionRepository<ExpiringSession> sessionRepository;

    @Resource(name = "sessionStrategy")
    private HttpSessionStrategy sessionStrategy;

    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String home(final Model model) {

        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();

        final String sessionIds = sessionStrategy.getRequestedSessionId(request);

        if (sessionIds != null) {
            final ExpiringSession session = sessionRepository.getSession(sessionIds);
            if (session != null) {
                session.setAttribute("attr", "value");
                sessionRepository.save(session);
                model.addAttribute("session", session);
            }
        }

        return "homepage";
    }

}

@Controller
public class IncludeController {

    private final static Logger LOG = LogManager.getLogger(IncludeController.class);

    @Resource(name = "sessionRepository")
    private SessionRepository<ExpiringSession> sessionRepository;

    @Resource(name = "sessionStrategy")
    private HttpSessionStrategy sessionStrategy;

    @RequestMapping(value = "/include", method = RequestMethod.GET)
    public String home(final Model model) {

        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();

        final String sessionIds = sessionStrategy.getRequestedSessionId(request);

        if (sessionIds != null) {
            final ExpiringSession session = sessionRepository.getSession(sessionIds);
            if (session != null) {
                LOG.error(session.getAttributeNames().size());
                model.addAttribute("session", session);
            }
        }

        return "include";
    }
}

您可以使用以下方法简化它:

@Controller
public class HomepageController {

    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String home(HttpServletRequest request, Model model) {

        String sessionIds = request.getRequestedSessionId();

        if (sessionIds != null) {
            final HttpSession session = request.getSession(false);
            if (session != null) {
                session.setAttribute("attr", "value");
                model.addAttribute("session", session);
            }
        }

        return "homepage";
    }

}

@Controller
public class IncludeController {

    @RequestMapping(value = "/include", method = RequestMethod.GET)
    public String home(HttpServletRequest request, final Model model) {

        final String sessionIds = request.getRequestedSessionId();

        if (sessionIds != null) {
            final HttpSession session = request.getSession(false);
            if (session != null) {
                model.addAttribute("session", session);
            }
        }

        return "include";
    }
}

使用 RedisOperationsSessionRepository

当然,如果我们不能直接使用 HttpSession API,这可能会有问题。要处理此问题,您需要使用 SessionRepository 的不同实现。例如,另一个修复是使用 RedisOperationsSessionRepository。这是有效的,因为它足够聪明,只更新已更改的属性。

这意味着在上面的第 3 步中,Redis 实现将仅更新上次访问时间,因为没有更新其他属性。当 IncludeController 请求 Spring 会话时,它仍然会看到保存在 HomepageController 中的属性。

那么为什么 MapSessionRepository 不这样做呢?因为 MapSessionRepository 是基于 Map 的,它是一个全有或全无的东西。当值被放置在映射中时,它是一个单独的放置(我们不能将其分解为多个操作)。