refactor(cache): 纵深防御与风格清理(autoType/锁key/抖动上界/key校验/proxyBeanMethods)
- CacheValueCodec.decode 关闭 fastjson @type 自动类型(Feature.IgnoreAutoType): 纵深防御;经验证常规链路本已安全(指定目标类型时 @type 被忽略),此为显式锁定契约。 - 模板锁 key 改用 keyBuilder.buildLock(key),与 CacheKeyBuilder 单一来源,消除重复派生。 - applyJitter 上界改 nextLong(bound) 对齐 spec 半开区间 [ttl, ttl*(1+ratio));测试收紧为严格 <。 - CacheKeyBuilder.build 对空白 bizKey fail-fast,避免 "cache:null" 静默键碰撞。 - RedisCacheAutoConfiguration 用 @Configuration(proxyBeanMethods=false),省 CGLIB 代理开销。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,7 +17,7 @@ import org.springframework.data.redis.core.StringRedisTemplate;
|
|||||||
* Redis 缓存自动装配。装配在 {@link RedisAutoConfiguration} 之后,确保 {@link StringRedisTemplate} 就绪。
|
* Redis 缓存自动装配。装配在 {@link RedisAutoConfiguration} 之后,确保 {@link StringRedisTemplate} 就绪。
|
||||||
* 与 stream 装配互不引用;纯模板、无后台线程。
|
* 与 stream 装配互不引用;纯模板、无后台线程。
|
||||||
*/
|
*/
|
||||||
@Configuration
|
@Configuration(proxyBeanMethods = false)
|
||||||
@ConditionalOnClass(StringRedisTemplate.class)
|
@ConditionalOnClass(StringRedisTemplate.class)
|
||||||
@ConditionalOnBean(StringRedisTemplate.class)
|
@ConditionalOnBean(StringRedisTemplate.class)
|
||||||
@AutoConfigureAfter(RedisAutoConfiguration.class)
|
@AutoConfigureAfter(RedisAutoConfiguration.class)
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ public class CacheKeyBuilder {
|
|||||||
|
|
||||||
/** 业务 key → 实际数据 key。 */
|
/** 业务 key → 实际数据 key。 */
|
||||||
public String build(String bizKey) {
|
public String build(String bizKey) {
|
||||||
|
if (StrUtil.isBlank(bizKey)) {
|
||||||
|
throw new IllegalArgumentException("bizKey must not be blank");
|
||||||
|
}
|
||||||
if (envIsolation && StrUtil.isNotBlank(env)) {
|
if (envIsolation && StrUtil.isNotBlank(env)) {
|
||||||
return keyPrefix + env + ":" + bizKey;
|
return keyPrefix + env + ":" + bizKey;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.njcn.middle.cache.support;
|
|||||||
|
|
||||||
import com.alibaba.fastjson.JSON;
|
import com.alibaba.fastjson.JSON;
|
||||||
import com.alibaba.fastjson.TypeReference;
|
import com.alibaba.fastjson.TypeReference;
|
||||||
|
import com.alibaba.fastjson.parser.Feature;
|
||||||
import com.njcn.middle.cache.constant.CacheConstant;
|
import com.njcn.middle.cache.constant.CacheConstant;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,13 +26,13 @@ public class CacheValueCodec {
|
|||||||
return CacheConstant.NULL_SENTINEL.equals(raw);
|
return CacheConstant.NULL_SENTINEL.equals(raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 字符串 → 对象(Class)。 */
|
/** 字符串 → 对象(Class)。关闭 @type 自动类型(纵深防御:禁止脏值据 @type 实例化任意类)。 */
|
||||||
public <T> T decode(String raw, Class<T> type) {
|
public <T> T decode(String raw, Class<T> type) {
|
||||||
return JSON.parseObject(raw, type);
|
return JSON.parseObject(raw, type, Feature.IgnoreAutoType);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 字符串 → 对象(TypeReference,支持泛型)。 */
|
/** 字符串 → 对象(TypeReference,支持泛型)。同样关闭 @type 自动类型。 */
|
||||||
public <T> T decode(String raw, TypeReference<T> type) {
|
public <T> T decode(String raw, TypeReference<T> type) {
|
||||||
return JSON.parseObject(raw, type);
|
return JSON.parseObject(raw, type, Feature.IgnoreAutoType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ public class RedisCacheEnhanceTemplate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 在原 TTL 上叠加 [0, ttl*ratio] 的随机抖动(只增不减);ratio<=0 或 ttl<=0 时原样返回。 */
|
/** 在原 TTL 上叠加 [0, ttl*ratio) 的随机抖动(只增不减,半开区间);ratio<=0 或 ttl<=0 时原样返回。 */
|
||||||
long applyJitter(long ttlSeconds) {
|
long applyJitter(long ttlSeconds) {
|
||||||
double ratio = props.getJitterRatio();
|
double ratio = props.getJitterRatio();
|
||||||
if (ratio <= 0 || ttlSeconds <= 0) {
|
if (ratio <= 0 || ttlSeconds <= 0) {
|
||||||
@@ -128,7 +128,7 @@ public class RedisCacheEnhanceTemplate {
|
|||||||
if (bound <= 0) {
|
if (bound <= 0) {
|
||||||
return ttlSeconds;
|
return ttlSeconds;
|
||||||
}
|
}
|
||||||
return ttlSeconds + ThreadLocalRandom.current().nextLong(bound + 1);
|
return ttlSeconds + ThreadLocalRandom.current().nextLong(bound);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** TTL 必须为正:0/负会被 Redis 当作"立即删除"(EXPIRE)或非法过期时间(SET),显式 fail-fast。 */
|
/** TTL 必须为正:0/负会被 Redis 当作"立即删除"(EXPIRE)或非法过期时间(SET),显式 fail-fast。 */
|
||||||
@@ -202,7 +202,7 @@ public class RedisCacheEnhanceTemplate {
|
|||||||
return loadAndCache(dataKey, ttlSeconds, loader);
|
return loadAndCache(dataKey, ttlSeconds, loader);
|
||||||
}
|
}
|
||||||
|
|
||||||
String lockKey = dataKey + CacheConstant.LOCK_SUFFIX;
|
String lockKey = keyBuilder.buildLock(key); // 复用 keyBuilder,锁 key 规则单一来源
|
||||||
String token = IdUtil.fastSimpleUUID();
|
String token = IdUtil.fastSimpleUUID();
|
||||||
boolean locked = Boolean.TRUE.equals(redis.opsForValue()
|
boolean locked = Boolean.TRUE.equals(redis.opsForValue()
|
||||||
.setIfAbsent(lockKey, token, Duration.ofMillis(bd.getLockTtlMs())));
|
.setIfAbsent(lockKey, token, Duration.ofMillis(bd.getLockTtlMs())));
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.njcn.middle.cache.support;
|
|||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
class CacheKeyBuilderTest {
|
class CacheKeyBuilderTest {
|
||||||
|
|
||||||
@@ -31,4 +32,12 @@ class CacheKeyBuilderTest {
|
|||||||
assertEquals("cache:prod:user:1:__lock__",
|
assertEquals("cache:prod:user:1:__lock__",
|
||||||
new CacheKeyBuilder("cache:", true, "prod").buildLock("user:1"));
|
new CacheKeyBuilder("cache:", true, "prod").buildLock("user:1"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void buildRejectsBlankBizKey() {
|
||||||
|
CacheKeyBuilder kb = new CacheKeyBuilder("cache:", false, "");
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> kb.build(null));
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> kb.build(""));
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> kb.build(" "));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,4 +61,13 @@ class CacheValueCodecTest {
|
|||||||
void decodeDirtyStringThrows() {
|
void decodeDirtyStringThrows() {
|
||||||
assertThrows(RuntimeException.class, () -> codec.decode("{not-json", Foo.class));
|
assertThrows(RuntimeException.class, () -> codec.decode("{not-json", Foo.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void decodeIgnoresAtTypeHint() {
|
||||||
|
// 含 @type 的脏值(如 Redis 值被外部篡改注入)应被忽略,安全解析为目标类型
|
||||||
|
String raw = "{\"@type\":\"java.util.HashMap\",\"id\":1,\"name\":\"a\"}";
|
||||||
|
Foo foo = codec.decode(raw, Foo.class);
|
||||||
|
assertEquals(1L, foo.getId());
|
||||||
|
assertEquals("a", foo.getName());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,9 +111,9 @@ class RedisCacheEnhanceTemplateTest {
|
|||||||
void applyJitterWithinBounds() {
|
void applyJitterWithinBounds() {
|
||||||
props.setJitterRatio(0.1);
|
props.setJitterRatio(0.1);
|
||||||
long bound = (long) (100 * 0.1);
|
long bound = (long) (100 * 0.1);
|
||||||
for (int i = 0; i < 50; i++) {
|
for (int i = 0; i < 500; i++) {
|
||||||
long ttl = template.applyJitter(100);
|
long ttl = template.applyJitter(100);
|
||||||
assertTrue(ttl >= 100 && ttl <= 100 + bound, "ttl=" + ttl);
|
assertTrue(ttl >= 100 && ttl < 100 + bound, "ttl=" + ttl); // 半开区间 [100,110)
|
||||||
}
|
}
|
||||||
props.setJitterRatio(0);
|
props.setJitterRatio(0);
|
||||||
assertEquals(100, template.applyJitter(100));
|
assertEquals(100, template.applyJitter(100));
|
||||||
|
|||||||
Reference in New Issue
Block a user