应用层的东西,找到接口实现它即可。
如果想要自己选择序列化工具,难点还是在自动转型上,在序列化字符串转成对象的过程中,Spring并未提供有效的、带Class参数的接口,类型自动转换问题,需要第三方框架自行处理。
最好选用带自动转型的序列化框架,错误的写法,很容易导致类型强转失败,本文采用的是FastJSON。
简单的工具类,按自己需求封装,可以指定各种数据类型序列化格式。
/**
* @author ChenSS
* @date 2018年7月13日 v1
* 2019年10月16日 v2 优化日期
*/
public class FastJsonUtils {
public static final SerializeConfig serializeConfig;
static {
serializeConfig = new SerializeConfig();
FastJsonDateSerializer dateTimeSerializer = new FastJsonDateSerializer("yyyy-MM-dd HH:mm:ss");
serializeConfig.put(Date.class, dateTimeSerializer);
serializeConfig.put(java.sql.Timestamp.class, dateTimeSerializer);
serializeConfig.put(java.sql.Date.class, new FastJsonDateSerializer("yyyy-MM-dd"));
serializeConfig.put(java.sql.Time.class, new FastJsonDateSerializer("HH:mm:ss"));
// // 使用和json-lib兼容的日期输出格式
// config.put(java.util.Date.class, new JSONLibDataFormatSerializer());
// config.put(java.sql.Date.class, new JSONLibDataFormatSerializer());
}
}
FastJson2JsonRedisSerializer
很多人写这个类,完全模仿Jackson的写法,其实没必要,明确自己的需求,保留最少的代码即可。
我泛型直接写Object,因为代码已经能够处理全部类型的数据了。
import cn.seaboot.common.core.FastJsonUtils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import java.nio.charset.Charset;
/**
* @author Mr.css
* @date 2020/1/2 11:24
*/
public class FastJson2JsonRedisSerializer implements RedisSerializer<Object> {
@Override
public byte[] serialize(Object o) throws SerializationException {
if (o == null) {
return new byte[0];
} else {
return JSON.toJSONString(o, FastJsonUtils.serializeConfig, SerializerFeature.WriteClassName).getBytes(Charset.defaultCharset());
}
}
@Override
public Object deserialize(byte[] bytes) throws SerializationException {
if (bytes == null || bytes.length <= 0) {
return null;
} else {
return JSON.parse(new String(bytes, Charset.defaultCharset()));
}
}
}
RedisConfig
如果在使用Cache注解的时候有写key的习惯,KeyGenerator 可以不需要配置,我这里把函数名和所有的参数拼在一起,做成默认的Key值。
import cn.seaboot.common.core.Converter;
import com.alibaba.fastjson.parser.ParserConfig;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import javax.annotation.Resource;
import java.time.Duration;
/**
* @author Mr.css on 2019/12/26
* @date 2019/12/31
*/
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
@Resource
private LettuceConnectionFactory lettuceConnectionFactory;
/**
* Cache注解可以不指定key,需要有默认策略,按需调整
*/
@Bean
@Override
public KeyGenerator keyGenerator() {
return (target, method, params) -> {
StringBuilder sb = new StringBuilder();
sb.append(method.getName());
if(params.length > 0){
for (int i = 1; i < params.length; i++) {
sb.append(Converter.toString(params[i]));
}
}
return sb.toString();
};
}
@Bean
@Override
public CacheManager cacheManager() {
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(lettuceConnectionFactory);
FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer();
RedisSerializationContext.SerializationPair<Object> pair =
RedisSerializationContext.SerializationPair.fromSerializer(serializer);
RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig().serializeValuesWith(pair);
//设置过期时间 30天
defaultCacheConfig = defaultCacheConfig.entryTtl(Duration.ofDays(30));
//初始化RedisCacheManager
RedisCacheManager cacheManager = new RedisCacheManager(redisCacheWriter, defaultCacheConfig);
//反序列化白名单
ParserConfig.getGlobalInstance().addAccept("cn.seaboot.admin.bean.");
return cacheManager;
}
}
yml
redis:
host: 127.0.0.1
port: 6379
timeout: 1000
jedis:
pool:
min-idle: 1
max-idle: 8
max-wait: 5000
Maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
补充
集成RedisCache,上面代码已经足够,这里介绍FastJSON的一些问题。
FastJSON自动转型的写法
public static void main(String[] args) {
ParserConfig.getGlobalInstance().addAccept("cn.swsk.xbry.entity");
TUserInfoEntity entity = new TUserInfoEntity();
entity.setId("13123");
//使用 SerializerFeature.WriteClassName 可以让JSON自动转型
//不依赖 clazz 参数也达到 JSON.parseObject(String json, Class<T> clazz) 相同效果
String str = JSON.toJSONString(entity, SerializerFeature.WriteClassName);
System.out.println(JSON.parse(str).getClass());
}
ParserConfig.getGlobalInstance()的必要性
FastJSON最初是没有白名单这个要求的,addAccept接口的设计源自于系统漏洞。
假设去除掉白名单的设计,在知道全类名的情况下,通过Http接口即可创建出系统的任何对象。
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
/**
* @author Mr.css
* @date 2020/1/6
*/
public class Test {
public static void main(String[] args) {
//在没有 ParserConfig.getGlobalInstance() 的情况下,只要知道全类名,即可 new 出程序中任何一个对象
ParserConfig.getGlobalInstance().addAccept("cn.swsk.xbry.entity");
//下列这行代码,等效于Class.forName().newInstance()
System.out.println(JSON.parse("{\"@type\":\"cn.swsk.xbry.entity.TUserInfoEntity\"}").getClass());
//因为设计原因,或者生产需求,或许你曾经改造过@RequestBody,如果是采用JSON.parse()方式实现的,
//那么,要是没有白名单的设计,通过http请求即可攻击到系统内部
}
}