Foreword
In the article about front-end and back-end separation of authorization code mode, Redis is used to save user authentication information. The value serializer configured in the Redis configuration file is the default Jdk serializer. Although this can also be used, When viewed in the Redis client, the code is garbled (it seems to be). If you switch to the value serializer provided by Jackson, it will fail during deserialization. This is unrealistic. After the project framework is set up or in In fact, these configurations in existing projects should have already been configured. It cannot be said that the original configuration has been changed for such a function.So I would like to say sorry to everyone for causing such a big problem because of my poor academic skills. The flaws remainto this day.
Problem analysis
The place used at that time was to store and retrieve SecurityContext
(authentication information) where the authentication information was initialized after successful login and initialization SecurityContextHolderFilter
. There was no problem when saving, but when fetching Sometimes, it will cause deserialization failure or type conversion exception because the classes in the framework do not provide a default constructor.
Jackson can only recognize java basic types. When encountering complex types, Jackson will first serialize it into a LinkedHashMap, and then try to force the conversion to the required category. In most cases, the forced conversion will fail. The exception information is as follows
java.lang.ClassCastException: class java.util.LinkedHashMap cannot be cast to class org.springframework.security.core.context.SecurityContext
In this case, you need to add a configuration, as follows
objectMapper.activateDefaultTyping( objectMapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
However, after adding this configuration and restarting, I found that there was still an exception when I tried again. However, this was because the classes in the framework did not provide a default constructor. The exception was as follows:
org.springframework.data.redis.serializer.SerializationException: Could not read JSON: Cannot construct instance of `org.springframework.security.authentication.UsernamePasswordAuthenticationToken` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator) at [Source: (byte[])"{"@class":"org.springframework.security.core.context.SecurityContextImpl","authentication":{"@class":\ "org.springframework.security.authentication.UsernamePasswordAuthenticationToken","authorities":["java.util.Collections$UnmodifiableRandomAccessList",[{"@class":"com.example.model.security .CustomGrantedAuthority","authority":"system"},{"@class":"com.example.model.security.CustomGrantedAuthority","authority":"app\ "},{"@class":"com.example.model.security.CustomGrantedAuthority","authority":"web"}]],"[truncated 893 bytes]; line: 1, column: 184] (through reference chain: org.springframework.security.core.context.SecurityContextImpl["authentication"])
The exception prompt problem is with the authentication
attribute of SecurityContextImpl
. Because the instance of this attribute is UsernamePasswordAuthenticationToken
, this class does not have a default constructor, so An error was reported directly during deserialization. At first, my idea was to write an implementation class, and then use a custom class to transfer it when accessing, but then I discovered Json Mixin
, I found this thing more convenient, so I implemented it and wrote a UsernamePasswordAuthenticationMixin
class to implement custom deserialization logic. But yesterday I suddenly discovered that this thing has actually been implemented in the framework? ?It’s very embarrassing. To add these things, just add the CoreJackson2Module
provided by the framework. The configuration is as follows:
//Add Jackson Mixin provided by Security objectMapper.registerModule(new CoreJackson2Module());
Solution
The RedisTemplate
configured in the Redis configuration file adds a value serializer, and the ObjectMapper
used by the value serializer adds the configurations mentioned above, including complex type mapping and security provided Json Mixin
, the complete Redis configuration class is as follows
package com.example.config; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; import org.springframework.security.jackson2.CoreJackson2Module; /** * Redis key serialization configuration class * * @author vains */ @Configuration @RequiredArgsConstructor public class RedisConfig {<!-- --> private final Jackson2ObjectMapperBuilder builder; /** * Used by default * * @param connectionFactory redis link factory * @return RedisTemplate */ @Bean public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {<!-- --> // String serializer StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); // Create ObjectMapper and add default configuration ObjectMapper objectMapper = builder.createXmlMapper(false).build(); //Serialize all fields objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // This item must be configured, otherwise if there are objects in the serialized object, the following error will be reported: // java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to XXX objectMapper.activateDefaultTyping( objectMapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); //Add Jackson Mixin provided by Security objectMapper.registerModule(new CoreJackson2Module()); //Serializer for serializing values when storing in redis Jackson2JsonRedisSerializer<Object> valueSerializer = new Jackson2JsonRedisSerializer<>(objectMapper, Object.class); RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>(); // Set value serialization redisTemplate.setValueSerializer(valueSerializer); //Set the serializer for hash format data values redisTemplate.setHashValueSerializer(valueSerializer); //The default Key serializer is: JdkSerializationRedisSerializer redisTemplate.setKeySerializer(stringRedisSerializer); // Set up string serializer redisTemplate.setStringSerializer(stringRedisSerializer); //Set the serializer for the key of the hash structure redisTemplate.setHashKeySerializer(stringRedisSerializer); // Set up the connection factory redisTemplate.setConnectionFactory(connectionFactory); return redisTemplate; } /** * Used when operating hash * * @param connectionFactory redis link factory * @return RedisTemplate */ @Bean public RedisTemplate<Object, Object> redisHashTemplate(RedisConnectionFactory connectionFactory) {<!-- --> return redisTemplate(connectionFactory); } }
Extended description
From the above configuration, we can see that Spring
has good deserialization support for classes that do not have default constructors in third-party frameworks. If you encounter this situation when integrating other frameworks, you can follow the example of Mixin
class to support deserialization. Of course, you can also find out if there is a similar one in the framework. Jackson2Module
class; when you encapsulate a starter
, you can also provide the Jackson2Module
class to map the class, but this is based on personal preference. High degree of packaging freedom.
Of course, if you encounter other classes that fail to deserialize when using Security
, you can look for other Jackson2Module
classes in the framework. If they are provided, then There is no need to encapsulate it yourself, just add a Module
directly to ObjectMapper
.