RedisTokenStore:Json序列化
一. 前言
Spring Security Oauth2 存储Token的方式有多种, 比如JWT、Jdbc(数据库)、Redis等,但是对于一个大型的分布式服务应用,Redis存储方式应该是最佳选择。
二. 问题
我们使用默认的Redis存储方式,序列化到到Redis的数据是采用JDK序列化策略写入到redis的。这样对于程序的功能毫无影响,但是对于开发者却很不直观,出现问题,也不容易排查,我们能不能把它们序列化成JSON格式呢?
默认序列化方式
Spring Security Oauth2 Redis序列化Token相关的数据是采用JdkSerializationStrategy,具体的代码如下:
//org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStorepublic class RedisTokenStore implements TokenStore {private static final String ACCESS = "access:";private static final String AUTH_TO_ACCESS = "auth_to_access:";private static final String AUTH = "auth:";private static final String REFRESH_AUTH = "refresh_auth:";private static final String ACCESS_TO_REFRESH = "access_to_refresh:";private static final String REFRESH = "refresh:";private static final String REFRESH_TO_ACCESS = "refresh_to_access:";private static final String CLIENT_ID_TO_ACCESS = "client_id_to_access:";private static final String UNAME_TO_ACCESS = "uname_to_access:";private static final boolean springDataRedis_2_0 = ClassUtils.isPresent("org.springframework.data.redis.connection.RedisStandaloneConfiguration",RedisTokenStore.class.getClassLoader());private final RedisConnectionFactory connectionFactory;private AuthenticationKeyGenerator authenticationKeyGenerator = new DefaultAuthenticationKeyGenerator();//Jdk序列方式private RedisTokenStoreSerializationStrategy serializationStrategy = new JdkSerializationStrategy();......省略无关代码
Fastjson序列化策略
使用Fastjson实现了一个序列化策略,并注入到Spring Bean容器中,代码如下:
FastjsonRedisTokenStoreSerializationStrategy 工具类,自定义反序列化设计器和反序列化时需要添加的白名单。
/*** @author suidd* @name FastjsonRedisTokenStoreSerializationStrategy* @description fastjson redis存储json格式序列化反序列化工具类* @date 2020/4/15 9:36* Version 1.0**/
public class FastjsonRedisTokenStoreSerializationStrategy implements RedisTokenStoreSerializationStrategy {private static ParserConfig config = new ParserConfig();static {init();}protected static void init() {//自定义oauth2序列化:DefaultOAuth2RefreshToken 没有setValue方法,会导致JSON序列化为nullconfig.setAutoTypeSupport(true);//开启AutoType//自定义DefaultOauth2RefreshTokenSerializer反序列化config.putDeserializer(DefaultOAuth2RefreshToken.class, new DefaultOauth2RefreshTokenSerializer());//自定义OAuth2Authentication反序列化config.putDeserializer(OAuth2Authentication.class, new OAuth2AuthenticationSerializer());//添加autotype白名单config.addAccept("org.springframework.security.oauth2.provider.");config.addAccept("org.springframework.security.oauth2.provider.client");TypeUtils.addMapping("org.springframework.security.oauth2.provider.OAuth2Authentication",OAuth2Authentication.class);TypeUtils.addMapping("org.springframework.security.oauth2.provider.client.BaseClientDetails",BaseClientDetails.class);config.addAccept("org.springframework.security.oauth2.common.");TypeUtils.addMapping("org.springframework.security.oauth2.common.DefaultOAuth2AccessToken", DefaultOAuth2AccessToken.class);TypeUtils.addMapping("org.springframework.security.oauth2.common.DefaultExpiringOAuth2RefreshToken", DefaultExpiringOAuth2RefreshToken.class);config.addAccept("cn.com.huak.securityoauth.entity");TypeUtils.addMapping("cn.com.huak.securityoauth.entity.UserDetailsEntity", UserDetailsEntity.class);config.addAccept("org.springframework.security.web.authentication.preauth");TypeUtils.addMapping("org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken", PreAuthenticatedAuthenticationToken.class);}@Overridepublic <T> T deserialize(byte[] bytes, Class<T> aClass) {Preconditions.checkArgument(aClass != null,"clazz can't be null");if (bytes == null || bytes.length == 0) {return null;}try {return JSON.parseObject(new String(bytes, IOUtils.UTF8), aClass, config);} catch (Exception ex) {throw new SerializationException("Could not serialize: " + ex.getMessage(), ex);}}@Overridepublic String deserializeString(byte[] bytes) {if (bytes == null || bytes.length == 0) {return null;}return new String(bytes, IOUtils.UTF8);}@Overridepublic byte[] serialize(Object o) {if (o == null) {return new byte[0];}try {return JSON.toJSONBytes(o, SerializerFeature.WriteClassName,SerializerFeature.DisableCircularReferenceDetect);} catch (Exception ex) {throw new SerializationException("Could not serialize: " + ex.getMessage(), ex);}}@Overridepublic byte[] serialize(String data) {if (data == null || data.length() == 0) {return new byte[0];}return data.getBytes(Charset.forName("utf-8"));}
}
自定义UserDetails实体,继承自org.springframework.security.core.userdetails下的UserDetails
/*** @author suidd* @name UserDetailsEntity* @description 用户详情实体类* @date 2020/4/7 17:39* Version 1.0**/
@Data
public class UserDetailsEntity implements UserDetails {private static final long serialVersionUID = 8081363717997957932L;/*** 帐号*/private String id;/*** 用户名*/private String username;/*** 密码*/private String password;/*** 组织ID*/private String orgId;/*** 组织名称*/private String orgName;/*** 用户角色权限*/private Collection<? extends GrantedAuthority> authorities;/*** 帐号是否过期*/private boolean accountNonExpired = true;/*** 认证是否过期*/private boolean credentialsNonExpired = true;/*** 帐号是否锁定*/private boolean accountNonLocked = true;/*** 帐号是否删除*/private boolean enabled = true;/*** 构造** @param id* @param username* @param password* @param orgId* @param orgName* @param authorities*/public UserDetailsEntity(String id, String username, String password, String orgId, String orgName, Collection<? extends GrantedAuthority> authorities) {this.id = id;this.username = username;this.password = password;this.orgId = orgId;this.orgName = orgName;this.authorities = authorities;}/*** @param* @return change notes* @author suidd* @description //获取用户角色权限* @date 2020/4/10 13:55**/@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return authorities;}/*** @param* @return change notes* @author suidd* @description //获取密码* @date 2020/4/10 13:56**/@Overridepublic String getPassword() {return password;}/*** @param* @return change notes* @author suidd* @description //获取用户名* @date 2020/4/10 13:56**/@Overridepublic String getUsername() {return username;}/*** @param* @return change notes* @author suidd* @description //帐号是否过期* @date 2020/4/10 13:52**/@Overridepublic boolean isAccountNonExpired() {return this.accountNonExpired;}/*** @param* @return change notes* @author suidd* @description //帐号是否被锁定* @date 2020/4/10 13:53**/@Overridepublic boolean isAccountNonLocked() {return this.accountNonLocked;}/*** @param* @return change notes* @author suidd* @description //认证是否过期* @date 2020/4/10 13:53**/@Overridepublic boolean isCredentialsNonExpired() {return this.credentialsNonExpired;}/*** @param* @return change notes* @author suidd* @description //帐号是否被删除* @date 2020/4/10 13:53**/@Overridepublic boolean isEnabled() {return this.enabled;}
}
自定义默认的刷新token序列化工具类
/*** @author suidd* @name DefaultOauth2RefreshTokenSerializer* @description 自定义默认的刷新token序列化工具类* @date 2020/4/15 9:45* Version 1.0**/
public class DefaultOauth2RefreshTokenSerializer implements ObjectDeserializer {@Overridepublic <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName) {if (type == DefaultOAuth2RefreshToken.class) {JSONObject jsonObject = parser.parseObject();String tokenId = jsonObject.getString("value");DefaultOAuth2RefreshToken refreshToken = new DefaultOAuth2RefreshToken(tokenId);return (T) refreshToken;}return null;}@Overridepublic int getFastMatchToken() {return 0;}
}
自定义OAuth2认证序列化工具类 ,反序列化时,需要特别注意deserialze方法中我写的注释。
//判断json节点userAuthentication的类型,根据类型动态取值
//UsernamePasswordAuthenticationToken 密码模式/授权码模式下,redis存储的json串类型为UsernamePasswordAuthenticationToken
//PreAuthenticatedAuthenticationToken 刷新token模式下,redis存储的json串类型为PreAuthenticatedAuthenticationToken
/*** @author suidd* @name OAuth2AuthenticationSerializer* @description 自定义OAuth2认证序列化工具类* @date 2020/4/15 9:43* Version 1.0**/
public class OAuth2AuthenticationSerializer implements ObjectDeserializer {@Overridepublic <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName) {if (type == OAuth2Authentication.class) {try {Object o = parse(parser);if (o == null) {return null;} else if (o instanceof OAuth2Authentication) {return (T) o;}JSONObject jsonObject = (JSONObject) o;OAuth2Request request = parseOAuth2Request(jsonObject);//判断json节点userAuthentication的类型,根据类型动态取值//UsernamePasswordAuthenticationToken 密码模式/授权码模式下,存储类型为UsernamePasswordAuthenticationToken//PreAuthenticatedAuthenticationToken 刷新token模式下,存储类型为PreAuthenticatedAuthenticationTokenObject autoType = jsonObject.get("userAuthentication");return (T) new OAuth2Authentication(request, jsonObject.getObject("userAuthentication", (Type) autoType.getClass()));} catch (Exception e) {e.printStackTrace();}return null;}return null;}private OAuth2Request parseOAuth2Request(JSONObject jsonObject) {JSONObject json = jsonObject.getObject("oAuth2Request", JSONObject.class);Map<String, String> requestParameters = json.getObject("requestParameters", Map.class);String clientId = json.getString("clientId");String grantType = json.getString("grantType");String redirectUri = json.getString("redirectUri");Boolean approved = json.getBoolean("approved");Set<String> responseTypes = json.getObject("responseTypes", new TypeReference<HashSet<String>>() {});Set<String> scope = json.getObject("scope", new TypeReference<HashSet<String>>() {});Set<String> authorities = json.getObject("authorities", new TypeReference<HashSet<String>>() {});Set<GrantedAuthority> grantedAuthorities = new HashSet<>(0);if (authorities != null && !authorities.isEmpty()) {authorities.forEach(s -> grantedAuthorities.add(new SimpleGrantedAuthority(s)));}Set<String> resourceIds = json.getObject("resourceIds", new TypeReference<HashSet<String>>() {});Map<String, Serializable> extensions = json.getObject("extensions", new TypeReference<HashMap<String, Serializable>>() {});OAuth2Request request = new OAuth2Request(requestParameters, clientId,grantedAuthorities, approved, scope, resourceIds, redirectUri, responseTypes, extensions);TokenRequest tokenRequest = new TokenRequest(requestParameters, clientId, scope, grantType);request.refresh(tokenRequest);return request;}@Overridepublic int getFastMatchToken() {return 0;}private Object parse(DefaultJSONParser parse) {JSONObject object = new JSONObject(parse.lexer.isEnabled(Feature.OrderedField));Object parsedObject = parse.parseObject((Map) object);if (parsedObject instanceof JSONObject) {return (JSONObject) parsedObject;} else if (parsedObject instanceof OAuth2Authentication) {return parsedObject;} else {return parsedObject == null ? null : new JSONObject((Map) parsedObject);}}
}
在认证核心配置类中,修改token存储方式为redis
@Autowiredprivate RedisConnectionFactory redisConnectionFactory;/*** redis存储token* @return*/@Beanpublic RedisTokenStore redisTokenStore(){RedisTokenStore store = new RedisTokenStore(redisConnectionFactory);//自定义json进行序列化和反序列化store.setSerializationStrategy(new FastjsonRedisTokenStoreSerializationStrategy());return store;}
至此,RedisTokenStore存储在redis为json格式就大功告成了,如果反序列化时报错,那请参照错误信息,进行修改。
我这里只列举因为未添加白名单导致报错的错误代码:
com.alibaba.fastjson.JSONException: autoType is not support. org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationTokenat com.alibaba.fastjson.parser.ParserConfig.checkAutoType(ParserConfig.java:1072)at com.alibaba.fastjson.parser.DefaultJSONParser.parseObject(DefaultJSONParser.java:327)at com.alibaba.fastjson.parser.DefaultJSONParser.parseObject(DefaultJSONParser.java:569)at com.alibaba.fastjson.parser.DefaultJSONParser.parseObject(DefaultJSONParser.java:1129)at cn.com.huak.securityoauth.common.OAuth2AuthenticationSerializer.parse(OAuth2AuthenticationSerializer.java:95)at cn.com.huak.securityoauth.common.OAuth2AuthenticationSerializer.deserialze(OAuth2AuthenticationSerializer.java:35)
......