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.RedisTokenStore
public 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序列化为null
config.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);
}
@Override
public T deserialize(byte[] bytes, Class 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);
}
}
@Override
public String deserializeString(byte[] bytes) {
if (bytes == null || bytes.length == 0) {
return null;
}
return new String(bytes, IOUtils.UTF8);
}
@Override
public 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);
}
}
@Override
public 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
**/
@Override
public Collection extends GrantedAuthority> getAuthorities() {
return authorities;
}
/**
* @param
* @return change notes
* @author suidd
* @description //获取密码
* @date 2020/4/10 13:56
**/
@Override
public String getPassword() {
return password;
}
/**
* @param
* @return change notes
* @author suidd
* @description //获取用户名
* @date 2020/4/10 13:56
**/
@Override
public String getUsername() {
return username;
}
/**
* @param
* @return change notes
* @author suidd
* @description //帐号是否过期
* @date 2020/4/10 13:52
**/
@Override
public boolean isAccountNonExpired() {
return this.accountNonExpired;
}
/**
* @param
* @return change notes
* @author suidd
* @description //帐号是否被锁定
* @date 2020/4/10 13:53
**/
@Override
public boolean isAccountNonLocked() {
return this.accountNonLocked;
}
/**
* @param
* @return change notes
* @author suidd
* @description //认证是否过期
* @date 2020/4/10 13:53
**/
@Override
public boolean isCredentialsNonExpired() {
return this.credentialsNonExpired;
}
/**
* @param
* @return change notes
* @author suidd
* @description //帐号是否被删除
* @date 2020/4/10 13:53
**/
@Override
public 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 {
@Override
public 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;
}
@Override
public 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 {
@Override
public 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模式下,存储类型为PreAuthenticatedAuthenticationToken
Object 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 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 responseTypes = json
.getObject("responseTypes", new TypeReference>() {
});
Set scope = json.getObject("scope", new TypeReference>() {
});
Set authorities = json.getObject("authorities", new TypeReference>() {
});
Set grantedAuthorities = new HashSet<>(0);
if (authorities != null && !authorities.isEmpty()) {
authorities.forEach(s -> grantedAuthorities.add(new SimpleGrantedAuthority(s)));
}
Set resourceIds = json
.getObject("resourceIds", new TypeReference>() {
});
Map extensions = json
.getObject("extensions", new TypeReference>() {
});
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;
}
@Override
public 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
@Autowired
private RedisConnectionFactory redisConnectionFactory;
/**
* redis存储token
* @return
*/
@Bean
public 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.PreAuthenticatedAuthenticationToken
at 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)
......