Spring Security OAuth2 缓存使用jackson序列化的处理

news/2024/11/22 19:57:07/

不知道这个问题有没有人遇到或者处理过,Spring Security OAuth2的tokenStore的redis缓存默认的序列化策略是jdk序列化,这意味着redis里面的值是无法阅读的状态,而且这个缓存也无法被其他语言的web应用所使用,于是就打算使用最常见的json序列化策略来存储。

这个问题想处理很久了,虽然现在也能正常使用,但是之前一直没有时间仔细的去研究解决方案,所以今天花了些时间搞定并分享给大家。

RedisTokenStore中序列化策略的声明代码如下:

private RedisTokenStoreSerializationStrategy serializationStrategy = new JdkSerializationStrategy(); 

改为json序列化需要实现接口 RedisTokenStoreSerializationStrategy,该接口在Spring的源码中并没有提供json序列化策略的实现,可见Spring官方并没有对OAuth2默认支持json序列化。

由于项目需要,并没有在RedisTokenStore中注入新的SerializationStrategy,而是重写了TokenStore,本质是没有区别的。 在TokenStore中创建一个GenericJackson2JsonRedisSerializer对象,并不是RedisTokenStoreSerializationStrategy的实现,反正只要能对对象进行序列化和反序列化就行了,相关代码如下:

 private val jacksonSerializer = buildSerializer()private fun buildMapper(): ObjectMapper {val mapper = createObjectMapper()mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY)mapper.disable(MapperFeature.AUTO_DETECT_SETTERS)mapper.registerModule(CoreJackson2Module())mapper.registerModule(WebJackson2Module())return mapper}private fun buildSerializer(): GenericJackson2JsonRedisSerializer {return GenericJackson2JsonRedisSerializer(buildMapper())} 

以为这样就OK了吗,too young!

来看一下对 OAuth2AccessToken 进行序列化的时候发生了什么

org.springframework.data.redis.serializer.SerializationException: Could not write JSON: Type id handling not implemented for type org.springframework.security.oauth2.common.OAuth2AccessToken (by serializer of type org.springframework.security.oauth2.common.OAuth2AccessTokenJackson2Serializer); nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Type id handling not implemented for type org.springframework.security.oauth2.common.OAuth2AccessToken (by serializer of type org.springframework.security.oauth2.common.OAuth2AccessTokenJackson2Serializer) 

我们再来看看 OAuth2AccessToken 的源码

@org.codehaus.jackson.map.annotate.JsonSerialize(using = OAuth2AccessTokenJackson1Serializer.class)
@org.codehaus.jackson.map.annotate.JsonDeserialize(using = OAuth2AccessTokenJackson1Deserializer.class)
@com.fasterxml.jackson.databind.annotation.JsonSerialize(using = OAuth2AccessTokenJackson2Serializer.class)
@com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = OAuth2AccessTokenJackson2Deserializer.class)public interface OAuth2AccessToken {
…… 

没错,Spring提供了对jackson序列化的支持,而且1.x和2.x都有。But,为什么还是会报错呢,我们来看一下 OAuth2AccessTokenJackson1Serializer 做了什么

 public OAuth2AccessTokenJackson1Serializer() {super(OAuth2AccessToken.class);
}@Override
public void serialize(OAuth2AccessToken token, JsonGenerator jgen, SerializerProvider provider) throws IOException,JsonGenerationException {
... 

这个Serializer的代码在刚才的报错中并没有执行,也就是说在序列化之前就报错了,这是为什么呢?因为它缺了点东西:

override fun serializeWithType(token: OAuth2AccessToken, jgen: JsonGenerator, serializers: SerializerProvider, typeSer: TypeSerializer?) {ser(token, jgen, serializers, typeSer)
} 

如果要在序列化时写入类型信息,必须要重载 serializeWithType 方法

所以我们需要自己写OAuth2AccessToken的Serializer:

/**** @author 吴昊* @since 2.2.1*/
class AccessTokenJackson2Serializer : StdSerializer<OAuth2AccessToken>(OAuth2AccessToken::class.java) {@Throws(IOException::class)override fun serialize(token: OAuth2AccessToken, jgen: JsonGenerator, provider: SerializerProvider) {ser(token, jgen, provider, null)}override fun serializeWithType(token: OAuth2AccessToken, jgen: JsonGenerator, serializers: SerializerProvider, typeSer: TypeSerializer?) {ser(token, jgen, serializers, typeSer)}private fun ser(token: OAuth2AccessToken, jgen: JsonGenerator, provider: SerializerProvider, typeSer: TypeSerializer?) {jgen.writeStartObject()if (typeSer != null) {jgen.writeStringField(typeSer.propertyName, token::class.java.name)}jgen.writeStringField(OAuth2AccessToken.ACCESS_TOKEN, token.value)jgen.writeStringField(OAuth2AccessToken.TOKEN_TYPE, token.tokenType)val refreshToken = token.refreshTokenif (refreshToken != null) {jgen.writeStringField(OAuth2AccessToken.REFRESH_TOKEN, refreshToken.value)}val expiration = token.expirationif (expiration != null) {val now = System.currentTimeMillis()jgen.writeNumberField(OAuth2AccessToken.EXPIRES_IN, (expiration.time - now) / 1000)}val scope = token.scopeif (scope != null && !scope.isEmpty()) {val scopes = StringBuffer()for (s in scope) {Assert.hasLength(s, "Scopes cannot be null or empty. Got $scope")scopes.append(s)scopes.append(" ")}jgen.writeStringField(OAuth2AccessToken.SCOPE, scopes.substring(0, scopes.length - 1))}val additionalInformation = token.additionalInformationfor (key in additionalInformation.keys) {jgen.writeObjectField(key, additionalInformation[key])}jgen.writeEndObject()}} 

反序列化的Deserializer也要重写:

fun JsonNode.readJsonNode(field: String): JsonNode? {return if (this.has(field)) {this.get(field)} else {null}
}/**** @author 吴昊* @since 2.2.1*/
class AccessTokenJackson2Deserializer : StdDeserializer<OAuth2AccessToken>(OAuth2AccessToken::class.java) {@Throws(IOException::class, JsonProcessingException::class)override fun deserialize(jp: JsonParser, ctxt: DeserializationContext): OAuth2AccessToken {val additionalInformation = LinkedHashMap<String, Any>()val mapper = jp.codec as ObjectMapperval jsonNode = mapper.readTree<JsonNode>(jp)val tokenValue: String? = jsonNode.readJsonNode(ACCESS_TOKEN)?.asText()val tokenType: String? = jsonNode.readJsonNode(TOKEN_TYPE)?.asText()val refreshToken: String? = jsonNode.readJsonNode(REFRESH_TOKEN)?.asText()val expiresIn: Long? = jsonNode.readJsonNode(EXPIRES_IN)?.asLong()val scopeNode = jsonNode.readJsonNode(SCOPE)val scope: Set<String>? = if (scopeNode != null) {if (scopeNode.isArray) {scopeNode.map {it.asText()}.toSet()} else {OAuth2Utils.parseParameterList(scopeNode.asText())}} else {null}jsonNode.fieldNames().asSequence().filter {it !in listOf(ACCESS_TOKEN, TOKEN_TYPE, REFRESH_TOKEN, EXPIRES_IN, SCOPE)}.forEach { name ->additionalInformation[name] = mapper.readValue(jsonNode.get(name).traverse(mapper),Any::class.java)}// TODO What should occur if a required parameter (tokenValue or tokenType) is missing?val accessToken = DefaultOAuth2AccessToken(tokenValue)accessToken.tokenType = tokenTypeif (expiresIn != null) {accessToken.expiration = Date(System.currentTimeMillis() + expiresIn * 1000)}if (refreshToken != null) {accessToken.refreshToken = DefaultOAuth2RefreshToken(refreshToken)}accessToken.scope = scopeaccessToken.additionalInformation = additionalInformationreturn accessToken}override fun deserializeWithType(jp: JsonParser, ctxt: DeserializationContext, typeDeserializer: TypeDeserializer?): Any {return des(jp, ctxt, typeDeserializer)}private fun des(jp: JsonParser, ctxt: DeserializationContext, typeDeserializer: TypeDeserializer?): DefaultOAuth2AccessToken {return des(jp, ctxt, typeDeserializer)}@Throws(JsonParseException::class, IOException::class)private fun parseScope(jp: JsonParser): Set<String> {val scope: MutableSet<String>if (jp.currentToken == JsonToken.START_ARRAY) {scope = TreeSet()while (jp.nextToken() != JsonToken.END_ARRAY) {scope.add(jp.valueAsString)}} else {val text = jp.textscope = OAuth2Utils.parseParameterList(text)}return scope}} 

但是,如何覆盖OAuth2AccessToken接口上的注解呢?使用jackson的注解混入,创建混入类:

/**** @author 吴昊* @since 2.2.1*/
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, property = "@class")
@com.fasterxml.jackson.databind.annotation.JsonSerialize(using = AccessTokenJackson2Serializer::class)
@com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = AccessTokenJackson2Deserializer::class)
abstract class AccessTokenMixIn 

这个类是abstract抑或不是并没有什么关系,jackson只会读取类上的注解

mapper中注册混入类

mapper.addMixIn(OAuth2AccessToken::class.java, AccessTokenMixIn::class.java) 

可以正确序列化和反序列化了吗,是的,可以了。但是,还没有结束,因为TokenStore中不仅要序列化OAuth2AccessToken,还要序列化OAuth2Authentication: 看一下错误:

org.springframework.data.redis.serializer.SerializationException: Could not read JSON: Cannot construct instance of `org.springframework.security.oauth2.provider.OAuth2Authentication` (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator) 

OAuth2Authentication 因为没有默认构造函数,不能反序列化(序列化是可以的)

实现OAuth2Authentication的deserializer

/**** @author 吴昊* @since 2.2.1*/
class OAuth2AuthenticationDeserializer : JsonDeserializer<OAuth2Authentication>() {@Throws(IOException::class, JsonProcessingException::class)override fun deserialize(jp: JsonParser, ctxt: DeserializationContext): OAuth2Authentication {var token: OAuth2Authentication? = nullval mapper = jp.codec as ObjectMapperval jsonNode = mapper.readTree<JsonNode>(jp)val requestNode = jsonNode.readJsonNode("storedRequest")val userAuthenticationNode = jsonNode.readJsonNode("userAuthentication")val request = mapper.readValue(requestNode!!.traverse(mapper), OAuth2Request::class.java)var auth: Authentication? = nullif (userAuthenticationNode != null && userAuthenticationNode !is MissingNode) {auth = mapper.readValue(userAuthenticationNode.traverse(mapper),UsernamePasswordAuthenticationToken::class.java)}token = OAuth2Authentication(request, auth)val detailsNode = jsonNode.readJsonNode("details")if (detailsNode != null && detailsNode !is MissingNode) {token.details = mapper.readValue(detailsNode.traverse(mapper), OAuth2AuthenticationDetails::class.java)}return token}} 

混入类

/**** @author 吴昊* @since 2.2.1*/
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, property = "@class")
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE)
@JsonDeserialize(using = OAuth2AuthenticationDeserializer::class)
internal abstract class OAuth2AuthenticationMixin 

限于篇幅,不再过多的讲述其他问题,需要注意的是,mapper还是需要注册两个module,是Spring源码中提供的

mapper.registerModule(CoreJackson2Module())
mapper.registerModule(WebJackson2Module()) 

这样jackson才能完全正确的序列化 OAuth2AccessToken 和 OAuth2Authentication


http://www.ppmy.cn/news/12144.html

相关文章

Open3D 曲面重建(Python版本)

文章目录 一、Alpha Shape算法二、滚球法三、泊松曲面重建参考资料一、Alpha Shape算法 在三维层面上来讲,该算法我们可以想象为一个球在一堆点集中进行滚动,符合条件的三个点即会构成一个多边形,这个条件在我看来是一种“空球法则”(类似于空圆法则),也就是说这个球除三…

[ECE]模拟试题-1

有一个索引task2&#xff0c;有field2字段&#xff0c;用match匹配the能查到很多数据&#xff0c;现在要求对task2索引进行重建&#xff0c;重建后的索引叫new_task2&#xff0c;然后match匹配the查不到数据 自定义分词&#xff1a; DELETE /task2 DELETE /new_task2 PUT tas…

Vue过滤器

Vue过滤器1. 概述2. 全局过滤器与局部过滤器2.1 过滤器参数2.2 过滤器的串联1. 概述 在Vue.js中&#xff0c;过滤器主要用于文本的格式化&#xff0c;或者组件数据的过滤与排序等。从Vue2.0.0版本开始&#xff0c;内置的过滤器已经被删除&#xff0c;需要自己编写。 2. 全局过…

ES6学习笔记

ECMAScript6学习笔记 ECMAScript 和 JavaScript 的关系&#xff0c;前者是后者的规格&#xff0c;后者是前者的一种实现。ECMAScript 的其他方言还有如 Jscript 和 ActionScript。ES6相对之前的版本语法更严格&#xff0c;新增了面向对象的很多特性以及一些高级特性。 1.let声…

糖果(差分约束+找最小值)

糖果 题目描述 幼儿园里有 n 个小朋友&#xff0c; lxhgww 老师现在想要给这些小朋友们分配糖果&#xff0c;要求每个小朋友都要分到糖果。 但是小朋友们也有嫉妒心&#xff0c;总是会提出一些要求&#xff0c;比如小明不希望小红分到的糖果比他的多&#xff0c;于是在分配糖果…

Simulink 自动代码生成电机控制:关于无传感控制开环启动控制的仿真和开发板运行

目录 开环启动原理 开环启动建模实现 开环启动仿真 代码生成和验证 总结 开环启动原理 永磁同步电机开环三步启动是比较传统也是比较常用的启动方式&#xff0c;典型的启动有&#xff1a; 对齐&#xff1a;也说是说的转子预定位&#xff0c;就是通过手动给定一个初始角度…

javascript模块那些事儿:commonJS和ES module

前言 模块定义&#xff0c;包管理&#xff0c;以及加载问题是所有编程语言不得不面临的问题&#xff0c;死生存亡之地&#xff0c;不可不察也。 什么是一个模块&#xff1f; 一个模块就是一个js/ts文件&#xff0c;可以定义函数、类、数据&#xff0c;并export出来让外部可见…

分布式文件系统

常见的文件系统&#xff1a;FAT16/FAT32、NTFS、HFS、UFS、APFS、XFS、Ext4等 。 通过概念可以简单理解为&#xff1a;一个计算机无法存储海量的文件&#xff0c;通过网络将若干计算机组织起来共同去存储海量的文件&#xff0c;去接收海量用户的请求&#xff0c;这些组织起来的…