一、前言
在单体应用开发框架中呢,我已经成功的集成了 spring security,实现了很多功能包括如下(后续文章将会体现):
* 1.异常统一处理
* 2.认证与授权;配置动态权限控制
* 3.匹配 bcrypt 或 MD5 加密方式
* 4.前后端分离登录退出配置;自定义登录成功失败处理
* 5.无状态会话,jwt认证
* 6.角色继承判断
* 7.图形验证码
今天不说这个,而是说,我在进行系统设计与搭建spring cloud greenwich + spring cloud alibaba,运用gateway集成security,使用redis + spring-cache缓存用户信息 UserDetailBO类,发生了如下异常:
Caused by: com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `org.springframework.security.core.authority.SimpleGrantedAuthority` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)
然后,我就调试了一整天,使用各种办法去解决它。。。,_(:3」∠❀)_菊花碎了一地
二、分析
1.SimpleGrantedAuthority (元凶就是它,其实,在我的单体框架中,ehcache缓存并没有出现那个异常)
可以看看源码
package org.springframework.security.core.authority;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.util.Assert;
public final class SimpleGrantedAuthority implements GrantedAuthority {
private static final long serialVersionUID = 510L;
private final String role;
public SimpleGrantedAuthority(String role) {
Assert.hasText(role, "A granted authority textual representation is required");
this.role = role;
}
public String getAuthority() {
return this.role;
}
public boolean equals(Object obj) {
if (this == obj) {
return true;
} else {
return obj instanceof SimpleGrantedAuthority ? this.role.equals(((SimpleGrantedAuthority)obj).role) : false;
}
}
public int hashCode() {
return this.role.hashCode();
}
public String toString() {
return this.role;
}
}
造成以上异常原因有两个:
- getAuthority(),就是以后代码中常用的;可是,它用的属性是:role
- SimpleGrantedAuthority 并没有提供"无参构造函数";无法反序列化
看我的 RedisConfig配置中,采用的是 Jackson2JsonRedisSerializer序列化策略。
这个是重点,可以解决第一个问题。那第二个问题怎么办呢?
// 反序列化时,遇到未知属性(那些没有对应的属性来映射的属性,并且没有任何setter或handler来处理这样的属性)时,是否引起结果失败(通过抛JsonMappingException异常)
// 默认是启用的(意味着,如果遇到未知属性时会抛一个JsonMappingException)
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
/**
* @desc: RedisConfig
* @author: yanfei
* @date: 2020/11/20
*/
@Configuration
public class RedisConfig {
@Autowired
private JwtSecurityProperties jwtSecurityProperties;
/**
* 配置Jackson2JsonRedisSerializer序列化策略
* */
private Jackson2JsonRedisSerializer<Object> serializer() {
// 使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
// 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
// 反序列化时,遇到未知属性(那些没有对应的属性来映射的属性,并且没有任何setter或handler来处理这样的属性)时,是否引起结果失败(通过抛JsonMappingException异常)
// 默认是启用的(意味着,如果遇到未知属性时会抛一个JsonMappingException)
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
return jackson2JsonRedisSerializer;
}
private RedisCacheConfiguration getRedisCacheConfigurationWithTtl(Integer seconds) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
redisCacheConfiguration = redisCacheConfiguration
// 使用StringRedisSerializer来序列化和反序列化redis的key值
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer()))
.entryTtl(Duration.ofSeconds(seconds));
return redisCacheConfiguration;
}
/**
* 可以配置多个,指定 cache 失效时间策略(结合@CacheConfig使用)
* @return
*/
private Map<String, RedisCacheConfiguration> getRedisCacheConfigurationMap() {
//过期时间配置,单位:秒
int sec = Math.toIntExact(jwtSecurityProperties.getTokenValidityInSeconds() / 1000);
Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>();
redisCacheConfigurationMap.put("loginUserCache", this.getRedisCacheConfigurationWithTtl(sec));
return redisCacheConfigurationMap;
}
/**
* 申明缓存管理器,会创建一个切面(aspect)并触发Spring缓存注解的切点(pointcut)
* 根据类或者方法所使用的注解以及缓存的状态,这个切面会从缓存中获取数据,将数据添加到缓存之中或者从缓存中移除某个值
*
* @return
*/
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
return new RedisCacheManager(
RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory),
// 默认策略,未配置的 key 会使用这个
this.getRedisCacheConfigurationWithTtl(24 * 60 * 60),
// 指定 key 策略
this.getRedisCacheConfigurationMap()
);
}
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
redisTemplate.setValueSerializer(serializer());
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// 使用StringRedisSerializer来序列化和反序列化redis的key值
redisTemplate.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
redisTemplate.setHashKeySerializer(stringRedisSerializer);
// hash的value序列化方式采用jackson
redisTemplate.setHashValueSerializer(serializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
2.UserGrantedAuthority
我仿照了一个 SimpleGrantedAuthority,提供了一个"无参构造函数"。
/**
* @desc:
* @author: yanfei
* @date: 2020/11/30
*/
public class UserGrantedAuthority implements GrantedAuthority {
private static final long serialVersionUID = -2985922934585450525L;
private String authority;
public UserGrantedAuthority() {}
public UserGrantedAuthority(String authority) {
Assert.hasText(authority, "A granted authority textual representation is required");
this.authority = authority;
}
@Override
public String getAuthority() {
return this.authority;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
} else {
return obj instanceof UserGrantedAuthority ? this.authority.equals(((UserGrantedAuthority)obj).authority) : false;
}
}
@Override
public int hashCode() {
return this.authority.hashCode();
}
@Override
public String toString() {
return this.authority;
}
}
同时,在 UserDetailsServiceImpl(实际不叫这个名字:-)实现类中有所改动;看到 UserGrantedAuthority被用上了吧。
@Override
public UserDetails loadUserByUsername(String usercode) throws UsernameNotFoundException {
BaseAcUser user = queryByUsercode(usercode);
//验证登录用户的有效状态
validateUser(user, usercode);
UserDetailBO userDetailBO = userDetailBOMapper.map(user);
//安全框架只认 username 和 password
//账号
userDetailBO.setUsername(user.getUsercode());
userDetailBO.setPassword(processPassword(user.getUserpassword()));
//昵称
userDetailBO.setNickname(user.getUsername());
long userid = userDetailBO.getUserid();
//查询用户拥有的所有角色
Set<String> roles = baseAcUserRoleMapper.queryUserRoleByUserId(userid);
//默认登录用户具有"R_ANONYMOUS"角色
if (! roles.contains(UnifyRoleNoEnum.R_ANONYMOUS.getRoleNo())) {
roles.add(UnifyRoleNoEnum.R_ANONYMOUS.getRoleNo());
}
StringJoiner roleStr = new StringJoiner(",");
roles.forEach(role -> {
roleStr.add(role);
});
//此账号拥有的角色
//TODO redis能正确反序列化
userDetailBO.setAuthorities(createAuthorityList(roleStr.toString().split(",")));
//TODO 抛出异常:
// userDetailBO.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(roleStr.toString()));
return userDetailBO;
}
private List<GrantedAuthority> createAuthorityList(String... roles) {
List<GrantedAuthority> authorities = new ArrayList(roles.length);
String[] var2 = roles;
int var3 = roles.length;
for(int var4 = 0; var4 < var3; ++var4) {
String role = var2[var4];
authorities.add(new UserGrantedAuthority(role));
}
return authorities;
}
}
3.其实,并不需要这么麻烦;只是,我发现 spring security封装的很优雅,就学着用了。
三、参考文献,谢谢他们的文章
https://blog.csdn.net/xiejx618/article/details/44653703