應用層的東西,找到接口實現它即可。
如果想要自己選擇序列化工具,難點還是在自動轉型上,在序列化字符串轉成對象的過程中,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請求即可攻擊到系統內部 } }