在 Spring Boot 4 里面想要使用 Redis 缓存,除了添加依赖和设置 spring.cache.type=redis 以外,还需要配置缓存的序列化器。
Spring Cache 默认使用的是 JdkSerializationRedisSerializer,但这个 Serializer 需要实现 java.io.Serializable 接口,但我写 Kotlin 的话用 data class 一般都不加这个了。
而且即使在纯 Java 当中,Java 默认序列化也不是那么的好用。大多数时候在 Cache 里存的都只是一个 DTO 类,只要能把数据值存上就足够了,所以我一般都会把序列化换成 Jackson 的,序列化成 Json 传进去。
恰好 Spring Boot 4 版本已经完全升级到 Jackson 3 了,有很多文档说明都还是 Jackson 2 的说法。下面是我总结出来的一个比较简单的配置方式。
实现说明
创建配置类
首先创建一个带有 @Configuration 的配置类。
我们主要需要扩展的是 RedisCacheManagerBuilderCustomizer,在 Spring Boot 自动配置的时候会查找我们定义的这个 Bean 来添加自定义配置。
1 2 3 4 5 6 7 8 9 10
| @Configuration @EnableCaching public class CacheConfiguration { @Bean public RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer() { return builder -> { }; } }
|
序列化配置
序列化我们采用 GenericJacksonJsonRedisSerializer,这里说明一下初始化的配置。
Spring Cache 在取出缓存值的时候会自动反序列化,所以它需要知道缓存值的类型。而一个普通的 Json 字符串它就不知道怎么反序列化了,所以我们需要在存入 Redis 的时候加入一些类型信息,比如生成类似下面的 Json,增加一个 @class 属性用于指定类型。
1 2 3 4
| { "@class": "com.example.demo.DemoDto", "name": "Helloworld" }
|
所以我们需要自定义 GenericJacksonJsonRedisSerializer,让它加入类型信息。
1 2 3
| GenericJacksonJsonRedisSerializer serializer = GenericJacksonJsonRedisSerializer.builder() .enableUnsafeDefaultTyping() .build();
|
enableUnsafeDefaultTyping() 就是用于开启默认的类型配置。
补充说明
从名称上来看这个配置带有一个 Unsafe 的名字,具体 Unsafe 在哪里呢?
简单来说就是因为反序列化成什么类型完全是由 @class 这个属性决定的,假设你的 Redis 中保存的内容可以被别人修改,比如和其他项目共用一个 Redis,相当于你对 @class 不是完全可控的,它可以反序列化成你代码中的任意一个类型。
举一个简单的例子。有一个存储信用卡号的敏感信息类,比如 CreditCard,为了不返回明文信用卡号,创建了一个返回给用户前端的 DTO 类CreditCardDTO
在CreditCardDTO里面,手动实现了 getCardNumber() 方法,返回的是一个加密后的字符串,比如 **** **** **** 1234,这样API返回给用户的就是隐藏的卡号。
但如果通过某种方式,修改了 Redis 里面的缓存内容,本来存进去的是 CreditCardDTO,但是 @class 的值却修改成了 CreditCard ,又直接把这个类通过API返回给了用户,导致卡号泄露了。
实际如果要针对这种漏洞进行攻击是比较难的,需要有 Redis 写入权限,并且清楚知道你的代码结构如何处理缓存结果才能利用。
所以如果是内网部署的 Redis ,没那么容易被修改,直接用 enableUnsafeDefaultTyping() 也是可以的。
安全一点的方式是使用 enableDefaultTyping(typeValidator),并且限制可以反序列化的类型。比如把 Redis 缓存全部放到一个 cache 的package里面,只允许反序列化这些类。
组合配置
修改 RedisCacheConfiguration,把我们创建好的 serilizer 设置到配置里面。
1 2 3 4 5 6 7 8
| GenericJacksonJsonRedisSerializer serializer = GenericJacksonJsonRedisSerializer.builder() .enableUnsafeDefaultTyping() .build();
RedisCacheConfiguration config = builder.cacheDefaults() .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer));
builder.cacheDefaults(config);
|
注:这里使用 builder.cacheDefaults() 先从 builder 获取默认配置,然后修改后再设置回 builder 里面。
因为我希望能够保留 properties 里面的配置,比如下面这些。如果创建一个新的 RedisCacheConfiguration,就要自己实现读取 properties 配置了。
1 2
| spring.cache.redis.key-prefix=spring-cache: spring.cache.redis.time-to-live=5m
|
省流
完整配置如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
import org.springframework.boot.cache.autoconfigure.RedisCacheManagerBuilderCustomizer; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.serializer.GenericJacksonJsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializationContext;
@Configuration @EnableCaching public class CacheConfiguration { @Bean public RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer() { return builder -> { GenericJacksonJsonRedisSerializer serializer = GenericJacksonJsonRedisSerializer.builder() .enableUnsafeDefaultTyping() .build();
RedisCacheConfiguration config = builder.cacheDefaults() .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer));
builder.cacheDefaults(config); }; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
import org.springframework.boot.cache.autoconfigure.RedisCacheManagerBuilderCustomizer import org.springframework.cache.annotation.EnableCaching import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.data.redis.serializer.GenericJacksonJsonRedisSerializer import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair.fromSerializer
@Configuration @EnableCaching class CacheConfig {
@Bean fun redisCacheManagerBuilderCustomizer(): RedisCacheManagerBuilderCustomizer { return RedisCacheManagerBuilderCustomizer { builder -> val serializer = GenericJacksonJsonRedisSerializer .builder() .enableUnsafeDefaultTyping() .build() val config = builder.cacheDefaults() .serializeValuesWith(fromSerializer(serializer)) builder.cacheDefaults(config) } } }
|