最近在使用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<T> implements RedisSerializer<T>
{public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");private Class<T> clazz;static{ParserConfig.getGlobalInstance().setAutoTypeSupport(true);}public FastJsonRedisSerializer(Class<T> clazz){super();this.clazz = clazz;}@Overridepublic byte[] serialize(T t) throws SerializationException{if (t == null){return new byte[0];}return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);}@Overridepublic 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*//*@Beanpublic RedisConnectionFactory redisConnectionFactory() {return new LettuceConnectionFactory();}*/@Bean@SuppressWarnings(value = { "unchecked", "rawtypes" })public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory){RedisTemplate<Object, Object> 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
{@Autowiredpublic RedisTemplate redisTemplate;/*** 缓存基本的对象,Integer、String、实体类等** @param key 缓存的键值* @param value 缓存的值*/public <T> void setCacheObject(final String key, final T value){redisTemplate.opsForValue().set(key, value);}/*** 缓存基本的对象,Integer、String、实体类等** @param key 缓存的键值* @param value 缓存的值* @param timeout 时间* @param timeUnit 时间颗粒度*/public <T> 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> T getCacheObject(final String key){ValueOperations<String, T> 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 <T> long setCacheList(final String key, final List<T> dataList){Long count = redisTemplate.opsForList().rightPushAll(key, dataList);return count == null ? 0 : count;}/*** 获得缓存的list对象** @param key 缓存的键值* @return 缓存键值对应的数据*/public <T> List<T> getCacheList(final String key){return redisTemplate.opsForList().range(key, 0, -1);}/*** 缓存Set** @param key 缓存键值* @param dataSet 缓存的数据* @return 缓存数据的对象*/public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet){BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);Iterator<T> it = dataSet.iterator();while (it.hasNext()){setOperation.add(it.next());}return setOperation;}/*** 获得缓存的set** @param key* @return*/public <T> Set<T> getCacheSet(final String key){return redisTemplate.opsForSet().members(key);}/*** 缓存Map** @param key* @param dataMap*/public <T> void setCacheMap(final String key, final Map<String, T> dataMap){if (dataMap != null) {redisTemplate.opsForHash().putAll(key, dataMap);}}/*** 获得缓存的Map** @param key* @return*/public <T> Map<String, T> getCacheMap(final String key){return redisTemplate.opsForHash().entries(key);}/*** 往Hash中存入数据** @param key Redis键* @param hKey Hash键* @param value 值*/public <T> 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> T getCacheMapValue(final String key, final String hKey){HashOperations<String, String, T> 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 <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys){return redisTemplate.opsForHash().multiGet(key, hKeys);}/*** 获得缓存的基本对象列表** @param pattern 字符串前缀* @return 对象列表*/public Collection<String> 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<SysPermission> permissionList;@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return permissionList.stream().filter(permission -> permission.getPermission() != null).map(permission -> new SimpleGrantedAuthority(permission.getPermission())).collect(Collectors.toList());}@Overridepublic String getPassword() {return user.getPassword();}@Overridepublic String getUsername() {return user.getUsername();}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}
}
5.登录设置
@Overridepublic 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();//设置redisredisHelper.setCacheObject(key,userDetails);//insertLoginLog(username);} catch (AuthenticationException e) {LOGGER.warn("登录异常:{}", e.getMessage());}return token;}@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {SysUser sysUser = sysUserMapper.selectOne(new QueryWrapper<SysUser>().eq("username", username));List<SysPermission> permissionsByUser = sysUserRoleMapper.findPermissionsByUser(sysUser.getUserId());sysUser.setPassword(new BCryptPasswordEncoder().encode(sysUser.getPassword()));// 将系统的用户信息和权限信息封装成UserDetailsUserDetails userDetail = new LoginUser(sysUser, permissionsByUser);return userDetail;}
6.JWT校验
@Overrideprotected 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中获取userDetailsString 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版本
<!--fastjson依赖--><!--第一个版本--><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.47</version></dependency><!--第二个版本--><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>2.0.22</version></dependency>
上面的代码中,先根据用户名获取用户对应的用户信息和权限信息,然后构建SpringSecurity的UserDetails对象,用户登录的时候将这个UserDetails对象放入redis中,后续校验请求携带的token与redis中的信息是否一致。
二、异常信息
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<SysPermission> permissionList;private List<GrantedAuthority> authorities;public LoginUser() {}public LoginUser(SysUser user, List<SysPermission> 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<SysPermission> permissionList, List<GrantedAuthority> authorities) {//返回当前用户的权限List<GrantedAuthority> 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;}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return this.authorities;}@Overridepublic String getPassword() {return user.getPassword();}@Overridepublic String getUsername() {return user.getUsername();}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}
}
发现这里改完之后,是可以正常运行的。 而且发现,当我们给getAuthorities()赋简单的值的时候,不会出现这个问题。
@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {List<GrantedAuthority> authoritieList = new ArrayList<>();authoritieList.add(new SimpleGrantedAuthority("ROLE_ADMIN"));return authoritieList;}
而且我们在构造函数中提前将权限列表加载出来,赋值给一个属性,属性不一定非得名为authorities,如属性名为authoritiesList。然后返回这个属性也不会报错。
@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return this.authoritieList;}
通过上面两个测试可以猜测下,如果构造函数中提前将比较复杂的实现暴露了,反系列化也不会报错。可能出现异常的原因fastjson是对不确定结果无法反系列化,如果是简单的结果或在构造器中就已经知道了确定结果,就不会出现反序列化的异常。
2.版本二报错信息
在使用fastJson 2.x版本的时候,同时需要对redis配置类做如下修改。
@Bean@SuppressWarnings(value = { "unchecked", "rawtypes" })public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory){RedisTemplate<Object, Object> 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<String> getApps();User getUser();String getName();String[] getNodes();Collection<String> getTests();List<User> getUsers();
}
定义一个实现类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MosulApp implements CrazyDetails{@Overridepublic User getUser() {return new User(this.appInfo.name);}private AppInfo appInfo;@Overridepublic List<User> getUsers() {List<User> userList = new ArrayList<>();for(int i = 0; i < this.appInfo.name.length(); i ++) {User user1 = new User("" + i);userList.add(user1);}return userList;}@Overridepublic String[] getNodes() {String[] strings = new String[2];strings = new String[]{this.appInfo.name,this.appInfo.details};return strings;}/*private List<String> apps;*///报错,添加需要private List<String> tests@Overridepublic Collection<String> getTests() {List<String> list = Arrays.asList(appInfo.name);return list;}@Overridepublic List<String> getApps() {List<String> objects = new ArrayList<>();for(int i = 0; i < this.appInfo.name.length(); i ++) {objects.add("tt" + i);}return objects;}@Overridepublic String getName() {return this.appInfo.name;}}
在测试发现fastJson 1.x版本对于Arrays.asList(appInfo.name);反序列化失败,fastJson 2.x版本则可以反序列化成功,但对于UserDetails中Collection<? extends GrantedAuthority> getAuthorities()中如果有比较复杂的实现,fastJson 2.x版本反序列化还是会失败。所以为了保险起见,最后在自定义的UserDetails中添加authorities属性,除了这种方法能外,应该也跟自定义的序列化器相关设置有关,需要进行探索。
三、Redis和SpringSecutiry相关配置
基于上述测试,最终fastJson选用2.0.22版本,最后将redis配置类和SpringSecutiry中UserDetails实现类修改为如下所示。
1.redis配置类
@Configuration
public class RedisConfig {/*** 指定特定的连接工厂* @return*//*@Beanpublic RedisConnectionFactory redisConnectionFactory() {return new LettuceConnectionFactory();}*/@Bean@SuppressWarnings(value = { "unchecked", "rawtypes" })public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory){RedisTemplate<Object, Object> 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<SysPermission> permissionList;// SpringSecurity对应的权限信息private List<GrantedAuthority> authorities;public LoginUser() {}public LoginUser(SysUser user, List<SysPermission> 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<SysPermission> permissionList, List<GrantedAuthority> authorities) {//返回当前用户的权限List<GrantedAuthority> 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;}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return this.authorities;}@Overridepublic String getPassword() {return user.getPassword();}@Overridepublic String getUsername() {return user.getUsername();}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}
}