Кэш Spring Security OAuth2 использует обработку сериализации Джексона

Redis задняя часть Spring Безопасность

Я не знаю, сталкивался ли кто-нибудь с этой проблемой или сталкивался с ней.Стратегия сериализации по умолчанию кеша redis tokenStore of Spring Security OAuth2 — это сериализация jdk, что означает, что значение в redis не читается, а к кешу нельзя получить доступ , Используется веб-приложениями на других языках, поэтому я планирую использовать наиболее распространенную стратегию сериализации json для хранения.

Я давно хотел разобраться с этой проблемой.Хотя сейчас ее можно нормально использовать, у меня не было времени внимательно изучить решение, поэтому сегодня я потратил некоторое время, чтобы решить ее и поделиться ею с вами.

Код объявления стратегии сериализации в RedisTokenStore выглядит следующим образом:

private RedisTokenStoreSerializationStrategy serializationStrategy = new JdkSerializationStrategy();

Чтобы перейти на сериализацию json, вам нужно реализовать интерфейсRedisTokenStoreSerializationStrategy, этот интерфейс не обеспечивает реализацию стратегии сериализации json в исходном коде Spring, видно, что Spring официально не поддерживает сериализацию json для OAuth2 по умолчанию.

В связи с потребностями проекта новый SerializationStrategy не внедрялся в RedisTokenStore, а был переписан TokenStore, что по сути то же самое. Создание объекта GenericJackson2JsonRedisSerializer в TokenStore не является реализацией 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())
  }

Как вы думаете, это нормально, слишком молод!

Давайте посмотрим, что происходит при сериализации 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 поддерживает сериализацию Джексона, и она есть как в 1.x, так и в 2.x. Но почему он все еще сообщает об ошибке, давайте посмотрим, что делает OAuth2AccessTokenJackson1Serializer.


public OAuth2AccessTokenJackson1Serializer() {
    super(OAuth2AccessToken.class);
}

@Override
public void serialize(OAuth2AccessToken token, JsonGenerator jgen, SerializerProvider provider) throws IOException,
	JsonGenerationException {
...

Код этого сериализатора не выполнялся в предыдущем отчете об ошибке, то есть перед сериализацией было сообщено об ошибке, почему так? Потому что чего-то не хватает:

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

Если вы хотите записать информацию о типе при сериализации, вы должны перегрузитьserializeWithTypeметод

Итак, нам нужно написать собственный сериализатор для OAuth2AccessToken:

/**
 *
 * @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()
  }

}

Также переписан десериализатор для десериализации:

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? используя ДжексонаАннотационный миксин, создайте класс миксина:

/**
 *
 * @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

Неважно, является ли этот класс абстрактным или нет, Джексон будет читать только аннотации к классу.

Регистрация классов примесей в маппере

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.

/**
 *
 * @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

Из-за нехватки места я не буду слишком много говорить о других вопросах, следует отметить, что маппер все еще должен зарегистрировать два модуля, которые предоставляются в исходном коде Spring.

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

Таким образом, Джексон может полностью правильно сериализовать OAuth2AccessToken и OAuth2Authentication.