Skip to content
This repository was archived by the owner on Apr 4, 2025. It is now read-only.

Track change of MongoSession's id to properly delete. #117

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,24 @@
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webflux</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>de.flapdoodle.embed</groupId>
<artifactId>de.flapdoodle.embed.mongo</artifactId>
Expand Down Expand Up @@ -575,6 +593,27 @@
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest</artifactId>
<version>2.1</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.2.3</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public class MongoSession implements Session {
private static final char DOT_COVER_CHAR = '';

private String id;
private String originalSessionId;
private long createdMillis = System.currentTimeMillis();
private long accessedMillis;
private long intervalSeconds;
Expand All @@ -60,6 +61,7 @@ public MongoSession(long maxInactiveIntervalInSeconds) {
public MongoSession(String id, long maxInactiveIntervalInSeconds) {

this.id = id;
this.originalSessionId = id;
this.intervalSeconds = maxInactiveIntervalInSeconds;
setLastAccessedTime(Instant.ofEpochMilli(this.createdMillis));
}
Expand All @@ -72,6 +74,7 @@ static String uncoverDot(String attributeName) {
return attributeName.replace(DOT_COVER_CHAR, '.');
}

@Override
public String changeSessionId() {

String changedId = UUID.randomUUID().toString();
Expand All @@ -85,10 +88,12 @@ public <T> T getAttribute(String attributeName) {
return (T) this.attrs.get(coverDot(attributeName));
}

@Override
public Set<String> getAttributeNames() {
return this.attrs.keySet().stream().map(MongoSession::uncoverDot).collect(Collectors.toSet());
}

@Override
public void setAttribute(String attributeName, Object attributeValue) {

if (attributeValue == null) {
Expand All @@ -98,10 +103,12 @@ public void setAttribute(String attributeName, Object attributeValue) {
}
}

@Override
public void removeAttribute(String attributeName) {
this.attrs.remove(coverDot(attributeName));
}

@Override
public Instant getCreationTime() {
return Instant.ofEpochMilli(this.createdMillis);
}
Expand All @@ -110,24 +117,29 @@ public void setCreationTime(long created) {
this.createdMillis = created;
}

@Override
public Instant getLastAccessedTime() {
return Instant.ofEpochMilli(this.accessedMillis);
}

@Override
public void setLastAccessedTime(Instant lastAccessedTime) {

this.accessedMillis = lastAccessedTime.toEpochMilli();
this.expireAt = Date.from(lastAccessedTime.plus(Duration.ofSeconds(this.intervalSeconds)));
}

@Override
public Duration getMaxInactiveInterval() {
return Duration.ofSeconds(this.intervalSeconds);
}

@Override
public void setMaxInactiveInterval(Duration interval) {
this.intervalSeconds = interval.getSeconds();
}

@Override
public boolean isExpired() {
return this.intervalSeconds >= 0 && new Date().after(this.expireAt);
}
Expand All @@ -140,14 +152,15 @@ public boolean equals(Object o) {
if (o == null || getClass() != o.getClass())
return false;
MongoSession that = (MongoSession) o;
return Objects.equals(id, that.id);
return Objects.equals(this.id, that.id);
}

@Override
public int hashCode() {
return Objects.hash(id);
return Objects.hash(this.id);
}

@Override
public String getId() {
return this.id;
}
Expand All @@ -159,4 +172,12 @@ public Date getExpireAt() {
public void setExpireAt(final Date expireAt) {
this.expireAt = expireAt;
}

boolean hasChangedSessionId() {
return !getId().equals(this.originalSessionId);
}

String getOriginalSessionId() {
return this.originalSessionId;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
package org.springframework.session.data.mongo;

import static org.springframework.data.mongodb.core.query.Criteria.*;
import static org.springframework.data.mongodb.core.query.Query.*;
import static org.springframework.session.data.mongo.MongoSessionUtils.*;

import java.time.Duration;
Expand Down Expand Up @@ -94,7 +96,13 @@ public Mono<Void> save(MongoSession session) {

DBObject dbObject = convertToDBObject(this.mongoSessionConverter, session);
if (dbObject != null) {
return this.mongoOperations.save(dbObject, this.collectionName).then();
if (session.hasChangedSessionId()) {
return this.mongoOperations.findAndRemove(query(where("_id").is(session.getOriginalSessionId())), MongoSession.class, this.collectionName)
.then(this.mongoOperations.save(dbObject, this.collectionName))
.then();
} else {
return this.mongoOperations.save(dbObject, this.collectionName).then();
}
} else {
return Mono.empty();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.session.data.mongo.integration;

import static org.assertj.core.api.AssertionsForClassTypes.*;

import java.io.IOException;
import java.net.URI;

import de.flapdoodle.embed.mongo.MongodExecutable;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import reactor.test.StepVerifier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.session.data.mongo.config.annotation.web.reactive.EnableMongoWebSession;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.reactive.server.FluxExchangeResult;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.util.SocketUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.reactive.function.BodyInserters;

import com.mongodb.reactivestreams.client.MongoClient;
import com.mongodb.reactivestreams.client.MongoClients;

/**
* @author Greg Turnquist
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration
public class MongoDbLogoutVerificationTest {

@Autowired ApplicationContext ctx;

WebTestClient client;

@BeforeEach
void setUp() {
this.client = WebTestClient.bindToApplicationContext(this.ctx).build();
}

@Test
void logoutShouldDeleteOldSessionIdFromMongoDB() {

// 1. `curl -i -v -X POST --data "username=admin&password=password" localhost:8080/login` - Save SESSION cookie and
// use it it nex step as {cookie-value-1}

FluxExchangeResult<String> loginResult = this.client.post().uri("/login")
.contentType(MediaType.APPLICATION_FORM_URLENCODED) //
.body(BodyInserters //
.fromFormData("username", "admin") //
.with("password", "password")) //
.exchange() //
.returnResult(String.class);

assertThat(loginResult.getResponseHeaders().getLocation()).isEqualTo(URI.create("/"));

String originalSessionId = loginResult.getResponseCookies().getFirst("SESSION").getValue();

// 2. `curl -i -L -v -X GET --cookie "SESSION=48eb6ab2-2c08-43b7-a303-46099bfef231" localhost:8080/hello` - response
// status will be 200, body will be "HelloWorld"

this.client.get().uri("/hello") //
.cookie("SESSION", originalSessionId) //
.exchange() //
.expectStatus().isOk() //
.returnResult(String.class).getResponseBody() //
.as(StepVerifier::create) //
.expectNext("HelloWorld") //
.verifyComplete();

// 3. `curl -i -L -v -X POST --cookie "SESSION=48eb6ab2-2c08-43b7-a303-46099bfef231" localhost:8080/logout` - Save
// SESSION cookie and use it it nex step as {cookie-value-2}

String newSessionId = this.client.post().uri("/logout") //
.cookie("SESSION", originalSessionId) //
.exchange() //
.expectStatus().isFound() //
.returnResult(String.class)
.getResponseCookies().getFirst("SESSION").getValue();

assertThat(newSessionId).isNotEqualTo(originalSessionId);

// 4. `curl -i -L -v -X GET --cookie "SESSION=3b20200c-cf5e-4529-b3af-3c37ed365f5a" localhost:8080/hello` - response
// status will be 302, body will be empty

this.client.get().uri("/hello") //
.cookie("SESSION", newSessionId) //
.exchange() //
.expectStatus().isFound() //
.expectHeader().value(HttpHeaders.LOCATION, value -> assertThat(value).isEqualTo("/login"));

// 5. `curl -i -L -v -X GET --cookie "SESSION=48eb6ab2-2c08-43b7-a303-46099bfef231" localhost:8080/hello` - response
// status will be 200, body will be "HelloWorld", but it should be the same as step 4

this.client.get().uri("/hello") //
.cookie("SESSION", originalSessionId) //
.exchange() //
.expectStatus().isFound() //
.expectHeader().value(HttpHeaders.LOCATION, value -> assertThat(value).isEqualTo("/login"));
}

@RestController
static class TestController {

@GetMapping("/hello")
public ResponseEntity<String> hello() {
return ResponseEntity.ok("HelloWorld");
}

}

@EnableWebFluxSecurity
static class SecurityConfig {

@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {

return http //
.logout()//
/**/.and() //
.formLogin() //
/**/.and() //
.csrf().disable() //
.authorizeExchange() //
.anyExchange().authenticated() //
/**/.and() //
.build();
}

@Bean
public MapReactiveUserDetailsService userDetailsService() {

return new MapReactiveUserDetailsService(User.withDefaultPasswordEncoder() //
.username("admin") //
.password("password") //
.roles("USER,ADMIN") //
.build());
}
}

@Configuration
@EnableWebFlux
@EnableMongoWebSession
static class Config {

private int embeddedMongoPort = SocketUtils.findAvailableTcpPort();

@Bean(initMethod = "start", destroyMethod = "stop")
public MongodExecutable embeddedMongoServer() throws IOException {
return MongoITestUtils.embeddedMongoServer(this.embeddedMongoPort);
}

@Bean
public ReactiveMongoOperations mongoOperations(MongodExecutable embeddedMongoServer) {

MongoClient mongo = MongoClients.create("mongodb://localhost:" + this.embeddedMongoPort);
return new ReactiveMongoTemplate(mongo, "test");
}

@Bean
TestController controller() {
return new TestController();
}
}
}
Loading