diff --git a/daxpay-platform/daxpay-platform-capability/capability-cache/README.md b/daxpay-platform/daxpay-platform-capability/capability-cache/README.md
index 90bc89144..4b561b778 100644
--- a/daxpay-platform/daxpay-platform-capability/capability-cache/README.md
+++ b/daxpay-platform/daxpay-platform-capability/capability-cache/README.md
@@ -39,6 +39,10 @@ daxpay:
maximum-size: 10000 # L1 本地缓存最大容量
l2:
default-ttl: 1800 # L2 Redis 缓存默认过期时间(秒)
+ secure-prefix: "secure:" # 敏感缓存名前缀,匹配的 L2 value 整包 AES-GCM 加密
+ # secure-names: [] # 额外精确敏感 cacheName 列表(可选)
+ # 敏感 L2 加密复用 DB 字段加密密钥(全进程同一 SecureAesGcmEncryptor)
+ # platform.config.encrypt.enable / keys
```
## 架构设计
@@ -178,6 +182,8 @@ public void clearUserCache() {
@Cacheable(value = "merchant:config", key = "#mchNo")
```
+**含密钥/证书等敏感数据的缓存名必须以 `secure:` 开头**(见下文「敏感缓存」)。
+
### 7. 如何处理缓存穿透?
当前实现不缓存 null 值(`disableCachingNullValues()`),如需防止缓存穿透:
@@ -208,3 +214,57 @@ public void printCacheStats(String cacheName) {
log.info("命中次数: {}, 未命中次数: {}", stats.hitCount(), stats.missCount());
}
```
+
+## 敏感缓存(secure:)
+
+通道密钥、商户对接密钥等敏感对象若使用 Spring Cache,**必须**使用 `secure:` 前缀的 cacheName,基础设施会自动对 L2 Redis value 做**整包 AES-GCM 加密**。
+
+### 与 DB 字段加密的区别
+
+| 层 | 粒度 | 说明 |
+|----|------|------|
+| DB(DataEncryptTypeHandler) | **字段级** | 仅 `privateKey` / `apiKey` 等列密文,其它列明文 |
+| Redis L2(secure:) | **整包** | 整个缓存对象 JSON 一次加密,Redis 中不可见任何字段明文 |
+| L1 本地 | 明文对象 | 进程内,与普通缓存一致 |
+
+密钥管理复用 `daxpay.platform.config.encrypt`(与 DB 同一 `SecureAesGcmEncryptor` Bean)。
+
+### 业务写法(与普通缓存相同)
+
+```java
+// 读:默认 CacheManager,仅 cacheName 用 secure: 前缀
+@Cacheable(value = "secure:channel-key:adapay-direct",
+ key = "#channelMchNo + ':' + #sandbox")
+public AdapayDirectKeyConfig getForPay(String channelMchNo, boolean sandbox) {
+ return manager.find(...).orElseThrow(...);
+}
+
+// 写:必须同名同 key 失效
+@CacheEvict(value = "secure:channel-key:adapay-direct",
+ key = "#param.channelMchNo + ':' + #param.sandbox")
+public void saveConfig(AdapayDirectKeyConfigParam param) {
+ // ...
+}
+```
+
+命名建议:
+
+```
+secure:channel-key:wechat-isv
+secure:channel-key:adapay-direct
+secure:merchant-credential
+secure:platform-encrypt-config
+```
+
+### 数据加密未启用时
+
+若 `daxpay.platform.config.encrypt.enable=false`:
+
+- `secure:*` **禁止写 Redis**(L1-only 降级),避免明文密钥落盘
+- 启动日志会 warn 提示
+
+### 禁止事项
+
+- 禁止对含密钥的实体使用非 `secure:` 的 cacheName(会明文进 Redis)
+- 禁止缓存完整 `*SdkCredential` 到普通 cacheName
+- 不需要也不应指定第二个 `cacheManager`:统一用默认二级缓存即可
diff --git a/daxpay-platform/daxpay-platform-capability/capability-cache/pom.xml b/daxpay-platform/daxpay-platform-capability/capability-cache/pom.xml
index 8d932f77a..4ca9fcb48 100644
--- a/daxpay-platform/daxpay-platform-capability/capability-cache/pom.xml
+++ b/daxpay-platform/daxpay-platform-capability/capability-cache/pom.xml
@@ -45,5 +45,10 @@
common-artemis
${project.version}
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
diff --git a/daxpay-platform/daxpay-platform-capability/capability-cache/src/main/java/cn/daxpay/open/platform/capability/cache/CacheAutoConfiguration.java b/daxpay-platform/daxpay-platform-capability/capability-cache/src/main/java/cn/daxpay/open/platform/capability/cache/CacheAutoConfiguration.java
index b088c2b33..f1a46ef55 100644
--- a/daxpay-platform/daxpay-platform-capability/capability-cache/src/main/java/cn/daxpay/open/platform/capability/cache/CacheAutoConfiguration.java
+++ b/daxpay-platform/daxpay-platform-capability/capability-cache/src/main/java/cn/daxpay/open/platform/capability/cache/CacheAutoConfiguration.java
@@ -1,14 +1,16 @@
package cn.daxpay.open.platform.capability.cache;
+import cn.daxpay.open.platform.common.config.ConfigAutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.context.annotation.ComponentScan;
/// # 缓存配置
///
+/// 依赖 [ConfigAutoConfiguration] 先注册 SecureAesGcmEncryptor,供敏感缓存 L2 加密使用。
@ComponentScan
@ConfigurationPropertiesScan
-@AutoConfiguration
+@AutoConfiguration(after = ConfigAutoConfiguration.class)
public class CacheAutoConfiguration {
}
diff --git a/daxpay-platform/daxpay-platform-capability/capability-cache/src/main/java/cn/daxpay/open/platform/capability/cache/configuration/CachingConfiguration.java b/daxpay-platform/daxpay-platform-capability/capability-cache/src/main/java/cn/daxpay/open/platform/capability/cache/configuration/CachingConfiguration.java
index 9c787f11c..16779523d 100644
--- a/daxpay-platform/daxpay-platform-capability/capability-cache/src/main/java/cn/daxpay/open/platform/capability/cache/configuration/CachingConfiguration.java
+++ b/daxpay-platform/daxpay-platform-capability/capability-cache/src/main/java/cn/daxpay/open/platform/capability/cache/configuration/CachingConfiguration.java
@@ -4,9 +4,14 @@ import cn.daxpay.open.platform.capability.cache.core.MultiLevelCacheManager;
import cn.daxpay.open.platform.capability.cache.core.DaxpayRedisCacheManager;
import cn.daxpay.open.platform.capability.cache.core.LocalCacheRegistry;
import cn.daxpay.open.platform.capability.cache.notify.publisher.CacheInvalidationPublisher;
+import cn.daxpay.open.platform.capability.cache.secure.EncryptingRedisSerializer;
+import cn.daxpay.open.platform.capability.cache.secure.SecureCacheNameMatcher;
+import cn.daxpay.open.platform.common.config.encrypt.SecureAesGcmEncryptor;
import cn.daxpay.open.platform.common.config.properties.PlatformCommonProperties;
import cn.daxpay.open.platform.common.redis.serializer.JacksonRedisSerializer;
import cn.daxpay.open.platform.common.artemis.service.ArtemisTemplateService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@@ -35,6 +40,8 @@ import java.util.stream.Collectors;
/// - L1 本地缓存始终启用,作为性能加速层
/// - L2 Redis 作为共享缓存主层
/// - 缓存失效通知通过 Artemis 广播,始终启用
+/// - 名称匹配 secure: 的缓存:L2 整包 AES-GCM 加密;加密未启用时仅 L1
+@Slf4j
@Configuration
@EnableCaching
@EnableConfigurationProperties(PlatformCommonProperties.class)
@@ -65,16 +72,44 @@ public class CachingConfiguration implements CachingConfigurer {
};
}
+ /// 敏感缓存名匹配器
+ @Bean
+ @ConditionalOnMissingBean
+ public SecureCacheNameMatcher secureCacheNameMatcher() {
+ var cache = platformCommonProperties.getCache();
+ return new SecureCacheNameMatcher(cache.getSecurePrefix(), cache.getSecureNames());
+ }
+
/// Redis 缓存管理器(L2)
///
/// 作为二级缓存的 L2 层,负责跨节点数据共享
+ ///
+ /// 普通 cacheName 使用明文 JSON;敏感 cacheName 在 encryptor 可用时使用整包加密序列化。
@Bean
@ConditionalOnMissingBean(DaxpayRedisCacheManager.class)
- public DaxpayRedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
+ public DaxpayRedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory,
+ SecureCacheNameMatcher secureCacheNameMatcher,
+ ObjectProvider encryptorProvider) {
var l2Config = platformCommonProperties.getCache().getL2();
+ Duration ttl = Duration.ofSeconds(l2Config.getDefaultTtl());
+ RedisCacheConfiguration plainConfig = this.plainValueConfig(ttl);
+
+ SecureAesGcmEncryptor encryptor = encryptorProvider.getIfAvailable();
+ RedisCacheConfiguration secureConfig = null;
+ if (encryptor != null) {
+ EncryptingRedisSerializer encryptingSerializer = new EncryptingRedisSerializer(encryptor);
+ secureConfig = this.secureValueConfig(ttl, encryptingSerializer);
+ log.info("敏感缓存 L2 整包加密已启用,前缀: {}", secureCacheNameMatcher.getSecurePrefix());
+ } else {
+ log.warn("未启用数据加密:名称匹配 {} 的敏感缓存将仅使用 L1,不写 Redis",
+ secureCacheNameMatcher.getSecurePrefix());
+ }
+
return new DaxpayRedisCacheManager(
RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory),
- this.getRedisCacheConfigurationWithTtl(Duration.ofSeconds(l2Config.getDefaultTtl())));
+ plainConfig,
+ secureConfig,
+ secureCacheNameMatcher);
}
/// 本地缓存注册表
@@ -106,16 +141,21 @@ public class CachingConfiguration implements CachingConfigurer {
@Primary
public CacheManager cacheManager(DaxpayRedisCacheManager redisCacheManager,
LocalCacheRegistry localCacheRegistry,
- CacheInvalidationPublisher publisher) {
+ CacheInvalidationPublisher publisher,
+ SecureCacheNameMatcher secureCacheNameMatcher,
+ ObjectProvider encryptorProvider) {
+ boolean secureL2Enabled = encryptorProvider.getIfAvailable() != null;
return new MultiLevelCacheManager(
redisCacheManager,
localCacheRegistry,
- publisher
+ publisher,
+ secureCacheNameMatcher,
+ secureL2Enabled
);
}
- /// 缓存管理器策略过期时间配置
- private RedisCacheConfiguration getRedisCacheConfigurationWithTtl(Duration duration) {
+ /// 缓存管理器策略过期时间配置(普通缓存:明文 JSON value)
+ private RedisCacheConfiguration plainValueConfig(Duration duration) {
// redis缓存配置
return RedisCacheConfiguration.defaultCacheConfig()
// 设置key为String
@@ -130,5 +170,20 @@ public class CachingConfiguration implements CachingConfigurer {
.entryTtl(duration);
}
-}
+ /// 缓存管理器策略过期时间配置(敏感缓存:整包 AES-GCM 加密 value)
+ private RedisCacheConfiguration secureValueConfig(Duration duration, EncryptingRedisSerializer encryptingSerializer) {
+ // redis缓存配置
+ return RedisCacheConfiguration.defaultCacheConfig()
+ // 设置key为String
+ .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
+ // 设置value 序列化方式为整包加密(JSON + AES-GCM)
+ .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(encryptingSerializer))
+ // 不缓存null
+ .disableCachingNullValues()
+ // 覆盖默认的构造key,否则会多出一个冒号
+ .computePrefixWith(name -> name + ":")
+ // 过期时间
+ .entryTtl(duration);
+ }
+}
diff --git a/daxpay-platform/daxpay-platform-capability/capability-cache/src/main/java/cn/daxpay/open/platform/capability/cache/core/DaxpayRedisCacheManager.java b/daxpay-platform/daxpay-platform-capability/capability-cache/src/main/java/cn/daxpay/open/platform/capability/cache/core/DaxpayRedisCacheManager.java
index d76ee7de8..94d72546d 100644
--- a/daxpay-platform/daxpay-platform-capability/capability-cache/src/main/java/cn/daxpay/open/platform/capability/cache/core/DaxpayRedisCacheManager.java
+++ b/daxpay-platform/daxpay-platform-capability/capability-cache/src/main/java/cn/daxpay/open/platform/capability/cache/core/DaxpayRedisCacheManager.java
@@ -1,52 +1,59 @@
package cn.daxpay.open.platform.capability.cache.core;
+import cn.daxpay.open.platform.capability.cache.secure.SecureCacheNameMatcher;
import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
-import java.util.Map;
-
/// # 自定义 Redis 缓存管理器
///
/// 作为二级缓存的 L2 层,负责跨节点数据共享
+///
+/// 按 cacheName 选择序列化策略:
+/// - 敏感名(SecureCacheNameMatcher 命中)且已配置 secureConfig:整包 AES-GCM 加密
+/// - 其它:明文 JSON
public class DaxpayRedisCacheManager extends RedisCacheManager {
private final RedisCacheWriter cacheWriter;
+ private final RedisCacheConfiguration defaultConfig;
+ private final RedisCacheConfiguration secureConfig;
+ private final SecureCacheNameMatcher secureMatcher;
- public DaxpayRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
+ /// @param cacheWriter Redis 写入器
+ /// @param defaultCacheConfiguration 普通缓存配置(明文 JSON)
+ /// @param secureCacheConfiguration 敏感缓存配置(整包加密);encrypt 未启用时可为 null
+ /// @param secureMatcher 敏感缓存名匹配器
+ public DaxpayRedisCacheManager(RedisCacheWriter cacheWriter,
+ RedisCacheConfiguration defaultCacheConfiguration,
+ RedisCacheConfiguration secureCacheConfiguration,
+ SecureCacheNameMatcher secureMatcher) {
super(cacheWriter, defaultCacheConfiguration);
this.cacheWriter = cacheWriter;
- }
-
- public DaxpayRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration, String... initialCacheNames) {
- super(cacheWriter, defaultCacheConfiguration, initialCacheNames);
- this.cacheWriter = cacheWriter;
- }
-
- public DaxpayRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration,
- boolean allowInFlightCacheCreation, String... initialCacheNames) {
- super(cacheWriter, defaultCacheConfiguration, allowInFlightCacheCreation, initialCacheNames);
- this.cacheWriter = cacheWriter;
- }
-
- public DaxpayRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration,
- Map initialCacheConfigurations) {
- super(cacheWriter, defaultCacheConfiguration, initialCacheConfigurations);
- this.cacheWriter = cacheWriter;
- }
-
- public DaxpayRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration,
- Map initialCacheConfigurations, boolean allowInFlightCacheCreation) {
- super(cacheWriter, defaultCacheConfiguration, allowInFlightCacheCreation, initialCacheConfigurations);
- this.cacheWriter = cacheWriter;
+ this.defaultConfig = defaultCacheConfiguration;
+ this.secureConfig = secureCacheConfiguration;
+ this.secureMatcher = secureMatcher;
}
/// 创建 Redis 缓存
@Override
@SuppressWarnings({ "ConstantConditions", "NullableProblems" })
protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
- return new DaxpayRedisCache(name, this.cacheWriter, cacheConfig);
+ RedisCacheConfiguration config = this.resolveConfig(name, cacheConfig);
+ return new DaxpayRedisCache(name, this.cacheWriter, config);
}
+ /// 按 cacheName 解析最终 RedisCacheConfiguration
+ private RedisCacheConfiguration resolveConfig(String name, RedisCacheConfiguration cacheConfig) {
+ // 敏感缓存且加密 L2 可用:强制使用 secure 序列化,避免 initialCacheConfigurations 漏配
+ if (this.secureMatcher != null
+ && this.secureMatcher.matches(name)
+ && this.secureConfig != null) {
+ return this.secureConfig;
+ }
+ if (cacheConfig != null) {
+ return cacheConfig;
+ }
+ return this.defaultConfig;
+ }
}
diff --git a/daxpay-platform/daxpay-platform-capability/capability-cache/src/main/java/cn/daxpay/open/platform/capability/cache/core/MultiLevelCache.java b/daxpay-platform/daxpay-platform-capability/capability-cache/src/main/java/cn/daxpay/open/platform/capability/cache/core/MultiLevelCache.java
index e5c21150d..f57d212d5 100644
--- a/daxpay-platform/daxpay-platform-capability/capability-cache/src/main/java/cn/daxpay/open/platform/capability/cache/core/MultiLevelCache.java
+++ b/daxpay-platform/daxpay-platform-capability/capability-cache/src/main/java/cn/daxpay/open/platform/capability/cache/core/MultiLevelCache.java
@@ -14,8 +14,9 @@ import java.util.concurrent.Callable;
/// 设计要点:
/// - L2 Redis 是共享缓存主层,负责跨节点数据共享
/// - L1 Caffeine 是性能加速层,仅在本节点生效
-/// - 缓存失效通过 RocketMQ 广播通知其他节点删除本地 L1
+/// - 缓存失效通过 Artemis 广播通知其他节点删除本地 L1
/// - 本地缓存 key 必须统一使用字符串形式,保证跨节点广播删除一致性
+/// - 敏感缓存(secure:)在数据加密未启用时仅使用 L1,禁止明文写 Redis
@Slf4j
public class MultiLevelCache implements Cache {
@@ -27,14 +28,27 @@ public class MultiLevelCache implements Cache {
private final CacheInvalidationPublisher publisher;
+ /// 是否为敏感缓存名
+ private final boolean secureCache;
+
+ /// 敏感缓存是否允许写/读 L2(依赖数据加密已启用)
+ private final boolean secureL2Enabled;
+
public MultiLevelCache(String name,
LocalCacheRegistry localCacheRegistry,
Cache redisCache,
- CacheInvalidationPublisher publisher) {
+ CacheInvalidationPublisher publisher,
+ boolean secureCache,
+ boolean secureL2Enabled) {
this.name = name;
this.localCache = localCacheRegistry.getOrCreate(name);
this.redisCache = redisCache;
this.publisher = publisher;
+ this.secureCache = secureCache;
+ this.secureL2Enabled = secureL2Enabled;
+ if (secureCache && !secureL2Enabled) {
+ log.warn("敏感缓存 [{}] 因未启用数据加密,仅使用 L1 本地缓存,不写 Redis", name);
+ }
}
@Override
@@ -47,6 +61,11 @@ public class MultiLevelCache implements Cache {
return this.localCache;
}
+ /// 是否跳过 L2(敏感且未开加密)
+ private boolean isL1Only() {
+ return this.secureCache && !this.secureL2Enabled;
+ }
+
/// 读取缓存,优先从 L1 本地缓存读取,未命中则从 L2 Redis 读取并回填 L1
///
/// 读取流程:
@@ -54,6 +73,8 @@ public class MultiLevelCache implements Cache {
/// - L1 未命中则查 L2 Redis
/// - L2 命中则回填 L1
/// - 全未命中返回 null
+ ///
+ /// 敏感缓存且未启用加密时跳过 L2,仅查 L1
@Override
public ValueWrapper get(Object key) {
String localKey = this.toLocalKey(key);
@@ -62,6 +83,12 @@ public class MultiLevelCache implements Cache {
return () -> localValue;
}
+ // 敏感缓存未开加密:禁止读 Redis,避免历史明文或无法解密的脏数据
+ if (this.isL1Only()) {
+ log.debug("敏感缓存 L1-only 未命中: cacheName={}, key={}", this.name, key);
+ return null;
+ }
+
ValueWrapper redisValue = this.redisCache.get(key);
if (redisValue != null) {
this.localCache.put(localKey, Objects.requireNonNull(redisValue.get()));
@@ -103,6 +130,8 @@ public class MultiLevelCache implements Cache {
/// 写入缓存,同时写入 L2 Redis 和 L1 本地缓存
///
/// 注意:写入操作不广播通知其他节点,其他节点在读取时会从 L2 加载最新值
+ ///
+ /// 敏感缓存且未启用加密时仅写 L1,禁止明文写 Redis
@Override
public void put(Object key, Object value) {
if (value == null) {
@@ -110,6 +139,11 @@ public class MultiLevelCache implements Cache {
return;
}
String localKey = this.toLocalKey(key);
+ if (this.isL1Only()) {
+ this.localCache.put(localKey, value);
+ log.debug("写入 L1-only 敏感缓存: cacheName={}, key={}", this.name, key);
+ return;
+ }
this.redisCache.put(key, value);
this.localCache.put(localKey, value);
log.debug("写入二级缓存: cacheName={}, key={}", this.name, key);
@@ -120,11 +154,12 @@ public class MultiLevelCache implements Cache {
/// 删除流程:
/// - 删除 L2 Redis
/// - 删除本机 L1
- /// - 发布 RocketMQ 广播消息
+ /// - 发布 Artemis 广播消息
/// - 其他节点收到消息后删除各自 L1
@Override
public void evict(Object key) {
String localKey = this.toLocalKey(key);
+ // 仍尝试删 Redis,清理可能存在的历史数据
this.redisCache.evict(key);
this.localCache.invalidate(localKey);
log.debug("删除二级缓存: cacheName={}, key={}", this.name, key);
@@ -144,7 +179,7 @@ public class MultiLevelCache implements Cache {
///
/// 为什么必须统一使用字符串 key:
/// - 本地缓存原始 key 可能是任意对象类型(Long、String、自定义对象等)
- /// - RocketMQ 广播消息中的 key 只能是字符串
+ /// - Artemis 广播消息中的 key 只能是字符串
/// - 如果本地缓存使用原始对象作为 key,广播消息使用字符串,会导致跨节点删除失败
/// - 例如:本机 key=Long(1),广播 key="1",远端无法匹配
///
@@ -159,4 +194,3 @@ public class MultiLevelCache implements Cache {
return String.valueOf(key);
}
}
-
diff --git a/daxpay-platform/daxpay-platform-capability/capability-cache/src/main/java/cn/daxpay/open/platform/capability/cache/core/MultiLevelCacheManager.java b/daxpay-platform/daxpay-platform-capability/capability-cache/src/main/java/cn/daxpay/open/platform/capability/cache/core/MultiLevelCacheManager.java
index 871659640..aa2da3a80 100644
--- a/daxpay-platform/daxpay-platform-capability/capability-cache/src/main/java/cn/daxpay/open/platform/capability/cache/core/MultiLevelCacheManager.java
+++ b/daxpay-platform/daxpay-platform-capability/capability-cache/src/main/java/cn/daxpay/open/platform/capability/cache/core/MultiLevelCacheManager.java
@@ -1,6 +1,7 @@
package cn.daxpay.open.platform.capability.cache.core;
import cn.daxpay.open.platform.capability.cache.notify.publisher.CacheInvalidationPublisher;
+import cn.daxpay.open.platform.capability.cache.secure.SecureCacheNameMatcher;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.Cache;
@@ -18,7 +19,8 @@ import java.util.concurrent.ConcurrentHashMap;
/// 设计要点:
/// - 作为 Spring 默认 CacheManager,通过 @Primary 标识
/// - L1 本地缓存始终启用,不需要开关控制
-/// - 缓存失效通知通过 RocketMQ 广播,始终启用
+/// - 缓存失效通知通过 Artemis 广播,始终启用
+/// - 敏感 cacheName 结合 secureL2Enabled 决定是否写 Redis
@Slf4j
@RequiredArgsConstructor
public class MultiLevelCacheManager implements CacheManager {
@@ -29,6 +31,11 @@ public class MultiLevelCacheManager implements CacheManager {
private final CacheInvalidationPublisher publisher;
+ private final SecureCacheNameMatcher secureMatcher;
+
+ /// 敏感缓存是否允许 L2(数据加密已启用时为 true)
+ private final boolean secureL2Enabled;
+
private final Map cacheMap = new ConcurrentHashMap<>();
@Override
@@ -50,13 +57,15 @@ public class MultiLevelCacheManager implements CacheManager {
throw new IllegalStateException("Redis cache not found: " + name);
}
- log.debug("创建二级缓存: name={}", name);
+ boolean secureCache = this.secureMatcher != null && this.secureMatcher.matches(name);
+ log.debug("创建二级缓存: name={}, secure={}, secureL2Enabled={}", name, secureCache, this.secureL2Enabled);
return new MultiLevelCache(
name,
this.localCacheRegistry,
redisCache,
- this.publisher
+ this.publisher,
+ secureCache,
+ this.secureL2Enabled
);
}
}
-
diff --git a/daxpay-platform/daxpay-platform-capability/capability-cache/src/main/java/cn/daxpay/open/platform/capability/cache/secure/EncryptingRedisSerializer.java b/daxpay-platform/daxpay-platform-capability/capability-cache/src/main/java/cn/daxpay/open/platform/capability/cache/secure/EncryptingRedisSerializer.java
new file mode 100644
index 000000000..05b34ebc3
--- /dev/null
+++ b/daxpay-platform/daxpay-platform-capability/capability-cache/src/main/java/cn/daxpay/open/platform/capability/cache/secure/EncryptingRedisSerializer.java
@@ -0,0 +1,77 @@
+package cn.daxpay.open.platform.capability.cache.secure;
+
+import cn.daxpay.open.platform.common.config.encrypt.SecureAesGcmEncryptor;
+import cn.daxpay.open.platform.common.json.util.JacksonUtil;
+import tools.jackson.core.JacksonException;
+import tools.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.serializer.RedisSerializer;
+import org.springframework.data.redis.serializer.SerializationException;
+
+import java.nio.charset.StandardCharsets;
+
+/// # 敏感缓存 L2 整包加密序列化器
+///
+/// 将缓存对象序列化为 JSON 后,对**整段 JSON 字符串**做 AES-256-GCM 加密再写入 Redis。
+/// 非字段级加密:密钥字段与非敏感字段一起进入同一密文包。
+///
+/// 流程:
+/// - serialize: Object → JSON → encrypt → `v{n}:...` bytes
+/// - deserialize: bytes → decrypt → JSON → Object
+///
+/// JSON 编解码与 JacksonRedisSerializer 一致,使用平台标准 ObjectMapper。
+@Slf4j
+public class EncryptingRedisSerializer implements RedisSerializer