从Redis反序列化UserDetails对象异常后中发现FastJson序列化的一些问题

        最近在使用SpringSecurity+JWT实现认证授权的时候,出现Redis在反序列化userDetails的异常。通过实践发现,使用不同的序列化方法和不同的fastJson版本,异常信息各不相同。所以特地记录了下来。

一、项目代码

        先来看看我项目中redis相关配置信息。

1.自定义的redis序列化器

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import com.alibaba.fastjson.parser.ParserConfig;
import org.springframework.util.Assert;
import java.nio.charset.Charset;

/**
 * Redis使用FastJson序列化
 *
 * @author mosul
 */
public class FastJsonRedisSerializer implements RedisSerializer
{

    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

    private Class clazz;

    static
    {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
    }

    public FastJsonRedisSerializer(Class clazz)
    {
        super();
        this.clazz = clazz;
    }

    @Override
    public byte[] serialize(T t) throws SerializationException
    {
        if (t == null)
        {
            return new byte[0];
        }
        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
    }

    @Override
    public T deserialize(byte[] bytes) throws SerializationException
    {
        if (bytes == null || bytes.length <= 0)
        {
            return null;
        }
        String str = new String(bytes, DEFAULT_CHARSET);

        return JSON.parseObject(str, clazz);
    }


    protected JavaType getJavaType(Class clazz)
    {
        return TypeFactory.defaultInstance().constructType(clazz);
    }
}

2.redis配置类

import com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {


    /**
     * 指定特定的连接工厂
     * @return
     */
   /*@Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory();
    }*/

    @Bean
    @SuppressWarnings(value = { "unchecked", "rawtypes" })
    public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory)
    {
        RedisTemplate template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class);

        // 使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);

        // Hash的key也采用StringRedisSerializer的序列化方式
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);

        template.afterPropertiesSet();
        return template;
    }
}

3.redis工具类

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.concurrent.TimeUnit;


/**
 * Redis帮助类
 *
 * @author mosul
 */
@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisHelper
{
    @Autowired
    public RedisTemplate redisTemplate;

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key 缓存的键值
     * @param value 缓存的值
     */
    public  void setCacheObject(final String key, final T value)
    {
        redisTemplate.opsForValue().set(key, value);
    }

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key 缓存的键值
     * @param value 缓存的值
     * @param timeout 时间
     * @param timeUnit 时间颗粒度
     */
    public  void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
    {
        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
    }

    /**
     * 设置有效时间
     *
     * @param key Redis键
     * @param timeout 超时时间
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout)
    {
        return expire(key, timeout, TimeUnit.SECONDS);
    }

    /**
     * 设置有效时间
     *
     * @param key Redis键
     * @param timeout 超时时间
     * @param unit 时间单位
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout, final TimeUnit unit)
    {
        return redisTemplate.expire(key, timeout, unit);
    }

    /**
     * 获得缓存的基本对象。
     *
     * @param key 缓存键值
     * @return 缓存键值对应的数据
     */
    public  T getCacheObject(final String key)
    {
        ValueOperations operation = redisTemplate.opsForValue();
        return operation.get(key);
    }

    /**
     * 删除单个对象
     *
     * @param key
     */
    public boolean deleteObject(final String key)
    {
        return redisTemplate.delete(key);
    }

    /**
     * 删除集合对象
     *
     * @param collection 多个对象
     * @return
     */
    public long deleteObject(final Collection collection)
    {
        return redisTemplate.delete(collection);
    }

    /**
     * 缓存List数据
     *
     * @param key 缓存的键值
     * @param dataList 待缓存的List数据
     * @return 缓存的对象
     */
    public  long setCacheList(final String key, final List dataList)
    {
        Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
        return count == null ? 0 : count;
    }

    /**
     * 获得缓存的list对象
     *
     * @param key 缓存的键值
     * @return 缓存键值对应的数据
     */
    public  List getCacheList(final String key)
    {
        return redisTemplate.opsForList().range(key, 0, -1);
    }

    /**
     * 缓存Set
     *
     * @param key 缓存键值
     * @param dataSet 缓存的数据
     * @return 缓存数据的对象
     */
    public  BoundSetOperations setCacheSet(final String key, final Set dataSet)
    {
        BoundSetOperations setOperation = redisTemplate.boundSetOps(key);
        Iterator it = dataSet.iterator();
        while (it.hasNext())
        {
            setOperation.add(it.next());
        }
        return setOperation;
    }

    /**
     * 获得缓存的set
     *
     * @param key
     * @return
     */
    public  Set getCacheSet(final String key)
    {
        return redisTemplate.opsForSet().members(key);
    }

    /**
     * 缓存Map
     *
     * @param key
     * @param dataMap
     */
    public  void setCacheMap(final String key, final Map dataMap)
    {
        if (dataMap != null) {
            redisTemplate.opsForHash().putAll(key, dataMap);
        }
    }

    /**
     * 获得缓存的Map
     *
     * @param key
     * @return
     */
    public  Map getCacheMap(final String key)
    {
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * 往Hash中存入数据
     *
     * @param key Redis键
     * @param hKey Hash键
     * @param value 值
     */
    public  void setCacheMapValue(final String key, final String hKey, final T value)
    {
        redisTemplate.opsForHash().put(key, hKey, value);
    }

    /**
     * 获取Hash中的数据
     *
     * @param key Redis键
     * @param hKey Hash键
     * @return Hash中的对象
     */
    public  T getCacheMapValue(final String key, final String hKey)
    {
        HashOperations opsForHash = redisTemplate.opsForHash();
        return opsForHash.get(key, hKey);
    }

    /**
     * 删除Hash中的数据
     *
     * @param key
     * @param hkey
     */
    public void delCacheMapValue(final String key, final String hkey)
    {
        HashOperations hashOperations = redisTemplate.opsForHash();
        hashOperations.delete(key, hkey);
    }

    /**
     * 获取多个Hash中的数据
     *
     * @param key Redis键
     * @param hKeys Hash键集合
     * @return Hash对象集合
     */
    public  List getMultiCacheMapValue(final String key, final Collection hKeys)
    {
        return redisTemplate.opsForHash().multiGet(key, hKeys);
    }

    /**
     * 获得缓存的基本对象列表
     *
     * @param pattern 字符串前缀
     * @return 对象列表
     */
    public Collection keys(final String pattern)
    {
        return redisTemplate.keys(pattern);
    }
} 
  

4.自己系统中的UserDetails

@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {

    private static final long serialVersionUID = 1L;


    // 系统用户
    private SysUser user;

    // 用户权限列表
    private List permissionList;


    @Override
    public Collection getAuthorities() {
        return permissionList.stream()
                .filter(permission -> permission.getPermission() != null)
                .map(permission -> new SimpleGrantedAuthority(permission.getPermission()))
                .collect(Collectors.toList());

    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

5.登录设置


@Override
    public String login(SysUser sysUser) {
        String token = null;
        //密码需要客户端加密后传递
        try {
            UserDetails userDetails = sysUserService.loadUserByUsername(sysUser.getUsername());
            if(!passwordEncoder.matches(sysUser.getPassword(),userDetails.getPassword())){
                throw new BadCredentialsException("密码不正确");
            }
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(authentication);
            token = jwtTokenUtil.generateToken(userDetails);

            String key = "login:" + sysUser.getUsername();
            //设置redis
            redisHelper.setCacheObject(key,userDetails);
            //insertLoginLog(username);
        } catch (AuthenticationException e) {
            LOGGER.warn("登录异常:{}", e.getMessage());
        }

        return token;
    }


@Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUser sysUser = sysUserMapper.selectOne(new QueryWrapper().eq("username", username));
        List permissionsByUser = sysUserRoleMapper.findPermissionsByUser(sysUser.getUserId());

        sysUser.setPassword(new BCryptPasswordEncoder().encode(sysUser.getPassword()));
        // 将系统的用户信息和权限信息封装成UserDetails
        UserDetails userDetail = new LoginUser(sysUser, permissionsByUser);

        return userDetail;
    }

6.JWT校验

 @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {


        String authHeader = request.getHeader(this.tokenHeader);
        if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
            String authToken = authHeader.substring(this.tokenHead.length());// The part after "Bearer "
            String username = jwtTokenUtil.getUserNameFromToken(authToken.trim());
            LOGGER.info("checking username:{}", username);
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {

                //从redis中获取userDetails
                String redisKey = "login:" + username;
                UserDetails userDetails = redisHelper.getCacheObject(redisKey);
                if(Objects.isNull(userDetails)){
                    throw new RuntimeException("用户未登录");
                }

                if (jwtTokenUtil.validateToken(authToken, userDetails)) {
                    //存入SecurityContextHolder
                    //TODO 获取权限信息封装到Authentication中
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    LOGGER.info("authenticated user:{}", username);
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }
        //放行
        filterChain.doFilter(request, response);
    }

7.fastjson版本

        
        
        
            com.alibaba
            fastjson
            1.2.47
        


        
        
            com.alibaba
            fastjson
            2.0.22
        

        上面的代码中,先根据用户名获取用户对应的用户信息和权限信息,然后构建SpringSecurity的UserDetails对象,用户登录的时候将这个UserDetails对象放入redis中,后续校验请求携带的token与redis中的信息是否一致。

二、异常信息

1.版本一报错信息

从Redis反序列化UserDetails对象异常后中发现FastJson序列化的一些问题_第1张图片

        需要说明的是,在redis系列化时,是正常的,对应的值也成功设置近缓存了,但是在JWT校验阶段,执行UserDetails userDetails = redisHelper.getCacheObject(redisKey);时出现异常,反序列化失败。

        针对这个问题,首先上面的代码逻辑是没有问题的,但是与fastjson反序列化不兼容导致的问题。

        根据异常信息提示,设置属性authorities错误,猜想下是因为LoginUser中没有authorities属性,但也说不过去,同样没有属性username和password怎么不会报错?

        带着这个疑问,我们先给将LoginUser代码改为下面这种形式。

@Data
public class LoginUser implements UserDetails {

    private static final long serialVersionUID = 1L;


    private SysUser user;

    private List permissionList;


    private List authorities;


    public LoginUser() {
    }

    public LoginUser(SysUser user, List permissionList) {
        this(user,permissionList,null);
    }


    /**
     * 针对fastJson中redis反序列化报错的改进
     * org.springframework.data.redis.serializer.SerializationException:
     * Could not deserialize: set authorities error; nested exception is com.alibaba.fastjson.JSONException: set authorities error
     *
     * @param user
     * @param permissionList
     * @param authorities
     */
    public LoginUser(SysUser user, List permissionList, List authorities) {
        //返回当前用户的权限
        List authoritieList = permissionList.stream()
                .filter(permission -> permission.getPermission() != null)
                .map(permission -> new SimpleGrantedAuthority(permission.getPermission()))
                .collect(Collectors.toList());
        this.user = user;
        this.permissionList = permissionList;
        this.authorities = authoritieList;
    }
    @Override
    public Collection getAuthorities() {
        return this.authorities;

    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

        发现这里改完之后,是可以正常运行的。 

2.版本二报错信息

从Redis反序列化UserDetails对象异常后中发现FastJson序列化的一些问题_第2张图片

         在使用fastJson 2.x版本的时候,同时需要对redis配置类做如下修改。

@Bean
    @SuppressWarnings(value = { "unchecked", "rawtypes" })
    public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory)
    {
        RedisTemplate template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);



        /*java.lang.ClassCastException:
         * com.alibaba.fastjson.JSONObject cannot be cast to org.springframework.security.core.userdetails.UserDetails
         * */
        String[] acceptNames = {"org.springframework.security.core.authority.SimpleGrantedAuthority"};
        GenericFastJsonRedisSerializer serializer = new GenericFastJsonRedisSerializer(acceptNames);

        // 使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);

        // Hash的key也采用StringRedisSerializer的序列化方式
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);

        template.afterPropertiesSet();
        return template;
    }

        修改完成之后,还是会出现设置属性authorities错误,同样需要对LoginUser做上述修改。

3.发现FastJson反系列的一般问题

        正如上面所说的,同样没有属性username和password怎么不会报错?于是做了一系列测试。发现了在低版本的fastJson中,对应集合类型接口方法中包含较复杂的实现(不是直接显示赋值),反序化可能要求必须有对应的属性。

        定义了一个有不同返回值类型的几种方法来测试。

public interface CrazyDetails {


    List getApps();

    User getUser();

    String getName();


    String[] getNodes();

    Collection getTests();

    List getUsers();
}

        定义一个实现类

@Data
@NoArgsConstructor
@AllArgsConstructor
public class MosulApp implements CrazyDetails{


    @Override
    public User getUser() {
        return new User(this.appInfo.name);
    }

    private AppInfo appInfo;

    @Override
    public List getUsers() {
        List userList = new ArrayList<>();

        for(int i = 0; i < this.appInfo.name.length(); i ++) {
            User user1 = new User("" + i);
            userList.add(user1);
        }


        return userList;
    }

    @Override
    public String[] getNodes() {


        String[] strings = new String[2];
        strings = new String[]{this.appInfo.name,this.appInfo.details};
        return strings;
    }

    /*private List apps;*/

    //报错,添加需要private List tests
    @Override
    public Collection getTests() {

        List list = Arrays.asList(appInfo.name);

        return list;
    }

    @Override
    public List getApps() {
        List objects = new ArrayList<>();

        for(int i = 0; i < this.appInfo.name.length(); i ++) {
            objects.add("tt" + i);
        }

        return objects;
    }

    @Override
    public String getName() {
        return this.appInfo.name;
    }

}

       从Redis反序列化UserDetails对象异常后中发现FastJson序列化的一些问题_第3张图片

        在测试发现fastJson 1.x版本对于Arrays.asList(appInfo.name);反序列化失败,fastJson 2.x版本则可以反序列化成功,但对于UserDetails中Collection getAuthorities()中如果有比较复杂的实现,fastJson 2.x版本反序列化还是会失败。所以为了保险起见,最后在自定义的UserDetails中添加authorities属性,除了这种方法能外,应该也跟自定义的序列化器相关设置有关,需要进行探索。

三、Redis和SpringSecutiry相关配置

        基于上述测试,最终fastJson选用2.0.22版本,最后将redis配置类和SpringSecutiry中UserDetails实现类修改为如下所示。

1.redis配置类

@Configuration
public class RedisConfig {


    /**
     * 指定特定的连接工厂
     * @return
     */
   /*@Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory();
    }*/

    @Bean
    @SuppressWarnings(value = { "unchecked", "rawtypes" })
    public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory)
    {
        RedisTemplate template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

       /* FastJsonRedisSerializer serializer = new FastJsonRedisSerializer(Object.class);*/


        /*解决java.lang.ClassCastException:
         * com.alibaba.fastjson.JSONObject cannot be cast to org.springframework.security.core.userdetails.UserDetails
         * */
        String[] acceptNames = {"org.springframework.security.core.authority.SimpleGrantedAuthority"};
        GenericFastJsonRedisSerializer serializer = new GenericFastJsonRedisSerializer(acceptNames);

        // 使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);

        // Hash的key也采用StringRedisSerializer的序列化方式
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);

        template.afterPropertiesSet();
        return template;
    }
}

2.LoginUser类

@Data
public class LoginUser implements UserDetails {

    private static final long serialVersionUID = 1L;


    // 用户信息
    private SysUser user;

    // 用户权限列表
    private List permissionList;


    // SpringSecurity对应的权限信息
    private List authorities;


    public LoginUser() {
    }

    public LoginUser(SysUser user, List permissionList) {
        this(user,permissionList,null);
    }


    /**
     * 针对fastJson中redis反序列化报错的改进
     * org.springframework.data.redis.serializer.SerializationException:
     * Could not deserialize: set authorities error; nested exception is com.alibaba.fastjson.JSONException: set authorities error
     *
     * @param user
     * @param permissionList
     * @param authorities
     */
    public LoginUser(SysUser user, List permissionList, List authorities) {
        //返回当前用户的权限
        List authoritieList = permissionList.stream()
                .filter(permission -> permission.getPermission() != null)
                .map(permission -> new SimpleGrantedAuthority(permission.getPermission()))
                .collect(Collectors.toList());
        this.user = user;
        this.permissionList = permissionList;
        this.authorities = authoritieList;
    }
    @Override
    public Collection getAuthorities() {
        return this.authorities;

    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

你可能感兴趣的:(redis,java,数据库,SpringSecurity,JWT)