I wanted to upgrade my Spring Webflux project from Spring Boot 2.0.1 to Spring Boot 2.0.3. In my project, my sessions are backed by Spring Session Data Redis. When upgrading the Spring Boot version, I noticed a websession invalidation problem from Spring Boot 2.0.2+.
In Spring Boot 2.0.1, I invalidated my sessions this way :
webSession.invalidate().subscribe();
This caused my current session to be destroyed, and a new one was generated with a new session ID, creation time, etc.
However, from Spring Boot 2.0.2, the same code doesn't seem to fully destroy the session. The current session information are just "cleared", and the same session ID is still used afterward. This is a problem because it causes a null pointer exception in ReactiveRedisOperationsSessionRepository.class :
private static final class SessionMapper implements Function<Map<String, Object>, MapSession> {
private final String id;
private SessionMapper(String id) {
this.id = id;
}
public MapSession apply(Map<String, Object> map) {
MapSession session = new MapSession(this.id);
session.setCreationTime(Instant.ofEpochMilli((Long)map.get("creationTime")));
session.setLastAccessedTime(Instant.ofEpochMilli((Long)map.get("lastAccessedTime")));
session.setMaxInactiveInterval(Duration.ofSeconds((long)(Integer)map.get("maxInactiveInterval")));
map.forEach((name, value) -> {
if (name.startsWith("sessionAttr:")) {
session.setAttribute(name.substring("sessionAttr:".length()), value);
}
});
return session;
}
}
As session data are empty (no creation time, etc.), the following line raises a NPE:
session.setCreationTime(Instant.ofEpochMilli((Long)map.get("creationTime")));
Is it a bug or have I missed something new in Spring Boot 2.0.2+ regarding Spring Session?
UPDATE
To provide more information, I created a sample project reproducing the problem: https://github.com/adsanche/test-redis-session
This project contains a simple controller exposing two endpoints:
#Controller
public class HelloController {
#GetMapping(value = "/hello")
public String hello(final WebSession webSession) {
webSession.getAttributes().put("test", "TEST");
return "index";
}
#GetMapping(value = "/invalidate")
public String invalidate(final WebSession webSession) {
webSession.invalidate().subscribe();
return UrlBasedViewResolver.REDIRECT_URL_PREFIX + "/hello";
}
}
Behavior when running the project with Spring Boot 2.0.1
Go to the first endpoint: http://localhost:8080/hello
State of the session in Redis:
We notice the session ID starting with 7546ff, and the session data containing the "test" attribute plus the default session information (creation/last access time, etc.).
Go to the second endpoint: http://localhost:8080/invalidate
The current session is invalidated, and a redirection is performed on "/hello", where the test attribute is added in a new web session.
State of the new session in Redis:
We notice the new session ID starting with ba7de, and the new session data still containing the test attribute plus the default session information.
Now, let's reproduce this scenario on the same project with Spring Boot 2.0.3.
Behavior when running the project with Spring Boot 2.0.3
Go to the first endpoint: http://localhost:8080/hello
State of the session in Redis:
We notice the session ID starting with 12d61, and the session data containing the "test" attribute plus the default session information (creation/last access time, etc.).
Go to the second endpoint: http://localhost:8080/invalidate
State of the new session in Redis:
We notice here that the same session ID is still used, but the session has been cleared even from its default information (creation date, etc.). So, when it is invoked again, a NPE is triggered in the reactive redis session repository in the apply() method at the place I mention in my original post:
Hope this sample project helps to find out if its a bug or an implementation problem on my side.
This is a bug in Spring Session's SpringSessionWebSessionStore, or more specifically, its internal WebSession implementation. The problem is that on WebSession#invalidate session is only cleared from the underlying session store, but not marked as invalid so the subsequent request processing still ends up using the same session id.
I've openend gh-1114 to address this in Spring Session.
I believe you haven't experienced this previously i.e. with Spring Session 2.0.2.RELEASE and below due to another bug in SpringSessionWebSessionStore that was addressed in 2.0.3.RELEASE - see gh-1039. This issue was about failing to update the lastAccessedTime, and once we fixed that the scenario you described started failing due to session for invalidated (and deleted) session id being saved again only with lastAccessedTime attribute.
Related
We use Spring-session. And we use JavaMelody for monitoring sessions.
JavaMelody uses net.bull.javamelody.SessionListener for monitoring sessions. It is implementation of HttpSessionListener from servlet API.
Spring-session have SessionEventHttpSessionListenerAdapter for capability with servlet api session listeners. It listens Spring session events and submit http session events. Wherein it creates ExpiringSessionHttpSession (wrapper for spring session).
private HttpSessionEvent createHttpSessionEvent(AbstractSessionEvent event) {
ExpiringSession session = event.getSession();
HttpSession httpSession = new ExpiringSessionHttpSession<ExpiringSession>(session,
this.context);
HttpSessionEvent httpSessionEvent = new HttpSessionEvent(httpSession);
return httpSessionEvent;
}
We use SessionEventHttpSessionListenerAdapter for working net.bull.javamelody.SessionListener. Spring sessions successful displays in JavaMelody sessions view.
But we have a problem. JavaMelody button "invalidate session" doesn't work correct. JavaMelody uses http session method invalidate for handling this button. The implementation method in ExpiringSessionHttpSession (part of Spring) looks like dummy:
public void invalidate() {
checkState();
this.invalidated = true;
}
In facts, it only set invalidate = true in adapter, but it is not invalidate session in Spring session registry. Real spring session continues be valid, and user can be used this session.
Could you have solution for this problem?
I found some solution but I don't think that it's good. We can override spring SessionEventHttpSessionListenerAdapter and ExpiringSessionHttpSession. And we can override method invalidate (adding call session registry). In this case, JavaMelody will invalidate session in registry.
Probably exists solution is better. I would be glad to get it.
I am implementing a redis-shiro session management feature in my project and currently I have very little information about Shiro & Redis.
I want to know whether calling the below will hit the redis database everytime to check whether any sessionId exist in the redis database or not.
Code in Service
Subject currentUser = SecurityUtils.getSubject();
Session session = currentUser.getSession();
Code in Controller:
public String getSomrthing(#CookieValue("JSESSIONID") String fooCookie){
callingSomeServiceMethod(fooCookie);
return "It does not matter";
}
Does we have to match the sessionId manually like below in our service or does Shiro will match it automatically, since my application will be running in a multi instance environment.?
Subject currentUser = SecurityUtils.getSubject();
if(currentUser.getId.equals(fooCookie)){
//.....Some Code
//.....Some Code
}
The session will looked up a max of once per request, less depending on any additional caching you have configured.
You wouldn't manage/lookup the sessionId directly from your controller though. All of this logic would be transparent and handled by Shiro and/or your servlet container's session management.
In our spring boot application we have a paging loop over the database which will send a JMS message for every page using a JMSTemplate. The method containing the loop is #Transactional. The JMSTemplate is created with the transacted flag set to true.
I have been browsing through the source code of JMSTemplate and as far as I can see it will not commit a transacted session if there is already a outer transaction going on but will put it into that transaction.
Let's now consider the following code:
#Transactional
public void loopThroughPages(String destination, String from, String to) {
Pageable pageRequest = PageRequest.of(0, maxPageSize);
Page<Event> eventPage;
do {
eventPage = eventRepo.getEventsInTimeRangeForDestination(from, to, destination, pageRequest);
if(eventPage.hasContent()) {
Message<String> eventMessage = buildEventMessage(eventPage.getContent());
JmsTemplate template = queueService.createTemplate(destination);
template.send(eventMessage);
pageRequest = eventPage.nextPageable();
}
} while(pageRequest != null && eventPage.hasNext());
}
The createTemplate creates a DynamicJmsTemplate using a CachingConnectionFactory and setSessionTransacted to true
I am now not entirely sure how this translates to the transactions. My understanding is that all N page messages are sent in the transaction created from the loopThroughPages and once the loopThroughPages method finishes it will commit all N messages and not after each message has been sent. That also implies that the transaction on MQ side will stay open until the last page has been processed. Is this understanding correct?
The key point here is the Transastion managment.
If you use a XA datasource and configure it on your spring boot app you will have a distributed transaction and the commit/rollback for your transaction will be managed by spring since that you have a method annotated with #Transactional otherwise you will have a local transaction managment and the transaction of your database and messaging system will be not syncronized.
For sending message you can by properties configure if the message are persisted or not that it means if your messages will be persisted by your messaging system, while for listener perspective you can configure the acknowledged mode.
By the way the my advice is let to spring managed the transaction and all will be fine but the real point of attention is if you want a distributed transaction between database and jms system you have only configure it atomikos can be an available option, otherwise it is sufficient do not manage the transaction by hand and let to Spring manage it for you.
I hope that it can help you
I am using Spring Session 1.0.1. I need to execute some logic when the user logs out, and I need to rely on the HTTP session being invalidated to cover the case where the user fails to explicitly log out.
The standard Spring Security SessionDestroyedEvent includes any applicable SecurityContext, but the Spring Session version of SessionDestroyedEvent only contains the session id. By the time this event fires, the session is no longer held by the SessionRepository so it can't be looked up by id.
Is there any way to retrieve the SecurityContext from the expired session using Spring Session?
Unfortunately there is not. The problem is that at the time Redis fires the event, the session is already gone. Furthermore, the event received from Redis does not contain the original information. This means there is no way to retrieve the SecurityContext.
For updates on this please track spring-projects/spring-session/issues/4
For sring-session 1.1+ with Redis
https://docs.spring.io/spring-session/docs/current/reference/html5/#httpsession-httpsessionlistener
You must configure HttpSessionEventPublisher and after that spring-session will propagate sessionDestroy event
#Configuration
#EnableRedisHttpSession
public class RedisHttpSessionConfig {
#Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
// ...
}
So you can use standard spting SessionDestroyedEvent listener
#Component
public class SessionDestroyListener implements ApplicationListener<SessionDestroyedEvent> {
#Override
public void onApplicationEvent(SessionDestroyedEvent event) {
logger.debug("session destroyed {}", event.getId());
if(!event.getSecurityContexts().isEmpty()) {
...
}
}
}
I'm trying to integrate Hazelcast (lasted version - 3.3) session replication.
Our infrastructure consists of :
Apache 2.0 server for the load balancing
Tomcat 7 servers serving our web application
Our main reasons are:
Duplicate user session for high availability
Atmosphere web-socket PubSub servlet need share the same data in order to make full
broadcasting
integrating Hazelcast to our Environment:
each of the tomcat servers will serve as Hazelcast member
basically Hazelcast WebFilter is the first one that executes and its
wrap with : WebFilter->HazelcastSession->setAttribute() implements
HttpSession interface
each time setAttribute is called Hazelcast sync the session attribute
with the rest of the cluster members.
now - its seems like every spring bean we injecting scoped as session bean don't get replicated .
as a workaround :
Save only basic session information via #Context annotation
Dont use Spring session scope , only Singletons and inject the HazelcastInstance
I can Wrap the relevant data as Hazelcast structures
Also, when i looked on other stackoverflow i saw the following : Spring "session" scope of a bean?
The Spring session does not exactly match the HttpSession, and even
the Spring documentation on the #SessionAttributes annotation says
that it might be stored in the session or "some conversational
storage". I got that from [The Spring docs for 2.5][1] I've basically
quit trying to make sense of it, and just got on with my life, if I
want something stored in the HttpSession, I just have Spring inject
the HttpSession to me, assuming you're using Spring MVC its pretty
easy, instructions on the same page.
[1]:
http://static.springsource.org/spring/docs/2.5.x/reference/mvc.html
Its seems strange , Does Spring session beans not exactly match the HttpSession.setAttribute ?
How spring know how to #Inject the proper bean ?
Maybe Spring save the Beans in an internal data storage and only in
the Injection phase Spring getting the proper element using the same
session id attribute and bind the proper bean.
is there any way to control this behavior ?
Update :
debugging spring-web -> ServletRequestAttributes-> is using the
Server impl HTTPSession (For example - in Dev Jetty - org.eclipse.jetty.server.session.HashedSession)
this way the Bean is update in the HTTPSession but skipping the
HazelcastSession :-(
/**
* Update all accessed session attributes through {#code session.setAttribute}
* calls, explicitly indicating to the container that they might have been modified.
*/
#Override
protected void updateAccessedSessionAttributes() {
// Store session reference for access after request completion.
this.session = this.request.getSession(false);
// Update all affected session attributes.
if (this.session != null) {
try {
for (Map.Entry<String, Object> entry : this.sessionAttributesToUpdate.entrySet()) {
String name = entry.getKey();
Object newValue = entry.getValue();
Object oldValue = this.session.getAttribute(name);
if (oldValue == newValue) {
this.session.setAttribute(name, newValue);
}
}
} catch (IllegalStateException ex) {
// Session invalidated - shouldn't usually happen.
}
}
this.sessionAttributesToUpdate.clear();
}
thanks in advance ,
elad.