? ? ? ? 最近在使用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;}
}
? ? ? ? 發現這里改完之后,是可以正常運行的。?
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;}
}