Spring SessionでRedisにセッション情報を格納する際に、Jacksonを使ってシリアライズをできるみたいなので試してみました。*1
ライブラリのバージョンなど
build.gradleを参照ください。
Sessionの格納先をRedisにする
application.propertiesに以下を追加します。
spring.session.store-type=redis
Jackson2JsonRedisSerializerをBean定義する
RedisHttpSessionConfigurationを見ると、こんな実装がるのでBean名にspringSessionDefaultRedisSerializerを指定してJacksonのシリアライザをBean定義すれば良さそうですね。
@Autowired(required = false) @Qualifier("springSessionDefaultRedisSerializer") public void setDefaultRedisSerializer( RedisSerializer<Object> defaultRedisSerializer) { this.defaultRedisSerializer = defaultRedisSerializer; }
実際のBean定義です。Jacksonでシリアライザを行うJackson2JsonRedisSerializerを返します。
@Qualifierアノテーションを使ってBean名にspringSessionDefaultRedisSerializerを指定します。
JacksonのObjectMapperには、セッション情報をシリアライズする際に必要となるためSecurityJackson2Modulesを使って、必要なModuleを登録してあげます。
@Bean @Qualifier("springSessionDefaultRedisSerializer") fun redisSerializer(): RedisSerializer<Any> { return Jackson2JsonRedisSerializer(Any::class.java).apply { val objectMapper = ObjectMapper() objectMapper.registerModules(SecurityJackson2Modules.getModules(this.javaClass.classLoader)) setObjectMapper(objectMapper) } }
ログインを行いRedisの中身を確認する
ログインを行ってセッションの中身を確認してみると、デフォルトで使われるUserクラスの情報がRedisに登録されていることが確認できます。
127.0.0.1:6379> hgetall spring:session:sessions:6ff0be42-853f-4184-a874-1e4881a822f7 creationTime 1547242900218 sessionAttr:SPRING_SECURITY_SAVED_REQUEST sessionAttr:org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.CSRF_TOKEN maxInactiveInterval 1800 lastAccessedTime 1547242908403 sessionAttr:SPRING_SECURITY_LAST_EXCEPTION sessionAttr:SPRING_SECURITY_CONTEXT {"@class":"org.springframework.security.core.context.SecurityContextImpl","authentication":{"@class":"org.springframework.security.authentication.UsernamePasswordAuthenticationToken","authorities":["java.util.Collections$UnmodifiableRandomAccessList",[{"@class":"org.springframework.security.core.authority.SimpleGrantedAuthority","authority":"ROLE_USER"}]],"details":{"@class":"org.springframework.security.web.authentication.WebAuthenticationDetails","remoteAddress":"0:0:0:0:0:0:0:1","sessionId":"272fd950-13c5-4d79-a3dc-54010be92ad7"},"authenticated":true,"principal":{"@class":"org.springframework.security.core.userdetails.User","password":null,"username":"test","authorities":["java.util.Collections$UnmodifiableSet",[{"@class":"org.springframework.security.core.authority.SimpleGrantedAuthority","authority":"ROLE_USER"}]],"accountNonExpired":true,"accountNonLocked":true,"credentialsNonExpired":true,"enabled":true},"credentials":null}}
UserDetailsの実装クラスを変えてみる
デフォルトで使われるorg.springframework.security.core.userdetails.Userではなく、独自のUserDetails実装を使った場合の動きを確認してみます。
UserDetails実装はSampleUserとしてデフォルトの情報以外に、ログインしたユーザの名前を保持するようにしています。
data class SampleUser(private val username: String, private val password: String, private val authorities: List<GrantedAuthority>, val loginUserName: String) : UserDetails { override fun getAuthorities(): List<GrantedAuthority> = authorities override fun isEnabled(): Boolean = true override fun getUsername(): String = username override fun isCredentialsNonExpired(): Boolean = true override fun getPassword(): String = password override fun isAccountNonExpired(): Boolean = true override fun isAccountNonLocked(): Boolean = true }
独自Userでも動作するか確認してみます
残念ながらエラーになってしまいました。デシリアライズ処理ができなかったようです。
org.springframework.data.redis.serializer.SerializationException: Could not read JSON: The class with siosio.springsessionredisexample.config.SampleUser and name of siosio.springsessionredisexample.config.SampleUser is not whitelisted. If you believe this class is safe to deserialize, please provide an explicit mapping using Jackson annotations or by providing a Mixin. If the serialization is only done by a trusted source, you can also enable default typing. See https://github.com/spring-projects/spring-security/issues/4370 for details (through reference chain: org.springframework.security.core.context.SecurityContextImpl["authentication"]); nested exception is com.fasterxml.jackson.databind.JsonMappingException: The class with siosio.springsessionredisexample.config.SampleUser and name of siosio.springsessionredisexample.config.SampleUser is not whitelisted. If you believe this class is safe to deserialize, please provide an explicit mapping using Jackson annotations or by providing a Mixin. If the serialization is only done by a trusted source, you can also enable default typing. See https://github.com/spring-projects/spring-security/issues/4370 for details (through reference chain: org.springframework.security.core.context.SecurityContextImpl["authentication"]) at org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer.deserialize(Jackson2JsonRedisSerializer.java:75) ~[spring-data-redis-2.1.3.RELEASE.jar:2.1.3.RELEASE] at org.springframework.data.redis.core.AbstractOperations.deserializeHashValue(AbstractOperations.java:354) ~[spring-data-redis-2.1.3.RELEASE.jar:2.1.3.RELEASE] at org.springframework.data.redis.core.AbstractOperations.deserializeHashMap(AbstractOperations.java:298) ~[spring-data-redis-2.1.3.RELEASE.jar:2.1.3.RELEASE] at org.springframework.data.redis.core.DefaultHashOperations.entries(DefaultHashOperations.java:247) ~[spring-data-redis-2.1.3.RELEASE.jar:2.1.3.RELEASE] at org.springframework.data.redis.core.DefaultBoundHashOperations.entries(DefaultBoundHashOperations.java:183) ~[spring-data-redis-2.1.3.RELEASE.jar:2.1.3.RELEASE] at org.springframework.session.data.redis.RedisOperationsSessionRepository.getSession(RedisOperationsSessionRepository.java:466) ~[spring-session-data-redis-2.1.2.RELEASE.jar:2.1.2.RELEASE] at org.springframework.session.data.redis.RedisOperationsSessionRepository.findById(RedisOperationsSessionRepository.java:435) ~[spring-session-data-redis-2.1.2.RELEASE.jar:2.1.2.RELEASE]
なお、Redisの内容を確認してみると、想定通りSampleUserの情報が格納されているのでシリアライズは問題なさそうですね。
sessionAttr:SPRING_SECURITY_CONTEXT {"@class":"org.springframework.security.core.context.SecurityContextImpl","authentication":{"@class":"org.springframework.security.authentication.UsernamePasswordAuthenticationToken","authorities":["java.util.Collections$UnmodifiableRandomAccessList",[{"@class":"org.springframework.security.core.authority.SimpleGrantedAuthority","authority":"ROLE_USER"}]],"details":{"@class":"org.springframework.security.web.authentication.WebAuthenticationDetails","remoteAddress":"0:0:0:0:0:0:0:1","sessionId":"0f181292-9a16-4622-b820-83adcc8ae33c"},"authenticated":true,"principal":{"@class":"siosio.springsessionredisexample.config.SampleUser","username":"test","password":"$2a$10$AXwQIZKK8yaNk2tFN1JwaOCxcn7DYuCwK5d6wvY.tX.ZGQUWeA2sy","authorities":["java.util.Collections$SingletonList",[{"@class":"org.springframework.security.core.authority.SimpleGrantedAuthority","authority":"ROLE_USER"}]],"loginUserName":"テストユーザ","enabled":true,"accounpired":true,"credentialsNonExpired":true,"accountNonLocked":true},"credentials":null}
デシリアライズ処理がうまく動くようMixInを追加する
デフォルトのUserクラス用のMixIn(org.springframework.security.jackson2.UserMixin)を参考にしてデシリアライズ処理を行うクラスなどを追加します。
SampleUserMixinでは、デシリアライズを行うクラスの指定や、フィールドをシリアライズ対象にするなどの設定をしています。
デシリアライズを行うSampleUserDeserializerは、SampleUserを生成して返します。
data class SampleUser(private val username: String, private val password: String, private val authorities: List<GrantedAuthority>, val loginUserName: String) : UserDetails { override fun getAuthorities(): List<GrantedAuthority> = authorities override fun isEnabled(): Boolean = true override fun getUsername(): String = username override fun isCredentialsNonExpired(): Boolean = true override fun getPassword(): String = password override fun isAccountNonExpired(): Boolean = true override fun isAccountNonLocked(): Boolean = true } @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY) @JsonDeserialize(using = SampleUserDeserializer::class) @JsonAutoDetect( fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE, creatorVisibility = JsonAutoDetect.Visibility.NONE) @JsonIgnoreProperties(ignoreUnknown = true) @JsonSubTypes internal abstract class SampleUserMixin class SampleUserDeserializer : JsonDeserializer<SampleUser>() { override fun deserialize(jp: JsonParser, ctxt: DeserializationContext): SampleUser { val mapper = jp.codec as ObjectMapper val jsonNode: JsonNode = mapper.readTree(jp) val authorities = mapper.convertValue<List<SimpleGrantedAuthority>>(jsonNode.get("authorities")) val password = jsonNode.readText("password") return SampleUser( jsonNode.readText("username"), password, authorities.toList(), jsonNode.readText("loginUserName") ) } fun JsonNode.readText(field: String, defaultValue: String = ""): String { return when { has(field) -> get(field).asText(defaultValue) else -> defaultValue } } }
最後に、JacksonのシリアライザのBean生成を行っているところで、作成したMixInをJacksonに登録します。
@Bean @Qualifier("springSessionDefaultRedisSerializer") fun redisSerializer(): RedisSerializer<Any> { return Jackson2JsonRedisSerializer(Any::class.java).apply { val objectMapper = ObjectMapper() objectMapper.registerModules(SecurityJackson2Modules.getModules(this.javaClass.classLoader)) objectMapper.addMixIn(SampleUser::class.java, SampleUserMixin::class.java) setObjectMapper(objectMapper) } }