(四)spring security:gateway集成security,之redis缓存用户信息踩坑记

一、前言

    在单体应用开发框架中呢,我已经成功的集成了 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

https://blog.csdn.net/yage124/article/details/107321339/

https://www.cnblogs.com/puzhiwei/p/12519304.html

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值