不知道這個問題有沒有人遇到或者處理過,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