Spring Authorization Server Optimization: Add Jackson Mixin to the Redis value serializer to solve the problem of Redis deserialization failure

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 provided by the code>Security framework implements its own 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.