Skip to content

Commit 641a21e

Browse files
committed
PMW-55: json sessie serialisatie
aangepaste klassen gekopieerd + Jackson 3 code voor converters
1 parent 56b2cd2 commit 641a21e

10 files changed

Lines changed: 275 additions & 49 deletions

File tree

build/ci/docker-compose.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,19 @@
44
name: 'planmonitor-wonen-ci'
55

66
volumes:
7-
postgis-db:
7+
planmonitor-wonen-data:
88

99

1010
services:
1111
db:
1212
image: postgis/postgis:18-3.6-alpine
1313
environment:
14+
TZ: Europe/Amsterdam
1415
POSTGRES_USER: planmonitor-wonen
1516
POSTGRES_PASSWORD: planmonitor-wonen
1617
POSTGRES_DB: planmonitor-wonen
1718
volumes:
18-
- postgis-db:/var/lib/postgresql/data
19+
- planmonitor-wonen-data:/var/lib/postgresql/data
1920
- ./initdb:/docker-entrypoint-initdb.d
2021
ports:
2122
- "127.0.0.1:5432:5432"
@@ -30,6 +31,7 @@ services:
3031
tailormap:
3132
image: ghcr.io/tailormap/tailormap:snapshot
3233
environment:
34+
- TZ=Europe/Amsterdam
3335
- SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/tailormap
3436
- SPRING_PROFILES_ACTIVE=populate-testdata
3537
- ADMIN_HASHED_PASSWORD

pom.xml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,10 @@ SPDX-License-Identifier: MIT
194194
<groupId>org.springframework.boot</groupId>
195195
<artifactId>spring-boot-starter-aspectj</artifactId>
196196
</dependency>
197+
<dependency>
198+
<groupId>org.springframework.boot</groupId>
199+
<artifactId>spring-boot-starter-json</artifactId>
200+
</dependency>
197201
<dependency>
198202
<groupId>org.springframework.boot</groupId>
199203
<artifactId>spring-boot-starter-security</artifactId>
@@ -245,10 +249,6 @@ SPDX-License-Identifier: MIT
245249
<groupId>org.apache.commons</groupId>
246250
<artifactId>commons-lang3</artifactId>
247251
</dependency>
248-
<dependency>
249-
<groupId>org.springframework.boot</groupId>
250-
<artifactId>spring-boot-starter-json</artifactId>
251-
</dependency>
252252
<dependency>
253253
<groupId>org.geotools</groupId>
254254
<artifactId>gt-main</artifactId>

src/main/java/nl/b3p/planmonitorwonen/api/configuration/JdbcSessionConfiguration.java

Lines changed: 112 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,14 @@
66

77
package nl.b3p.planmonitorwonen.api.configuration;
88

9-
import java.io.InputStream;
10-
import java.io.ObjectInputStream;
9+
import com.fasterxml.jackson.annotation.JsonTypeInfo;
10+
import java.lang.invoke.MethodHandles;
11+
import java.nio.charset.StandardCharsets;
1112
import javax.sql.DataSource;
13+
import org.jspecify.annotations.NonNull;
14+
import org.slf4j.Logger;
15+
import org.slf4j.LoggerFactory;
16+
import org.springframework.beans.factory.BeanClassLoaderAware;
1217
import org.springframework.beans.factory.annotation.Qualifier;
1318
import org.springframework.beans.factory.annotation.Value;
1419
import org.springframework.boot.jdbc.DataSourceBuilder;
@@ -17,19 +22,28 @@
1722
import org.springframework.context.annotation.Profile;
1823
import org.springframework.core.convert.ConversionService;
1924
import org.springframework.core.convert.support.GenericConversionService;
20-
import org.springframework.core.serializer.Deserializer;
21-
import org.springframework.core.serializer.support.DeserializingConverter;
22-
import org.springframework.core.serializer.support.SerializingConverter;
2325
import org.springframework.jdbc.core.simple.JdbcClient;
2426
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
27+
import org.springframework.security.jackson.SecurityJacksonModules;
28+
import org.springframework.session.config.SessionRepositoryCustomizer;
29+
import org.springframework.session.jdbc.JdbcIndexedSessionRepository;
2530
import org.springframework.session.jdbc.config.annotation.SpringSessionDataSource;
2631
import org.springframework.session.jdbc.config.annotation.web.http.EnableJdbcHttpSession;
2732
import org.springframework.transaction.PlatformTransactionManager;
33+
import tools.jackson.core.JacksonException;
34+
import tools.jackson.core.StreamReadFeature;
35+
import tools.jackson.databind.DefaultTyping;
36+
import tools.jackson.databind.SerializationFeature;
37+
import tools.jackson.databind.json.JsonMapper;
38+
import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
2839

2940
@Configuration
3041
@EnableJdbcHttpSession
3142
@Profile("!test")
32-
public class JdbcSessionConfiguration {
43+
public class JdbcSessionConfiguration implements BeanClassLoaderAware {
44+
private static final Logger logger =
45+
LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
46+
3347
@Value("${spring.datasource.url}")
3448
private String dataSourceUrl;
3549

@@ -48,6 +62,27 @@ public class JdbcSessionConfiguration {
4862
@Value("${tailormap.datasource.password}")
4963
private String sessionDataSourcePassword;
5064

65+
private static final String CREATE_SESSION_ATTRIBUTE_QUERY =
66+
"""
67+
INSERT INTO %TABLE_NAME%_ATTRIBUTES (SESSION_PRIMARY_ID, ATTRIBUTE_NAME, ATTRIBUTE_BYTES)
68+
VALUES (?, ?, convert_from(?, 'UTF8')::jsonb)
69+
""";
70+
71+
private static final String UPDATE_SESSION_ATTRIBUTE_QUERY =
72+
"""
73+
UPDATE %TABLE_NAME%_ATTRIBUTES
74+
SET ATTRIBUTE_BYTES = encode(?, 'escape')::jsonb
75+
WHERE SESSION_PRIMARY_ID = ?
76+
AND ATTRIBUTE_NAME = ?
77+
""";
78+
79+
private ClassLoader classLoader;
80+
81+
@Override
82+
public void setBeanClassLoader(@NonNull ClassLoader classLoader) {
83+
this.classLoader = classLoader;
84+
}
85+
5186
@Bean
5287
public DataSource dataSource() {
5388
DataSourceBuilder<?> dataSourceBuilder = DataSourceBuilder.create();
@@ -67,6 +102,14 @@ public JdbcClient tailormapJdbcClient(@Qualifier("tailormapDataSource") DataSour
67102
return JdbcClient.create(data);
68103
}
69104

105+
@Bean
106+
SessionRepositoryCustomizer<JdbcIndexedSessionRepository> customizer() {
107+
return (sessionRepository) -> {
108+
sessionRepository.setCreateSessionAttributeQuery(CREATE_SESSION_ATTRIBUTE_QUERY);
109+
sessionRepository.setUpdateSessionAttributeQuery(UPDATE_SESSION_ATTRIBUTE_QUERY);
110+
};
111+
}
112+
70113
@Bean(name = {"springSessionDataSource", "tailormapDataSource"})
71114
@SpringSessionDataSource
72115
public DataSource sessionDataSource() {
@@ -85,20 +128,70 @@ public PlatformTransactionManager springSessionTransactionOperations(
85128

86129
@Bean("springSessionConversionService")
87130
public ConversionService springSessionConversionService() {
88-
GenericConversionService converter = new GenericConversionService();
89-
converter.addConverter(Object.class, byte[].class, new SerializingConverter());
90-
converter.addConverter(byte[].class, Object.class, new DeserializingConverter(new CustomDeserializer()));
91-
return converter;
92-
}
131+
BasicPolymorphicTypeValidator.Builder builder = BasicPolymorphicTypeValidator.builder()
132+
.allowIfSubType("org.tailormap.api.security.")
133+
.allowIfSubType("org.springframework.security.")
134+
.allowIfSubType("java.util.")
135+
.allowIfSubType(java.lang.Number.class)
136+
.allowIfSubType("java.time.")
137+
.allowIfBaseType(Object.class);
138+
139+
JsonMapper mapper = JsonMapper.builder()
140+
.configure(
141+
StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION,
142+
(logger.isDebugEnabled() || logger.isTraceEnabled()))
143+
.configure(SerializationFeature.INDENT_OUTPUT, (logger.isDebugEnabled() || logger.isTraceEnabled()))
144+
.addMixIn(
145+
org.tailormap.api.security.TailormapUserDetailsImpl.class,
146+
org.tailormap.api.security.TailormapUserDetailsImplMixin.class)
147+
.addMixIn(
148+
org.tailormap.api.security.TailormapOidcUser.class,
149+
org.tailormap.api.security.TailormapOidcUserMixin.class)
150+
.addModules(SecurityJacksonModules.getModules(this.classLoader, builder))
151+
.activateDefaultTyping(builder.build(), DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY)
152+
.build();
93153

94-
static class CustomDeserializer implements Deserializer<Object> {
95-
@Override
96-
public Object deserialize(InputStream inputStream) {
97-
try (ObjectInputStream ois = new ObjectInputStream(inputStream)) {
98-
return ois.readObject();
99-
} catch (Exception ignored) {
100-
return null;
154+
final GenericConversionService converter = new GenericConversionService();
155+
// Object -> byte[] (serialize to JSON bytes)
156+
converter.addConverter(Object.class, byte[].class, source -> {
157+
try {
158+
logger.debug("Serializing Spring Session: {}", source);
159+
return mapper.writerFor(Object.class).writeValueAsBytes(source);
160+
} catch (JacksonException e) {
161+
logger.error("Error serializing Spring Session object: {}", source, e);
162+
throw e;
101163
}
102-
}
164+
});
165+
// byte[] -> Object (deserialize from JSON bytes)
166+
converter.addConverter(byte[].class, Object.class, source -> {
167+
try {
168+
logger.debug(
169+
"Deserializing Spring Session from bytes, length: {} ({})",
170+
source.length,
171+
new String(source, StandardCharsets.UTF_8));
172+
return mapper.readValue(source, Object.class);
173+
} catch (JacksonException e) {
174+
String preview;
175+
try {
176+
String content = new String(source, StandardCharsets.UTF_8);
177+
int maxLength = 256;
178+
if ((logger.isDebugEnabled() || logger.isTraceEnabled())) {
179+
preview = content;
180+
} else {
181+
preview = content.length() > maxLength ? content.substring(0, maxLength) + "..." : content;
182+
}
183+
} catch (Exception ex) {
184+
preview = "<unavailable>";
185+
}
186+
logger.error(
187+
"Error deserializing Spring Session from bytes, length: {}, preview: {}",
188+
source.length,
189+
preview,
190+
e);
191+
throw e;
192+
}
193+
});
194+
195+
return converter;
103196
}
104197
}

src/main/java/nl/b3p/planmonitorwonen/api/configuration/PopulateTestData.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ public void init() throws ParseException {
131131

132132
private void populateGemeentes() throws IOException {
133133
String sql = ImportGemeentesApplication.getGemeentesSql(bestuurlijkeGebiedenWfs, gemeentesTypename);
134+
this.jdbcClient.sql("delete from gemeente").update();
134135
this.jdbcClient.sql(sql).update();
135136
}
136137
}

src/main/java/org/tailormap/api/security/TailormapOidcUser.java

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@
77
package org.tailormap.api.security;
88

99
import java.io.Serial;
10+
import java.util.ArrayList;
1011
import java.util.Collection;
1112
import java.util.Collections;
12-
import java.util.List;
13+
import org.jspecify.annotations.NonNull;
1314
import org.springframework.security.core.GrantedAuthority;
1415
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
1516
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
@@ -19,7 +20,7 @@ public class TailormapOidcUser extends DefaultOidcUser implements TailormapUserD
1920
@Serial
2021
private static final long serialVersionUID = 1L;
2122

22-
private final Collection<TailormapAdditionalProperty> additionalGroupProperties;
23+
private final Collection<TailormapAdditionalProperty> additionalGroupProperties = new ArrayList<>();
2324

2425
private final String oidcRegistrationName;
2526

@@ -32,17 +33,14 @@ public TailormapOidcUser(
3233
Collection<TailormapAdditionalProperty> additionalGroupProperties) {
3334
super(authorities, idToken, userInfo, nameAttributeKey);
3435
this.oidcRegistrationName = oidcRegistrationName;
35-
this.additionalGroupProperties = Collections.unmodifiableCollection(additionalGroupProperties);
36-
}
37-
38-
@Override
39-
public Collection<TailormapAdditionalProperty> getAdditionalProperties() {
40-
return List.of();
36+
if (additionalGroupProperties != null) {
37+
this.additionalGroupProperties.addAll(additionalGroupProperties);
38+
}
4139
}
4240

4341
@Override
4442
public Collection<TailormapAdditionalProperty> getAdditionalGroupProperties() {
45-
return additionalGroupProperties;
43+
return Collections.unmodifiableCollection(additionalGroupProperties);
4644
}
4745

4846
@Override
@@ -51,7 +49,7 @@ public String getPassword() {
5149
}
5250

5351
@Override
54-
public String getUsername() {
52+
@NonNull public String getUsername() {
5553
return super.getName();
5654
}
5755

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright (C) 2025 B3Partners B.V.
3+
*
4+
* SPDX-License-Identifier: MIT
5+
*/
6+
package org.tailormap.api.security;
7+
8+
import com.fasterxml.jackson.annotation.JsonAutoDetect;
9+
import com.fasterxml.jackson.annotation.JsonCreator;
10+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
11+
import com.fasterxml.jackson.annotation.JsonProperty;
12+
import com.fasterxml.jackson.annotation.JsonTypeInfo;
13+
import java.util.Collection;
14+
import java.util.Map;
15+
import org.springframework.security.core.GrantedAuthority;
16+
17+
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
18+
@JsonAutoDetect(
19+
fieldVisibility = JsonAutoDetect.Visibility.ANY,
20+
getterVisibility = JsonAutoDetect.Visibility.NONE,
21+
isGetterVisibility = JsonAutoDetect.Visibility.NONE)
22+
@JsonIgnoreProperties(ignoreUnknown = true)
23+
public abstract class TailormapOidcUserMixin {
24+
25+
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
26+
public TailormapOidcUserMixin(
27+
@SuppressWarnings("unused") @JsonProperty("claims") Map<String, Object> claims,
28+
@SuppressWarnings("unused") @JsonProperty("authorities") Collection<? extends GrantedAuthority> authorities,
29+
@SuppressWarnings("unused") @JsonProperty("attributes") Map<String, Object> attributes,
30+
@SuppressWarnings("unused") @JsonProperty("nameAttributeKey") String nameAttributeKey,
31+
@SuppressWarnings("unused") @JsonProperty("oidcRegistrationName") String oidcRegistrationName,
32+
@SuppressWarnings("unused") @JsonProperty("additionalGroupProperties")
33+
Collection<TailormapAdditionalProperty> additionalGroupProperties) {
34+
// mixin constructor only for Jackson; no implementation
35+
}
36+
}

src/main/java/org/tailormap/api/security/TailormapUserDetails.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@
88

99
import java.io.Serializable;
1010
import java.util.Collection;
11+
import java.util.Collections;
1112
import java.util.stream.Stream;
1213
import org.springframework.security.core.userdetails.UserDetails;
1314

1415
public interface TailormapUserDetails extends Serializable, UserDetails {
1516

16-
Collection<TailormapAdditionalProperty> getAdditionalProperties();
17+
default Collection<TailormapAdditionalProperty> getAdditionalProperties() {
18+
return Collections.emptyList();
19+
}
1720

1821
Collection<TailormapAdditionalProperty> getAdditionalGroupProperties();
1922

0 commit comments

Comments
 (0)