一、当认证与授权数据缓存至redis后,进行反序列化时,会出现反序列化失败:
分析
提示很清楚,就是shiro的SimplePrincipalCollection类中realmNames字段没有setter方法,没法反序列化。
来看看realmNames是什么,作为成熟的框架为什么不写setter?仔细一看,发现并不那么回事。类里面没有realmNames,只有个getRealmNames方法。
原来是个假getter,是由其他字段动态生成的,如下:
public Set<String> getRealmNames() {
if (realmPrincipals == null) {
return null;
} else {
return realmPrincipals.keySet();
}
}
看下redis里面存的值
这个get方法被序列化了,并存到redis了,然后在反序列化的时候,又没有setter,就报了异常
尝试在反序列化时忽略
本以为objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)就能解决,但是跟踪了一圈源码之后,发现因为有getter,这个字段已经不算未知字段了。。。
其实这样的字段序列化和反序列化也没啥意义,仔细研究这个类之后,发现整个类值得存储的字段就只有realmPrincipals。
序列化时去掉无关字段
既然不能反序列化的时候解决,那就在序列化的时候控制需要序列化的字段就可以了。
常规的方式是在不需要的属性上面加注解@JsonIgnore就ok的,就是那么简单也办不到,因为这个类是框架jar包的类无法修改。那就只有寻找不常规的方法了。
最后终于找到了,ObjectMapper配置如下。
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
ObjectMapper objectMapper = ApplicationContextUtils.getBean(ObjectMapper.class).copy();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
//objectMapper.configure(MapperFeature.USE_ANNOTATIONS, false);
//设置输入时忽略JSON字符串中存在而Java对象实际没有的属性
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
//只序列化必要shiro字段
String[] needSerialize = {"realmPrincipals"};
objectMapper.addMixIn(SimplePrincipalCollection.class, IncludeShiroFields.class);
objectMapper.setFilterProvider(new SimpleFilterProvider().addFilter("shiroFilter", SimpleBeanPropertyFilter.filterOutAllExcept(needSerialize)));
// 此项必须配置,否则会报java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to XXX
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
// 使用Jackson2JsonRedisSerialize 替换默认序列化
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// 设置value的序列化规则和key的序列化规则
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
核心就是objectMapper.addMixIn()和objectMapper.setFilters()两个方法
SimplePrincipalCollection是需要处理的类,IncludShiroFields就是一个简单的接口,如下:
@JsonFilter("shiroFilter")
interface IncludeShiroFields {
}
通过上面的配置间接控制SimplePrincipalCollection类中必要字段的序列化,从而解决了问题。
二、当认证与授权数据缓存至redis时,盐不能序列化:
分析
进入ByteSource类中,可以清楚的看到,该类没有实现Serializable,导致redis无法序列化至缓存。
解决办法就是自定义加盐工具类,实现Serializable,代码如下:
public class SaltSourceUtils implements ByteSource, Serializable {
private byte[] bytes;
private String cachedHex;
private String cachedBase64;
public SaltSourceUtils() {
}
private SaltSourceUtils(byte[] bytes) {
this.bytes = bytes;
}
public SaltSourceUtils(char[] chars) {
this.bytes = CodecSupport.toBytes(chars);
}
private SaltSourceUtils(String string) {
this.bytes = CodecSupport.toBytes(string);
}
public SaltSourceUtils(ByteSource source) {
this.bytes = source.getBytes();
}
public SaltSourceUtils(File file) {
this.bytes = (new SaltSourceUtils.BytesHelper()).getBytes(file);
}
public SaltSourceUtils(InputStream stream) {
this.bytes = (new SaltSourceUtils.BytesHelper()).getBytes(stream);
}
public static boolean isCompatible(Object o) {
return o instanceof byte[] || o instanceof char[] || o instanceof String || o instanceof ByteSource || o instanceof File || o instanceof InputStream;
}
@Override
public byte[] getBytes() {
return this.bytes;
}
@Override
public boolean isEmpty() {
return this.bytes == null || this.bytes.length == 0;
}
@Override
public String toHex() {
if (this.cachedHex == null) {
this.cachedHex = Hex.encodeToString(this.getBytes());
}
return this.cachedHex;
}
@Override
public String toBase64() {
if (this.cachedBase64 == null) {
this.cachedBase64 = Base64.encodeToString(this.getBytes());
}
return this.cachedBase64;
}
@Override
public String toString() {
return this.toBase64();
}
@Override
public int hashCode() {
return this.bytes != null && this.bytes.length != 0 ? Arrays.hashCode(this.bytes) : 0;
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
} else if (o instanceof ByteSource) {
ByteSource bs = (ByteSource) o;
return Arrays.equals(this.getBytes(), bs.getBytes());
} else {
return false;
}
}
public static SaltSourceUtils bytes(byte[] bytes){
return new SaltSourceUtils(bytes);
}
public static SaltSourceUtils bytes(String string){
return new SaltSourceUtils(string);
}
private static final class BytesHelper extends CodecSupport {
private BytesHelper() {
}
byte[] getBytes(File file) {
return this.toBytes(file);
}
byte[] getBytes(InputStream stream) {
return this.toBytes(stream);
}
}
}
在使用时只需要将加盐工具类换成自定义的即可: