2
votes

I think I have come across a bug in spring-session but I just want to ask here if it really is a bug. Before I forget

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

here's a github repository that reproduces the problem. Basically I have a 2 controllers and 2 jsps, so the flow goes like this:

  • User opens http://localhost:8080/ and the flow goes through HomepageController, which puts 1 attribute in the spring-session and returns the homepage.jsp which renders the session id and the number of attributes (1)
  • The homepage.jsp has this line inside it: ${pageContext.include("/include")} which calls the IncludeController to be invoked.
  • The IncludeController finds the session from the session repository and LOGs the number of attributes (now absolutely weird they are logged as 0) and returns the include.jsp which renders both the session id and the number of session attributes (0). The session id in both jsps is the same, but somehow after the pageContext.include call the attributes were reset to an empty map!!! Can someone please confirm if this is a bug.

Thank you.

1

1 Answers

2
votes

Problem

The problem is that when using MapSessionRepository the SessionRepositoryFilter will automatically sync the HttpSession to the Spring Session which overrides explicit use of the APIs. Specifically the following is happening:

  1. SessionRepositoryFilter is obtaining the current Spring Session. It caches it in the HttpServletRequest to ensure that every invocation of HttpServletRequest.getSession() does not make a database call. This cached version of the Spring Session has no attributes associated with it.
  2. The HomepageController obtains its own copy of Spring Session, modifies it, and then saves it.
  3. The JSP flushes the response which commits the HttpServletResponse. This means we must write out the session cookie just prior to the flush being set. We also need to ensure that the session is persisted at this point because immediately afterwards the client may have access to the session id and be able to make another request. This means that the Spring Session from #1 is saved with no attributes which overrides the session saved in #2.
  4. The IncludeController obtains the Spring Session that was saved from #3 (which has no attributes)

Solution

There are two options I see to solving this.

Use HttpSession APIs

So how would I solve this. The easiest approach is to stop using the Spring Session APIs directly. This is preferred anyways since we do not want to tie ourselves to the Spring Session APIs if possible. For example, instead of using the following:

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

You can simplify it using the following:

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

Use RedisOperationsSessionRepository

Of course this may be problematic in the event that we cannot use the HttpSession API directly. To handle this, you need to use a different implementation of SessionRepository. For example, another fix is to use the RedisOperationsSessionRepository. This works because it is smart enough to only update attributes that have been changed.

This means in step #3 from above, the Redis implementation will only update the last accessed time since no other attributes were updated. When the IncludeController requests the Spring Session it will still see the attribute saved in HomepageController.

So why doesn't MapSessionRepository do this? Because MapSessionRepository is based on a Map which is an all or nothing thing. When the value is placed in the map it is a single put (we cannot break that up into multiple operations).