The default serialization strategy for Spring Security OAuth2’s tokenStore redis cache is JDK serialization, which means that values in Redis are unreadable. Since this cache cannot be used by web applications in other languages, we intend to store it using the most common JSON serialization strategy.

I have been trying to deal with this problem for a long time. Although it can be used normally now, I have not had time to study the solution carefully before, so I spent some time to solve it and share it with you today.

The serialization policy declaration code in RedisTokenStore is as follows:

private RedisTokenStoreSerializationStrategy serializationStrategy = new JdkSerializationStrategy();
Copy the code

To json serialization RedisTokenStoreSerializationStrategy need to implement the interface, the interface in the source of the Spring does not provide a json serialization strategy implementation, visible Spring official did not support for OAuth2 default json serialization.

Due to project requirements, instead of injecting a new SerializationStrategy into RedisTokenStore, TokenStore was rewritten, essentially the same. Create a GenericJackson2JsonRedisSerializer object in TokenStore, not RedisTokenStoreSerializationStrategy implementation, anyway as long as you can on object serialization and deserialization went, The relevant codes are as follows:

  private val jacksonSerializer = buildSerializer()
  
  private fun buildMapper(a): 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(a): GenericJackson2JsonRedisSerializer {
    return GenericJackson2JsonRedisSerializer(buildMapper())
  }
Copy the code

Think this is OK, too young!

So what happens when you serialize 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)
Copy the code

Let’s take a look at OAuth2AccessToken source code

@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 {...Copy the code

Yes, Spring provides support for Jackson serialization, both 1.x and 2.x. But, why still complains, we look at OAuth2AccessTokenJackson1Serializer do


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

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

The Serializer code was not executed before the Serializer was serialized. Because it’s missing something:

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

If you want to write type information at serialization time, you must override the serializeWithType method

So we need to write the Serializer for OAuth2AccessToken ourselves:

/ * * * * @ author waley * @ 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.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.additionalInformation
    for (key in additionalInformation.keys) {
      jgen.writeObjectField(key, additionalInformation[key])
    }
    jgen.writeEndObject()
  }

}
Copy the code

Deserializer should also be rewritten:

fun JsonNode.readJsonNode(field: String): JsonNode? {
  return if (this.has(field)) {
    this.get(field)
  } else{null}} / * * * * @ author waley * @ 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 = 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.text
      scope = OAuth2Utils.parseParameterList(text)
    }
    return scope
  }

}

Copy the code

But how do you override annotations on the OAuth2AccessToken interface? Create a mixin class using Jackson’s annotation mixin:

/** ** @author Hao * @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
Copy the code

It doesn’t matter if the class is abstract or not, Jackson will just read the annotations on the class

Mapper register mixin classes

mapper.addMixIn(OAuth2AccessToken::class.java, AccessTokenMixIn::class.java)
Copy the code

Can you serialize and deserialize correctly? Yes, you can. However, this is not the end of the story, because TokenStore serializes not only OAuth2AccessToken, but also 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)
Copy the code

OAuth2Authentication cannot be deserialized because there is no default constructor (serialization is possible)

Implement deserializer for OAuth2Authentication

/ * * * * @ author waley * @ 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? = 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
  }

}
Copy the code

With class

/** ** @author Hao * @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
Copy the code

There is no room to cover other issues, but mapper still needs to register two modules, which are provided in the Spring source code

mapper.registerModule(CoreJackson2Module())
mapper.registerModule(WebJackson2Module())
Copy the code

In this way, Jackson can fully serialize OAuth2AccessToken and OAuth2Authentication