mirror of https://github.com/jwtk/jjwt.git
Add support for custom type deserialization with Jackson (#495)
- Adds new constructor JacksonDeserializer(Map<String, Class> claimTypeMap), which will enable later calls Claims.get("key", CustomType.class) to work as expectd - Adds new Maps utility class to make map creation fluent Fixes: #369
This commit is contained in:
parent
a0060d60f9
commit
7090bf39c3
|
@ -2,8 +2,12 @@
|
|||
|
||||
### 0.11.0
|
||||
|
||||
This minor release:
|
||||
|
||||
* Updates the Jackson dependency version to [2.9.10](https://github.com/FasterXML/jackson/wiki/Jackson-Release-2.9#patches)
|
||||
to address three security vulnerabilities in Jackson.
|
||||
* Adds support for custom types when deserializing with Jackson. To use configure your parser with `Jwts.parserBuilder().deserializeJsonWith(new JacksonDeserializer(Maps.of("claimName", YourType.class).build())).build()`.
|
||||
* Adds `io.jsonwebtoken.lang.Maps` utility class to make creation of maps fluent.
|
||||
* Moves JSON Serializer/Deserializer implementations to a different package name.
|
||||
- `io.jsonwebtoken.io.JacksonSerializer` -> `io.jsonwebtoken.jackson.io.JacksonSerializer`
|
||||
- `io.jsonwebtoken.io.JacksonDeserializer` -> `io.jsonwebtoken.jackson.io.JacksonDeserializer`
|
||||
|
|
|
@ -170,5 +170,22 @@ public interface Claims extends Map<String, Object>, ClaimsMutator<Claims> {
|
|||
@Override //only for better/targeted JavaDoc
|
||||
Claims setId(String jti);
|
||||
|
||||
/**
|
||||
* Returns the JWTs claim ({@code claimName}) value as a type {@code requiredType}, or {@code null} if not present.
|
||||
*
|
||||
* <p>JJWT only converts simple String, Date, Long, Integer, Short and Byte types automatically. Anything more
|
||||
* complex is expected to be already converted to your desired type by the JSON
|
||||
* {@link io.jsonwebtoken.io.Deserializer Deserializer} implementation. You may specify a custom Deserializer for a
|
||||
* JwtParser with the desired conversion configuration via the {@link JwtParserBuilder#deserializeJsonWith} method.
|
||||
* See <a href="https://github.com/jwtk/jjwt#custom-json-processor">custom JSON processor</a></a> for more
|
||||
* information. If using Jackson, you can specify custom claim POJO types as described in
|
||||
* <a href="https://github.com/jwtk/jjwt#json-jackson-custom-types">custom claim types</a>.
|
||||
*
|
||||
* @param claimName name of claim
|
||||
* @param requiredType the type of the value expected to be returned
|
||||
* @param <T> the type of the value expected to be returned
|
||||
* @return the JWT {@code claimName} value or {@code null} if not present.
|
||||
* @throws RequiredTypeException throw if the claim value is not null and not of type {@code requiredType}
|
||||
*/
|
||||
<T> T get(String claimName, Class<T> requiredType);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* Copyright (C) 2019 jsonwebtoken.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.jsonwebtoken.lang;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Utility class to help with the manipulation of working with Maps.
|
||||
* @since 0.11.0
|
||||
*/
|
||||
public final class Maps {
|
||||
|
||||
private Maps() {} //prevent instantiation
|
||||
|
||||
/**
|
||||
* Creates a new map builder with a single entry.
|
||||
* <p> Typical usage: <pre>{@code
|
||||
* Map<K,V> result = Maps.of("key1", value1)
|
||||
* .and("key2", value2)
|
||||
* // ...
|
||||
* .build();
|
||||
* }</pre>
|
||||
* @param key the key of an map entry to be added
|
||||
* @param value the value of map entry to be added
|
||||
* @param <K> the maps key type
|
||||
* @param <V> the maps value type
|
||||
* Creates a new map builder with a single entry.
|
||||
*/
|
||||
public static <K, V> MapBuilder<K, V> of(K key, V value) {
|
||||
return new HashMapBuilder<K, V>().and(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility Builder class for fluently building maps:
|
||||
* <p> Typical usage: <pre>{@code
|
||||
* Map<K,V> result = Maps.of("key1", value1)
|
||||
* .and("key2", value2)
|
||||
* // ...
|
||||
* .build();
|
||||
* }</pre>
|
||||
* @param <K> the maps key type
|
||||
* @param <V> the maps value type
|
||||
*/
|
||||
public interface MapBuilder<K, V> {
|
||||
/**
|
||||
* Add a new entry to this map builder
|
||||
* @param key the key of an map entry to be added
|
||||
* @param value the value of map entry to be added
|
||||
* @return the current MapBuilder to allow for method chaining.
|
||||
*/
|
||||
MapBuilder and(K key, V value);
|
||||
|
||||
/**
|
||||
* Returns a the resulting Map object from this MapBuilder.
|
||||
* @return Returns a the resulting Map object from this MapBuilder.
|
||||
*/
|
||||
Map<K, V> build();
|
||||
}
|
||||
|
||||
private static class HashMapBuilder<K, V> implements MapBuilder<K, V> {
|
||||
|
||||
private final Map<K, V> data = new HashMap<>();
|
||||
|
||||
public MapBuilder and(K key, V value) {
|
||||
data.put(key, value);
|
||||
return this;
|
||||
}
|
||||
public Map<K, V> build() {
|
||||
return Collections.unmodifiableMap(data);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright (C) 2019 jsonwebtoken.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.jsonwebtoken.lang
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat
|
||||
import static org.hamcrest.CoreMatchers.is
|
||||
|
||||
class MapsTest {
|
||||
|
||||
@Test
|
||||
void testSingleMapOf() {
|
||||
assertThat Maps.of("aKey", "aValue").build(), is([aKey: "aValue"])
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMapOfAnd() {
|
||||
assertThat Maps.of("aKey1", "aValue1")
|
||||
.and("aKey2", "aValue2")
|
||||
.and("aKey3", "aValue3")
|
||||
.build(),
|
||||
is([aKey1: "aValue1", aKey2: "aValue2", aKey3: "aValue3"])
|
||||
}
|
||||
}
|
|
@ -15,12 +15,19 @@
|
|||
*/
|
||||
package io.jsonwebtoken.jackson.io;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import com.fasterxml.jackson.databind.deser.std.UntypedObjectDeserializer;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
import io.jsonwebtoken.io.DeserializationException;
|
||||
import io.jsonwebtoken.io.Deserializer;
|
||||
import io.jsonwebtoken.lang.Assert;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @since 0.10.0
|
||||
|
@ -35,6 +42,42 @@ public class JacksonDeserializer<T> implements Deserializer<T> {
|
|||
this(JacksonSerializer.DEFAULT_OBJECT_MAPPER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new JacksonDeserializer where the values of the claims can be parsed into given types. A common usage
|
||||
* example is to parse custom User object out of a claim, for example the claims:
|
||||
* <pre>{@code
|
||||
* {
|
||||
* "issuer": "https://issuer.example.com",
|
||||
* "user": {
|
||||
* "firstName": "Jill",
|
||||
* "lastName": "Coder"
|
||||
* }
|
||||
* }}</pre>
|
||||
* Passing a map of {@code ["user": User.class]} to this constructor would result in the {@code user} claim being
|
||||
* transformed to an instance of your custom {@code User} class, instead of the default of {@code Map}.
|
||||
* <p>
|
||||
* Because custom type parsing requires modifying the state of a Jackson {@code ObjectMapper}, this
|
||||
* constructor creates a new internal {@code ObjectMapper} instance and customizes it to support the
|
||||
* specified {@code claimTypeMap}. This ensures that the JJWT parsing behavior does not unexpectedly
|
||||
* modify the state of another application-specific {@code ObjectMapper}.
|
||||
* <p>
|
||||
* If you would like to use your own {@code ObjectMapper} instance that also supports custom types for
|
||||
* JWT {@code Claims}, you will need to first customize your {@code ObjectMapper} instance by registering
|
||||
* your custom types and then use the {@link JacksonDeserializer(ObjectMapper)} constructor instead.
|
||||
*
|
||||
* @param claimTypeMap The claim name-to-class map used to deserialize claims into the given type
|
||||
*/
|
||||
public JacksonDeserializer(Map<String, Class> claimTypeMap) {
|
||||
// DO NOT reuse JacksonSerializer.DEFAULT_OBJECT_MAPPER as this could result in sharing the custom deserializer
|
||||
// between instances
|
||||
this(new ObjectMapper());
|
||||
Assert.notNull(claimTypeMap, "Claim type map cannot be null.");
|
||||
// register a new Deserializer
|
||||
SimpleModule module = new SimpleModule();
|
||||
module.addDeserializer(Object.class, new MappedTypeDeserializer(Collections.unmodifiableMap(claimTypeMap)));
|
||||
objectMapper.registerModule(module);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked", "WeakerAccess", "unused"}) // for end-users providing a custom ObjectMapper
|
||||
public JacksonDeserializer(ObjectMapper objectMapper) {
|
||||
this(objectMapper, (Class<T>) Object.class);
|
||||
|
@ -60,4 +103,29 @@ public class JacksonDeserializer<T> implements Deserializer<T> {
|
|||
protected T readValue(byte[] bytes) throws IOException {
|
||||
return objectMapper.readValue(bytes, returnType);
|
||||
}
|
||||
|
||||
/**
|
||||
* A Jackson {@link com.fasterxml.jackson.databind.JsonDeserializer JsonDeserializer}, that will convert claim
|
||||
* values to types based on {@code claimTypeMap}.
|
||||
*/
|
||||
private static class MappedTypeDeserializer extends UntypedObjectDeserializer {
|
||||
|
||||
private final Map<String, Class> claimTypeMap;
|
||||
|
||||
private MappedTypeDeserializer(Map<String, Class> claimTypeMap) {
|
||||
super(null, null);
|
||||
this.claimTypeMap = claimTypeMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object deserialize(JsonParser parser, DeserializationContext context) throws IOException {
|
||||
// check if the current claim key is mapped, if so traverse it's value
|
||||
if (claimTypeMap != null && claimTypeMap.containsKey(parser.currentName())) {
|
||||
Class type = claimTypeMap.get(parser.currentName());
|
||||
return parser.readValueAsTree().traverse(parser.getCodec()).readValueAs(type);
|
||||
}
|
||||
// otherwise default to super
|
||||
return super.deserialize(parser, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,9 @@ package io.jsonwebtoken.jackson.io
|
|||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import io.jsonwebtoken.io.DeserializationException
|
||||
import io.jsonwebtoken.jackson.io.JacksonDeserializer
|
||||
import io.jsonwebtoken.io.Encoders
|
||||
import io.jsonwebtoken.jackson.io.stubs.CustomBean
|
||||
import io.jsonwebtoken.lang.Maps
|
||||
import io.jsonwebtoken.lang.Strings
|
||||
import org.junit.Test
|
||||
|
||||
|
@ -41,7 +43,7 @@ class JacksonDeserializerTest {
|
|||
|
||||
@Test(expected = IllegalArgumentException)
|
||||
void testObjectMapperConstructorWithNullArgument() {
|
||||
new JacksonDeserializer<>(null)
|
||||
new JacksonDeserializer<>((ObjectMapper) null)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -52,6 +54,62 @@ class JacksonDeserializerTest {
|
|||
assertEquals expected, result
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeserializeWithCustomObject() {
|
||||
|
||||
long currentTime = System.currentTimeMillis()
|
||||
|
||||
byte[] serialized = """{
|
||||
"oneKey":"oneValue",
|
||||
"custom": {
|
||||
"stringValue": "s-value",
|
||||
"intValue": "11",
|
||||
"dateValue": ${currentTime},
|
||||
"shortValue": 22,
|
||||
"longValue": 33,
|
||||
"byteValue": 15,
|
||||
"byteArrayValue": "${base64('bytes')}",
|
||||
"nestedValue": {
|
||||
"stringValue": "nested-value",
|
||||
"intValue": "111",
|
||||
"dateValue": ${currentTime + 1},
|
||||
"shortValue": 222,
|
||||
"longValue": 333,
|
||||
"byteValue": 10,
|
||||
"byteArrayValue": "${base64('bytes2')}"
|
||||
}
|
||||
}
|
||||
}
|
||||
""".getBytes(Strings.UTF_8)
|
||||
|
||||
CustomBean expectedCustomBean = new CustomBean()
|
||||
.setByteArrayValue("bytes".getBytes("UTF-8"))
|
||||
.setByteValue(0xF as byte)
|
||||
.setDateValue(new Date(currentTime))
|
||||
.setIntValue(11)
|
||||
.setShortValue(22 as short)
|
||||
.setLongValue(33L)
|
||||
.setStringValue("s-value")
|
||||
.setNestedValue(new CustomBean()
|
||||
.setByteArrayValue("bytes2".getBytes("UTF-8"))
|
||||
.setByteValue(0xA as byte)
|
||||
.setDateValue(new Date(currentTime+1))
|
||||
.setIntValue(111)
|
||||
.setShortValue(222 as short)
|
||||
.setLongValue(333L)
|
||||
.setStringValue("nested-value")
|
||||
)
|
||||
|
||||
def expected = [oneKey: "oneValue", custom: expectedCustomBean]
|
||||
def result = new JacksonDeserializer(Maps.of("custom", CustomBean).build()).deserialize(serialized)
|
||||
assertEquals expected, result
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException)
|
||||
void testNullClaimTypeMap() {
|
||||
new JacksonDeserializer((Map) null)
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeserializeFailsWithJsonProcessingException() {
|
||||
|
||||
|
@ -78,4 +136,8 @@ class JacksonDeserializerTest {
|
|||
|
||||
verify ex
|
||||
}
|
||||
|
||||
private String base64(String input) {
|
||||
return Encoders.BASE64.encode(input.getBytes('UTF-8'))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,6 @@ package io.jsonwebtoken.jackson.io
|
|||
import com.fasterxml.jackson.core.JsonProcessingException
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import io.jsonwebtoken.io.SerializationException
|
||||
import io.jsonwebtoken.jackson.io.JacksonSerializer
|
||||
import io.jsonwebtoken.lang.Strings
|
||||
import org.junit.Test
|
||||
|
||||
|
|
|
@ -0,0 +1,146 @@
|
|||
/*
|
||||
* Copyright (C) 2019 jsonwebtoken.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.jsonwebtoken.jackson.io.stubs
|
||||
|
||||
class CustomBean {
|
||||
|
||||
private String stringValue
|
||||
private int intValue
|
||||
private Date dateValue
|
||||
private short shortValue
|
||||
private long longValue
|
||||
private byte byteValue
|
||||
private byte[] byteArrayValue
|
||||
private CustomBean nestedValue
|
||||
|
||||
String getStringValue() {
|
||||
return stringValue
|
||||
}
|
||||
|
||||
CustomBean setStringValue(String stringValue) {
|
||||
this.stringValue = stringValue
|
||||
return this
|
||||
}
|
||||
|
||||
int getIntValue() {
|
||||
return intValue
|
||||
}
|
||||
|
||||
CustomBean setIntValue(int intValue) {
|
||||
this.intValue = intValue
|
||||
return this
|
||||
}
|
||||
|
||||
Date getDateValue() {
|
||||
return dateValue
|
||||
}
|
||||
|
||||
CustomBean setDateValue(Date dateValue) {
|
||||
this.dateValue = dateValue
|
||||
return this
|
||||
}
|
||||
|
||||
short getShortValue() {
|
||||
return shortValue
|
||||
}
|
||||
|
||||
CustomBean setShortValue(short shortValue) {
|
||||
this.shortValue = shortValue
|
||||
return this
|
||||
}
|
||||
|
||||
long getLongValue() {
|
||||
return longValue
|
||||
}
|
||||
|
||||
CustomBean setLongValue(long longValue) {
|
||||
this.longValue = longValue
|
||||
return this
|
||||
}
|
||||
|
||||
byte getByteValue() {
|
||||
return byteValue
|
||||
}
|
||||
|
||||
CustomBean setByteValue(byte byteValue) {
|
||||
this.byteValue = byteValue
|
||||
return this
|
||||
}
|
||||
|
||||
byte[] getByteArrayValue() {
|
||||
return byteArrayValue
|
||||
}
|
||||
|
||||
CustomBean setByteArrayValue(byte[] byteArrayValue) {
|
||||
this.byteArrayValue = byteArrayValue
|
||||
return this
|
||||
}
|
||||
|
||||
CustomBean getNestedValue() {
|
||||
return nestedValue
|
||||
}
|
||||
|
||||
CustomBean setNestedValue(CustomBean nestedValue) {
|
||||
this.nestedValue = nestedValue
|
||||
return this
|
||||
}
|
||||
|
||||
boolean equals(o) {
|
||||
if (this.is(o)) return true
|
||||
if (getClass() != o.class) return false
|
||||
|
||||
CustomBean that = (CustomBean) o
|
||||
|
||||
if (byteValue != that.byteValue) return false
|
||||
if (intValue != that.intValue) return false
|
||||
if (longValue != that.longValue) return false
|
||||
if (shortValue != that.shortValue) return false
|
||||
if (!Arrays.equals(byteArrayValue, that.byteArrayValue)) return false
|
||||
if (dateValue != that.dateValue) return false
|
||||
if (nestedValue != that.nestedValue) return false
|
||||
if (stringValue != that.stringValue) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
int hashCode() {
|
||||
int result
|
||||
result = stringValue.hashCode()
|
||||
result = 31 * result + intValue
|
||||
result = 31 * result + dateValue.hashCode()
|
||||
result = 31 * result + (int) shortValue
|
||||
result = 31 * result + (int) (longValue ^ (longValue >>> 32))
|
||||
result = 31 * result + (int) byteValue
|
||||
result = 31 * result + Arrays.hashCode(byteArrayValue)
|
||||
result = 31 * result + nestedValue.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "CustomBean{" +
|
||||
"stringValue='" + stringValue + '\'' +
|
||||
", intValue=" + intValue +
|
||||
", dateValue=" + dateValue?.time+
|
||||
", shortValue=" + shortValue +
|
||||
", longValue=" + longValue +
|
||||
", byteValue=" + byteValue +
|
||||
// ", byteArrayValue=" + Arrays.toString(byteArrayValue) +
|
||||
", nestedValue=" + nestedValue +
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -22,6 +22,14 @@ import java.util.Map;
|
|||
|
||||
public class DefaultClaims extends JwtMap implements Claims {
|
||||
|
||||
private static final String CONVERSION_ERROR_MSG = "Cannot convert existing claim value of type '%s' to desired type " +
|
||||
"'%s'. JJWT only converts simple String, Date, Long, Integer, Short and Byte types automatically. " +
|
||||
"Anything more complex is expected to be already converted to your desired type by the JSON Deserializer " +
|
||||
"implementation. You may specify a custom Deserializer for a JwtParser with the desired conversion " +
|
||||
"configuration via the JwtParserBuilder.deserializeJsonWith() method. " +
|
||||
"See https://github.com/jwtk/jjwt#custom-json-processor for more information. If using Jackson, you can " +
|
||||
"specify custom claim POJO types as described in https://github.com/jwtk/jjwt#json-jackson-custom-types";
|
||||
|
||||
public DefaultClaims() {
|
||||
super();
|
||||
}
|
||||
|
@ -158,7 +166,7 @@ public class DefaultClaims extends JwtMap implements Claims {
|
|||
}
|
||||
|
||||
if (!requiredType.isInstance(value)) {
|
||||
throw new RequiredTypeException("Expected value to be of type: " + requiredType + ", but was " + value.getClass());
|
||||
throw new RequiredTypeException(String.format(CONVERSION_ERROR_MSG, value.getClass(), requiredType));
|
||||
}
|
||||
|
||||
return requiredType.cast(value);
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* Copyright (C) 2019 jsonwebtoken.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.jsonwebtoken
|
||||
|
||||
import io.jsonwebtoken.io.Deserializer
|
||||
import io.jsonwebtoken.jackson.io.JacksonDeserializer
|
||||
import org.junit.Test
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is
|
||||
import static org.junit.Assert.assertEquals
|
||||
import static org.junit.Assert.assertNotNull
|
||||
import static org.junit.Assert.assertThat
|
||||
|
||||
class CustomObjectDeserializationTest {
|
||||
|
||||
/**
|
||||
* Test parsing without and then with a custom deserializer. Ensures custom type is parsed from claims
|
||||
*/
|
||||
@Test
|
||||
void testCustomObjectDeserialization() {
|
||||
|
||||
CustomBean customBean = new CustomBean()
|
||||
customBean.key1 = "value1"
|
||||
customBean.key2 = 42
|
||||
|
||||
String jwtString = Jwts.builder().claim("cust", customBean).compact()
|
||||
|
||||
// no custom deserialization, object is a map
|
||||
Jwt<Header, Claims> jwt = Jwts.parser().parseClaimsJwt(jwtString)
|
||||
assertNotNull jwt
|
||||
assertEquals jwt.getBody().get('cust'), [key1: 'value1', key2: 42]
|
||||
|
||||
// custom type for 'cust' claim
|
||||
Deserializer deserializer = new JacksonDeserializer([cust: CustomBean])
|
||||
jwt = Jwts.parser().deserializeJsonWith(deserializer).parseClaimsJwt(jwtString)
|
||||
assertNotNull jwt
|
||||
CustomBean result = jwt.getBody().get("cust", CustomBean)
|
||||
assertThat result, is(customBean)
|
||||
}
|
||||
|
||||
static class CustomBean {
|
||||
private String key1
|
||||
private Integer key2
|
||||
|
||||
String getKey1() {
|
||||
return key1
|
||||
}
|
||||
|
||||
void setKey1(String key1) {
|
||||
this.key1 = key1
|
||||
}
|
||||
|
||||
Integer getKey2() {
|
||||
return key2
|
||||
}
|
||||
|
||||
void setKey2(Integer key2) {
|
||||
this.key2 = key2
|
||||
}
|
||||
|
||||
boolean equals(o) {
|
||||
if (this.is(o)) return true
|
||||
if (getClass() != o.class) return false
|
||||
|
||||
CustomBean that = (CustomBean) o
|
||||
|
||||
if (key1 != that.key1) return false
|
||||
if (key2 != that.key2) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
int hashCode() {
|
||||
int result
|
||||
result = (key1 != null ? key1.hashCode() : 0)
|
||||
result = 31 * result + (key2 != null ? key2.hashCode() : 0)
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
|
@ -45,7 +45,7 @@ class DefaultClaimsTest {
|
|||
fail()
|
||||
} catch (RequiredTypeException e) {
|
||||
assertEquals(
|
||||
"Expected value to be of type: class java.lang.String, but was class java.lang.Integer",
|
||||
String.format(DefaultClaims.CONVERSION_ERROR_MSG, 'class java.lang.Integer', 'class java.lang.String'),
|
||||
e.getMessage()
|
||||
)
|
||||
}
|
||||
|
@ -96,7 +96,7 @@ class DefaultClaimsTest {
|
|||
} catch (RequiredTypeException e) {
|
||||
assertEquals(
|
||||
e.getMessage(),
|
||||
"Expected value to be of type: class java.lang.Short, but was class java.lang.Integer"
|
||||
String.format(DefaultClaims.CONVERSION_ERROR_MSG, 'class java.lang.Integer', 'class java.lang.Short')
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -110,7 +110,7 @@ class DefaultClaimsTest {
|
|||
} catch (RequiredTypeException e) {
|
||||
assertEquals(
|
||||
e.getMessage(),
|
||||
"Expected value to be of type: class java.lang.Short, but was class java.lang.Integer"
|
||||
String.format(DefaultClaims.CONVERSION_ERROR_MSG, 'class java.lang.Integer', 'class java.lang.Short')
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -132,7 +132,7 @@ class DefaultClaimsTest {
|
|||
} catch (RequiredTypeException e) {
|
||||
assertEquals(
|
||||
e.getMessage(),
|
||||
"Expected value to be of type: class java.lang.Byte, but was class java.lang.Integer"
|
||||
String.format(DefaultClaims.CONVERSION_ERROR_MSG, 'class java.lang.Integer', 'class java.lang.Byte')
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -146,7 +146,7 @@ class DefaultClaimsTest {
|
|||
} catch (RequiredTypeException e) {
|
||||
assertEquals(
|
||||
e.getMessage(),
|
||||
"Expected value to be of type: class java.lang.Byte, but was class java.lang.Integer"
|
||||
String.format(DefaultClaims.CONVERSION_ERROR_MSG, 'class java.lang.Integer', 'class java.lang.Byte')
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue