设置 Sping boot 4 上的 Redis 缓存 (spring-boot-starter-cache)

在 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
// Java 版本

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
// Kotlin 版本

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)
}
}
}