66
77package 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 ;
1112import 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 ;
1217import org .springframework .beans .factory .annotation .Qualifier ;
1318import org .springframework .beans .factory .annotation .Value ;
1419import org .springframework .boot .jdbc .DataSourceBuilder ;
1722import org .springframework .context .annotation .Profile ;
1823import org .springframework .core .convert .ConversionService ;
1924import 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 ;
2325import org .springframework .jdbc .core .simple .JdbcClient ;
2426import 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 ;
2530import org .springframework .session .jdbc .config .annotation .SpringSessionDataSource ;
2631import org .springframework .session .jdbc .config .annotation .web .http .EnableJdbcHttpSession ;
2732import 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}
0 commit comments