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
|
### 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)
|
* 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.
|
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.
|
* Moves JSON Serializer/Deserializer implementations to a different package name.
|
||||||
- `io.jsonwebtoken.io.JacksonSerializer` -> `io.jsonwebtoken.jackson.io.JacksonSerializer`
|
- `io.jsonwebtoken.io.JacksonSerializer` -> `io.jsonwebtoken.jackson.io.JacksonSerializer`
|
||||||
- `io.jsonwebtoken.io.JacksonDeserializer` -> `io.jsonwebtoken.jackson.io.JacksonDeserializer`
|
- `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
|
@Override //only for better/targeted JavaDoc
|
||||||
Claims setId(String jti);
|
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);
|
<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;
|
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.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.DeserializationException;
|
||||||
import io.jsonwebtoken.io.Deserializer;
|
import io.jsonwebtoken.io.Deserializer;
|
||||||
import io.jsonwebtoken.lang.Assert;
|
import io.jsonwebtoken.lang.Assert;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @since 0.10.0
|
* @since 0.10.0
|
||||||
|
@ -35,6 +42,42 @@ public class JacksonDeserializer<T> implements Deserializer<T> {
|
||||||
this(JacksonSerializer.DEFAULT_OBJECT_MAPPER);
|
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
|
@SuppressWarnings({"unchecked", "WeakerAccess", "unused"}) // for end-users providing a custom ObjectMapper
|
||||||
public JacksonDeserializer(ObjectMapper objectMapper) {
|
public JacksonDeserializer(ObjectMapper objectMapper) {
|
||||||
this(objectMapper, (Class<T>) Object.class);
|
this(objectMapper, (Class<T>) Object.class);
|
||||||
|
@ -60,4 +103,29 @@ public class JacksonDeserializer<T> implements Deserializer<T> {
|
||||||
protected T readValue(byte[] bytes) throws IOException {
|
protected T readValue(byte[] bytes) throws IOException {
|
||||||
return objectMapper.readValue(bytes, returnType);
|
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 com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import io.jsonwebtoken.io.DeserializationException
|
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 io.jsonwebtoken.lang.Strings
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
@ -41,7 +43,7 @@ class JacksonDeserializerTest {
|
||||||
|
|
||||||
@Test(expected = IllegalArgumentException)
|
@Test(expected = IllegalArgumentException)
|
||||||
void testObjectMapperConstructorWithNullArgument() {
|
void testObjectMapperConstructorWithNullArgument() {
|
||||||
new JacksonDeserializer<>(null)
|
new JacksonDeserializer<>((ObjectMapper) null)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -52,6 +54,62 @@ class JacksonDeserializerTest {
|
||||||
assertEquals expected, result
|
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
|
@Test
|
||||||
void testDeserializeFailsWithJsonProcessingException() {
|
void testDeserializeFailsWithJsonProcessingException() {
|
||||||
|
|
||||||
|
@ -78,4 +136,8 @@ class JacksonDeserializerTest {
|
||||||
|
|
||||||
verify ex
|
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.core.JsonProcessingException
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import io.jsonwebtoken.io.SerializationException
|
import io.jsonwebtoken.io.SerializationException
|
||||||
import io.jsonwebtoken.jackson.io.JacksonSerializer
|
|
||||||
import io.jsonwebtoken.lang.Strings
|
import io.jsonwebtoken.lang.Strings
|
||||||
import org.junit.Test
|
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 {
|
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() {
|
public DefaultClaims() {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
@ -158,7 +166,7 @@ public class DefaultClaims extends JwtMap implements Claims {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!requiredType.isInstance(value)) {
|
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);
|
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()
|
fail()
|
||||||
} catch (RequiredTypeException e) {
|
} catch (RequiredTypeException e) {
|
||||||
assertEquals(
|
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()
|
e.getMessage()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -96,7 +96,7 @@ class DefaultClaimsTest {
|
||||||
} catch (RequiredTypeException e) {
|
} catch (RequiredTypeException e) {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
e.getMessage(),
|
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) {
|
} catch (RequiredTypeException e) {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
e.getMessage(),
|
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) {
|
} catch (RequiredTypeException e) {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
e.getMessage(),
|
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) {
|
} catch (RequiredTypeException e) {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
e.getMessage(),
|
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