1
2
3
4
5
6
7 package org.tailormap.api.configuration;
8
9 import com.fasterxml.jackson.annotation.JsonTypeInfo;
10 import com.fasterxml.jackson.core.StreamReadFeature;
11 import com.fasterxml.jackson.databind.ObjectMapper;
12 import com.fasterxml.jackson.databind.SerializationFeature;
13 import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
14 import java.io.IOException;
15 import java.lang.invoke.MethodHandles;
16 import java.nio.charset.StandardCharsets;
17 import org.jspecify.annotations.NonNull;
18 import org.slf4j.Logger;
19 import org.slf4j.LoggerFactory;
20 import org.springframework.beans.factory.BeanClassLoaderAware;
21 import org.springframework.context.annotation.Bean;
22 import org.springframework.context.annotation.Configuration;
23 import org.springframework.core.convert.ConversionService;
24 import org.springframework.core.convert.support.GenericConversionService;
25 import org.springframework.security.jackson2.SecurityJackson2Modules;
26 import org.springframework.session.config.SessionRepositoryCustomizer;
27 import org.springframework.session.jdbc.JdbcIndexedSessionRepository;
28
29 @Configuration(proxyBeanMethods = false)
30 public class JdbcSessionConfiguration implements BeanClassLoaderAware {
31 private static final Logger logger =
32 LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
33
34 private static final String CREATE_SESSION_ATTRIBUTE_QUERY = """
35 INSERT INTO %TABLE_NAME%_ATTRIBUTES (SESSION_PRIMARY_ID, ATTRIBUTE_NAME, ATTRIBUTE_BYTES)
36 VALUES (?, ?, convert_from(?, 'UTF8')::jsonb)
37 """;
38
39 private static final String UPDATE_SESSION_ATTRIBUTE_QUERY = """
40 UPDATE %TABLE_NAME%_ATTRIBUTES
41 SET ATTRIBUTE_BYTES = encode(?, 'escape')::jsonb
42 WHERE SESSION_PRIMARY_ID = ?
43 AND ATTRIBUTE_NAME = ?
44 """;
45
46 private ClassLoader classLoader;
47
48 @Bean
49 SessionRepositoryCustomizer<JdbcIndexedSessionRepository> customizer() {
50 return (sessionRepository) -> {
51 sessionRepository.setCreateSessionAttributeQuery(CREATE_SESSION_ATTRIBUTE_QUERY);
52 sessionRepository.setUpdateSessionAttributeQuery(UPDATE_SESSION_ATTRIBUTE_QUERY);
53 };
54 }
55
56 @Bean("springSessionConversionService")
57 public ConversionService springSessionConversionService(ObjectMapper objectMapper) {
58
59 BasicPolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder()
60 .allowIfSubType("org.tailormap.api.security.")
61 .allowIfSubType("org.springframework.security.")
62 .allowIfSubType("java.util.")
63 .allowIfSubType(java.lang.Number.class)
64 .allowIfSubType("java.time.")
65 .allowIfBaseType(Object.class)
66 .build();
67
68 ObjectMapper copy = objectMapper
69 .copy()
70 .configure(
71 StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION.mappedFeature(),
72 (logger.isDebugEnabled() || logger.isTraceEnabled()))
73 .configure(SerializationFeature.INDENT_OUTPUT, (logger.isDebugEnabled() || logger.isTraceEnabled()));
74
75
76
77 copy.addMixIn(
78 org.tailormap.api.security.TailormapUserDetailsImpl.class,
79 org.tailormap.api.security.TailormapUserDetailsImplMixin.class);
80
81 copy.addMixIn(
82 org.tailormap.api.security.TailormapOidcUser.class,
83 org.tailormap.api.security.TailormapOidcUserMixin.class);
84
85 copy.registerModules(SecurityJackson2Modules.getModules(this.classLoader))
86 .activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
87
88 final GenericConversionService converter = new GenericConversionService();
89
90 converter.addConverter(Object.class, byte[].class, source -> {
91 try {
92 logger.debug("Serializing Spring Session: {}", source);
93 return copy.writerFor(Object.class).writeValueAsBytes(source);
94 } catch (IOException e) {
95 logger.error("Error serializing Spring Session object: {}", source, e);
96 throw new RuntimeException("Unable to serialize Spring Session.", e);
97 }
98 });
99
100 converter.addConverter(byte[].class, Object.class, source -> {
101 try {
102 logger.debug(
103 "Deserializing Spring Session from bytes, length: {} ({})",
104 source.length,
105 new String(source, StandardCharsets.UTF_8));
106 return copy.readValue(source, Object.class);
107 } catch (IOException e) {
108 String preview;
109 try {
110 String content = new String(source, StandardCharsets.UTF_8);
111 int maxLength = 256;
112 preview = content.length() > maxLength ? content.substring(0, maxLength) + "..." : content;
113 } catch (Exception ex) {
114 preview = "<unavailable>";
115 }
116 logger.error(
117 "Error deserializing Spring Session from bytes, length: {}, preview: {}",
118 source.length,
119 preview,
120 e);
121 throw new RuntimeException("Unable to deserialize Spring Session.", e);
122 }
123 });
124
125 return converter;
126 }
127
128 @Override
129 public void setBeanClassLoader(@NonNull ClassLoader classLoader) {
130 this.classLoader = classLoader;
131 }
132 }