redis反序列化的一次问题
1. 问题描述
springboot+redis不少用,但是一直没遇到什么问题,直接代码拷贝上去就用了。这次结合spring-security,将自定义的spring-security的UserDetails接口的实现类SecurityUser,反序列化取出时报错。
org.springframework.data.redis.serializer.SerializationException: Could not read JSON: Cannot construct instance of `com.jankin.inoteadmin.security.entity.SecurityUser` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
......
org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer.deserialize(GenericJackson2JsonRedisSerializer.java:165)
2. 问题解读
我猜肯定有人骂我了,上面问题描述不是很清晰了吗 no Creators, like default constructor, exist
。我的回答是,该骂,大骂特骂~
作为一名不称职的代码搬砖工,我遇到问题时,第一是间是查看是否实现了Serializable接口。确认父接口UserDetails实现了该接口还不死心,然后在本类中添加该接口,保存运行,报错依旧。慌了,按道理没道理啊,之前项目也是直接这样引依赖,建个RedisConfig类就行了啊,然后就打开百度……
结果还百度不出……(估计是没有人连这种问题需要百度,所以搜不到)
好,言归正传,问题就如上述所说,没有默认够构造器,也就是无参构造器。
我依稀记得大学教我java的老师,说到构造器时,反复强调,无论你建多少个有参构造器,都必须带上无参构造器,毕业后其实一直遵守老师所说的,每次建造构造方法时,都会建无参构造方法,lombok的 @AllArgsConstructor
和 @NoArgsConstructor
我一直都是连着用的。这次没用不是因为忘记了,而是SpringSecurity中自带的UserDetails实现类org.springframework.security.core.userdetails.User
本身就没带无参构造器,并且,其中的类属性还带上了final关键字,意思是在声明时或者构造方法中必须定义初始值。
好嘛,SpringSecurity默认的User都没问题,我自定义的SecurityUser仿照应该问题也不大。
然后就出这档子事儿了。
扯远了,总而言之啊,就是spring中redis的反序列化需要依赖无参构造器
3. 问题解决
那就加上无参构造器呗,SpringSecurity的自定义的SecurityUser还需要将对象属性的final关键字去掉。
public class SecurityUser implements UserDetails{
private static final long serialVersionUID = 1L;
private String userId;
private String password;
private String username;
private Set<GrantedAuthority> authorities;
private boolean accountNonExpired;
private boolean accountNonLocked;
private boolean credentialsNonExpired;
private boolean enabled;
/**
* 无参构造器
*/
public SecurityUser(){}
/**
* Simple Param Constructor
* @param userId 用户id
* @param password 用户密码
* @param username 用户名
* @param authorities 权限列表
*/
public SecurityUser(String userId, String username, String password, Set<GrantedAuthority> authorities) {
this(userId,username,password,authorities,
true,true,true,true);
}
/**
* AllParamConstructor
* @param userId 用户id
* @param password 用户密码
* @param username 用户名
* @param authorities 权限列表
* @param accountNonExpired 账号没过期
* @param accountNonLocked 账号没被锁
* @param credentialsNonExpired 凭证没过期
* @param enabled 正常
*/
public SecurityUser(String userId, String username, String password, Set<GrantedAuthority> authorities,
boolean accountNonExpired, boolean accountNonLocked,
boolean credentialsNonExpired, boolean enabled) {
this.userId = userId;
this.username = username;
this.password = password;
this.authorities = Collections.unmodifiableSet(authorities);
this.accountNonExpired = accountNonExpired;
this.accountNonLocked = accountNonLocked;
this.credentialsNonExpired = credentialsNonExpired;
this.enabled = enabled;
}
// other todo
}
一点点题外话
作者还没做过什么大型项目,单体应用上看,前后端分离的情况下SpringSecurity的认证和授权根本上就是两个Filter和AOP做注解鉴权就行,感觉没有必要用SpringSecurity,之前用的shiro,也用过sa-token,相比之下,SpringSecurity是真的复杂,前后端分离情况下,SpringSecurity的UserDetailsService也拉,不好做缓存认证。
说到SpringSecurity的认证,网上很多方案都是使用jwt做token认证标识,对于jwt做下线功能该怎么做呢,jwt一旦颁发就只能到期失效。这样token被盗用的话一点办法都没有。有人说在缓存或数据库中添加signature的加密串,或者用账户密码做加密串,这样下线时或者更改密码时修改加密串就行……这样做确实能解决jwt的失效问题,但是jwt的主要特点(优点)就是无状态,每次认证只需要依赖jwt本体就行,这样数据库或者缓存添加jwt的加密串,岂不是违背了原本jwt的初衷?这样和使用普通token,用token做key缓存用户登录状态信息有什么区别。
虽然言语略带偏激,但是确实是心中疑问,有朋友看到且有见解的话,还请指教一下。