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

栏目: Java · 发布时间: 7年前

内容简介:不知道这个问题有没有人遇到或者处理过,Spring Security OAuth2的tokenStore的redis缓存默认的序列化策略是jdk序列化,这意味着redis里面的值是无法阅读的状态,而且这个缓存也无法被其他语言的web应用所使用,于是就打算使用最常见的json序列化策略来存储。这个问题想处理很久了,虽然现在也能正常使用,但是之前一直没有时间仔细的去研究解决方案,所以今天花了些时间搞定并分享给大家。RedisTokenStore中序列化策略的声明代码如下:

不知道这个问题有没有人遇到或者处理过,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.refreshToken
    if (refreshToken != null) {
      jgen.writeStringField(OAuth2AccessToken.REFRESH_TOKEN, refreshToken.value)
    }
    val expiration = token.expiration
    if (expiration != null) {
      val now = System.currentTimeMillis()
      jgen.writeNumberField(OAuth2AccessToken.EXPIRES_IN, (expiration.time - now) / 1000)
    }
    val scope = token.scope
    if (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.additionalInformation
    for (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 ObjectMapper
    val 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 = tokenType
    if (expiresIn != null) {
      accessToken.expiration = Date(System.currentTimeMillis() + expiresIn * 1000)
    }
    if (refreshToken != null) {
      accessToken.refreshToken = DefaultOAuth2RefreshToken(refreshToken)
    }
    accessToken.scope = scope
    accessToken.additionalInformation = additionalInformation

    return 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.text
      scope = 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? = null
    val mapper = jp.codec as ObjectMapper
    val 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? = null
    if (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


以上所述就是小编给大家介绍的《Spring Security OAuth2 缓存使用jackson序列化的处理》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Algorithms

Algorithms

Robert Sedgewick、Kevin Wayne / Addison-Wesley Professional / 2011-3-19 / USD 89.99

Essential Information about Algorithms and Data Structures A Classic Reference The latest version of Sedgewick,s best-selling series, reflecting an indispensable body of knowledge developed over the ......一起来看看 《Algorithms》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具