Fix Jackson MongoSession deserialization. (#122)
It used originalSessionId at moment when it wasn't necessary. Added mixin with correct deserialization behavior. Resolves #119.
This commit is contained in:
committed by
Greg Turnquist
parent
d0bb1f13e0
commit
50c518493a
@@ -20,6 +20,8 @@ import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.bson.Document;
|
||||
@@ -126,6 +128,7 @@ public class JacksonMongoSessionConverter extends AbstractMongoSessionConverter
|
||||
protected MongoSession convert(Document source) {
|
||||
|
||||
Date expireAt = (Date) source.remove(EXPIRE_AT_FIELD_NAME);
|
||||
source.remove("originalSessionId");
|
||||
String json = source.toJson(JsonWriterSettings.builder().outputMode(JsonMode.RELAXED).build());
|
||||
|
||||
try {
|
||||
@@ -142,7 +145,10 @@ public class JacksonMongoSessionConverter extends AbstractMongoSessionConverter
|
||||
* Used to whitelist {@link MongoSession} for {@link SecurityJackson2Modules}.
|
||||
*/
|
||||
private static class MongoSessionMixin {
|
||||
// Nothing special
|
||||
@JsonCreator
|
||||
public MongoSessionMixin(@JsonProperty("_id") String id,
|
||||
@JsonProperty("intervalSeconds") long maxInactiveIntervalInSeconds) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
* 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 com.mongodb.reactivestreams.client.MongoClient;
|
||||
import com.mongodb.reactivestreams.client.MongoClients;
|
||||
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 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.AbstractMongoSessionConverter;
|
||||
import org.springframework.session.data.mongo.JacksonMongoSessionConverter;
|
||||
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 reactor.test.StepVerifier;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
|
||||
/**
|
||||
* @author Boris Finkelshteyn
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration
|
||||
public class MongoDbDeleteJacksonSessionVerificationTest {
|
||||
|
||||
@Autowired ApplicationContext ctx;
|
||||
|
||||
WebTestClient client;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
this.client = WebTestClient.bindToApplicationContext(this.ctx).build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void logoutShouldDeleteOldSessionFromMongoDB() {
|
||||
|
||||
// 1. Login and capture the SESSION cookie value.
|
||||
|
||||
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. Fetch a protected resource using the SESSION cookie.
|
||||
|
||||
this.client.get().uri("/hello") //
|
||||
.cookie("SESSION", originalSessionId) //
|
||||
.exchange() //
|
||||
.expectStatus().isOk() //
|
||||
.returnResult(String.class).getResponseBody() //
|
||||
.as(StepVerifier::create) //
|
||||
.expectNext("HelloWorld") //
|
||||
.verifyComplete();
|
||||
|
||||
// 3. Logout using the SESSION cookie, and capture the new SESSION cookie.
|
||||
|
||||
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. Verify the new SESSION cookie is not yet authorized.
|
||||
|
||||
this.client.get().uri("/hello") //
|
||||
.cookie("SESSION", newSessionId) //
|
||||
.exchange() //
|
||||
.expectStatus().isFound() //
|
||||
.expectHeader().value(HttpHeaders.LOCATION, value -> assertThat(value).isEqualTo("/login"));
|
||||
|
||||
// 5. Verify the original SESSION cookie no longer works.
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AbstractMongoSessionConverter mongoSessionConverter() {
|
||||
return new JacksonMongoSessionConverter();
|
||||
}
|
||||
}
|
||||
|
||||
@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, "DB_Name_DeleteJacksonSessionVerificationTest");
|
||||
}
|
||||
|
||||
@Bean
|
||||
TestController controller() {
|
||||
return new TestController();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user