Merge pull request #20 from CodeCraftersCN/dev/v1.2.0
Upgrade to v1.2.0.
This commit is contained in:
+1
-1
@@ -23,7 +23,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>cn.org.codecrafters</groupId>
|
<groupId>cn.org.codecrafters</groupId>
|
||||||
<artifactId>jdevkit</artifactId>
|
<artifactId>jdevkit</artifactId>
|
||||||
<version>1.1.2</version>
|
<version>1.2.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>devkit-core</artifactId>
|
<artifactId>devkit-core</artifactId>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>cn.org.codecrafters</groupId>
|
<groupId>cn.org.codecrafters</groupId>
|
||||||
<artifactId>jdevkit</artifactId>
|
<artifactId>jdevkit</artifactId>
|
||||||
<version>1.1.2</version>
|
<version>1.2.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>devkit-utils</artifactId>
|
<artifactId>devkit-utils</artifactId>
|
||||||
|
|||||||
+1
-1
@@ -23,7 +23,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>cn.org.codecrafters</groupId>
|
<groupId>cn.org.codecrafters</groupId>
|
||||||
<artifactId>jdevkit</artifactId>
|
<artifactId>jdevkit</artifactId>
|
||||||
<version>1.1.2</version>
|
<version>1.2.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>guid</artifactId>
|
<artifactId>guid</artifactId>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
|
|
||||||
<groupId>cn.org.codecrafters</groupId>
|
<groupId>cn.org.codecrafters</groupId>
|
||||||
<artifactId>jdevkit</artifactId>
|
<artifactId>jdevkit</artifactId>
|
||||||
<version>1.1.2</version>
|
<version>1.2.0</version>
|
||||||
<inceptionYear>2023</inceptionYear>
|
<inceptionYear>2023</inceptionYear>
|
||||||
|
|
||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>cn.org.codecrafters</groupId>
|
<groupId>cn.org.codecrafters</groupId>
|
||||||
<artifactId>jdevkit</artifactId>
|
<artifactId>jdevkit</artifactId>
|
||||||
<version>1.1.2</version>
|
<version>1.2.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>property-guard-spring-boot-starter</artifactId>
|
<artifactId>property-guard-spring-boot-starter</artifactId>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>cn.org.codecrafters</groupId>
|
<groupId>cn.org.codecrafters</groupId>
|
||||||
<artifactId>jdevkit</artifactId>
|
<artifactId>jdevkit</artifactId>
|
||||||
<version>1.1.2</version>
|
<version>1.2.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>simple-jwt-authzero</artifactId>
|
<artifactId>simple-jwt-authzero</artifactId>
|
||||||
@@ -40,6 +40,11 @@
|
|||||||
<artifactId>simple-jwt-facade</artifactId>
|
<artifactId>simple-jwt-facade</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.auth0</groupId>
|
<groupId>com.auth0</groupId>
|
||||||
<artifactId>java-jwt</artifactId>
|
<artifactId>java-jwt</artifactId>
|
||||||
|
|||||||
+115
-68
@@ -17,21 +17,32 @@
|
|||||||
|
|
||||||
package cn.org.codecrafters.simplejwt.authzero;
|
package cn.org.codecrafters.simplejwt.authzero;
|
||||||
|
|
||||||
|
import cn.org.codecrafters.devkit.utils.Base64Util;
|
||||||
import cn.org.codecrafters.guid.GuidCreator;
|
import cn.org.codecrafters.guid.GuidCreator;
|
||||||
import cn.org.codecrafters.simplejwt.SecretCreator;
|
import cn.org.codecrafters.simplejwt.SecretCreator;
|
||||||
import cn.org.codecrafters.simplejwt.TokenPayload;
|
import cn.org.codecrafters.simplejwt.TokenPayload;
|
||||||
import cn.org.codecrafters.simplejwt.TokenResolver;
|
import cn.org.codecrafters.simplejwt.TokenResolver;
|
||||||
import cn.org.codecrafters.simplejwt.annotations.ExcludeFromPayload;
|
import cn.org.codecrafters.simplejwt.annotations.ExcludeFromPayload;
|
||||||
|
import cn.org.codecrafters.simplejwt.annotations.TokenEnum;
|
||||||
import cn.org.codecrafters.simplejwt.authzero.config.AuthzeroTokenResolverConfig;
|
import cn.org.codecrafters.simplejwt.authzero.config.AuthzeroTokenResolverConfig;
|
||||||
import cn.org.codecrafters.simplejwt.config.TokenResolverConfig;
|
import cn.org.codecrafters.simplejwt.config.TokenResolverConfig;
|
||||||
|
import cn.org.codecrafters.simplejwt.constants.PredefinedKeys;
|
||||||
import cn.org.codecrafters.simplejwt.constants.TokenAlgorithm;
|
import cn.org.codecrafters.simplejwt.constants.TokenAlgorithm;
|
||||||
import com.auth0.jwt.JWT;
|
import com.auth0.jwt.JWT;
|
||||||
import com.auth0.jwt.JWTCreator;
|
import com.auth0.jwt.JWTCreator;
|
||||||
import com.auth0.jwt.algorithms.Algorithm;
|
import com.auth0.jwt.algorithms.Algorithm;
|
||||||
|
import com.auth0.jwt.interfaces.Claim;
|
||||||
import com.auth0.jwt.interfaces.DecodedJWT;
|
import com.auth0.jwt.interfaces.DecodedJWT;
|
||||||
import com.auth0.jwt.interfaces.JWTVerifier;
|
import com.auth0.jwt.interfaces.JWTVerifier;
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.JsonMappingException;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
@@ -114,21 +125,27 @@ public class AuthzeroTokenResolver implements TokenResolver<DecodedJWT> {
|
|||||||
*/
|
*/
|
||||||
private final JWTVerifier verifier;
|
private final JWTVerifier verifier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jackson JSON handler.
|
||||||
|
*/
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
private final AuthzeroTokenResolverConfig config = AuthzeroTokenResolverConfig.getInstance();
|
private final AuthzeroTokenResolverConfig config = AuthzeroTokenResolverConfig.getInstance();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new instance of {@code AuthzeroTokenResolver} with the
|
* Creates a new instance of {@code AuthzeroTokenResolver} with the
|
||||||
* provided configurations.
|
* provided configurations.
|
||||||
*
|
*
|
||||||
* @param jtiCreator the {@link GuidCreator} used for generating unique
|
* @param jtiCreator the {@link GuidCreator} used for generating unique
|
||||||
* identifiers for "jti" claim in JWT tokens
|
* identifiers for "jti" claim in JWT tokens
|
||||||
* @param algorithm the algorithm used for signing and verifying JWT
|
* @param algorithm the algorithm used for signing and verifying JWT
|
||||||
* tokens
|
* tokens
|
||||||
* @param issuer the issuer claim value to be included in JWT tokens
|
* @param issuer the issuer claim value to be included in JWT tokens
|
||||||
* @param secret the secret used for HMAC-based algorithms (HS256,
|
* @param secret the secret used for HMAC-based algorithms (HS256,
|
||||||
* HS384, HS512) for token signing and verification
|
* HS384, HS512) for token signing and verification
|
||||||
|
* @param objectMapper JSON handler
|
||||||
*/
|
*/
|
||||||
public AuthzeroTokenResolver(GuidCreator<?> jtiCreator, TokenAlgorithm algorithm, String issuer, String secret) {
|
public AuthzeroTokenResolver(GuidCreator<?> jtiCreator, TokenAlgorithm algorithm, String issuer, String secret, ObjectMapper objectMapper) {
|
||||||
if (secret == null || secret.isBlank()) {
|
if (secret == null || secret.isBlank()) {
|
||||||
throw new IllegalArgumentException("A secret is required to build a JSON Web Token.");
|
throw new IllegalArgumentException("A secret is required to build a JSON Web Token.");
|
||||||
}
|
}
|
||||||
@@ -143,6 +160,21 @@ public class AuthzeroTokenResolver implements TokenResolver<DecodedJWT> {
|
|||||||
.apply(secret);
|
.apply(secret);
|
||||||
this.issuer = issuer;
|
this.issuer = issuer;
|
||||||
this.verifier = JWT.require(this.algorithm).build();
|
this.verifier = JWT.require(this.algorithm).build();
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance of {@link AuthzeroTokenResolver} with the
|
||||||
|
* provided configurations and a simple UUID GuidCreator.
|
||||||
|
*
|
||||||
|
* @param algorithm the algorithm used for signing and verifying JWT tokens
|
||||||
|
* @param issuer the issuer claim value to be included in JWT tokens
|
||||||
|
* @param secret the secret used for HMAC-based algorithms (HS256,
|
||||||
|
* HS384, HS512) for token signing and verification
|
||||||
|
* @param objectMapper Jackson Databind JSON Handler
|
||||||
|
*/
|
||||||
|
public AuthzeroTokenResolver(TokenAlgorithm algorithm, String issuer, String secret, ObjectMapper objectMapper) {
|
||||||
|
this(UUID::randomUUID, algorithm, issuer, secret, objectMapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -155,20 +187,7 @@ public class AuthzeroTokenResolver implements TokenResolver<DecodedJWT> {
|
|||||||
* HS384, HS512) for token signing and verification
|
* HS384, HS512) for token signing and verification
|
||||||
*/
|
*/
|
||||||
public AuthzeroTokenResolver(TokenAlgorithm algorithm, String issuer, String secret) {
|
public AuthzeroTokenResolver(TokenAlgorithm algorithm, String issuer, String secret) {
|
||||||
if (secret == null || secret.isBlank()) {
|
this(UUID::randomUUID, algorithm, issuer, secret, new ObjectMapper());
|
||||||
throw new IllegalArgumentException("A secret is required to build a JSON Web Token.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (secret.length() <= 32) {
|
|
||||||
log.warn("The provided secret which owns {} characters is too weak. Please consider replacing it with a stronger one.", secret.length());
|
|
||||||
}
|
|
||||||
|
|
||||||
this.jtiCreator = UUID::randomUUID;
|
|
||||||
this.algorithm = config
|
|
||||||
.getAlgorithm(algorithm)
|
|
||||||
.apply(secret);
|
|
||||||
this.issuer = issuer;
|
|
||||||
this.verifier = JWT.require(this.algorithm).build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -181,20 +200,7 @@ public class AuthzeroTokenResolver implements TokenResolver<DecodedJWT> {
|
|||||||
* HS384, HS512) for token signing and verification
|
* HS384, HS512) for token signing and verification
|
||||||
*/
|
*/
|
||||||
public AuthzeroTokenResolver(String issuer, String secret) {
|
public AuthzeroTokenResolver(String issuer, String secret) {
|
||||||
if (secret == null || secret.isBlank()) {
|
this(UUID::randomUUID, TokenAlgorithm.HS256, issuer, secret, new ObjectMapper());
|
||||||
throw new IllegalArgumentException("A secret is required to build a JSON Web Token.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (secret.length() <= 32) {
|
|
||||||
log.warn("The provided secret which owns {} characters is too weak. Please consider replacing it with a stronger one.", secret.length());
|
|
||||||
}
|
|
||||||
|
|
||||||
this.jtiCreator = UUID::randomUUID;
|
|
||||||
this.algorithm = config
|
|
||||||
.getAlgorithm(TokenAlgorithm.HS256)
|
|
||||||
.apply(secret);
|
|
||||||
this.issuer = issuer;
|
|
||||||
this.verifier = JWT.require(this.algorithm).build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -213,6 +219,7 @@ public class AuthzeroTokenResolver implements TokenResolver<DecodedJWT> {
|
|||||||
.apply(secret);
|
.apply(secret);
|
||||||
this.issuer = issuer;
|
this.issuer = issuer;
|
||||||
this.verifier = JWT.require(this.algorithm).build();
|
this.verifier = JWT.require(this.algorithm).build();
|
||||||
|
this.objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
log.info("The secret has been set to {}.", secret);
|
log.info("The secret has been set to {}.", secret);
|
||||||
}
|
}
|
||||||
@@ -370,16 +377,28 @@ public class AuthzeroTokenResolver implements TokenResolver<DecodedJWT> {
|
|||||||
var fields = payloadClass.getDeclaredFields();
|
var fields = payloadClass.getDeclaredFields();
|
||||||
|
|
||||||
for (var field : fields) {
|
for (var field : fields) {
|
||||||
// Skip the fields which are annotated with ExcludeFromPayload
|
|
||||||
if (field.isAnnotationPresent(ExcludeFromPayload.class))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
field.setAccessible(true);
|
var fieldName = field.getName();
|
||||||
|
// Skip the fields which are annotated with ExcludeFromPayload
|
||||||
|
if (field.isAnnotationPresent(ExcludeFromPayload.class))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
Object invokeObj = payload;
|
||||||
|
var getter = payloadClass.getDeclaredMethod("get" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1));
|
||||||
|
if (field.isAnnotationPresent(TokenEnum.class)) {
|
||||||
|
var tokenEnum = field.getAnnotation(TokenEnum.class);
|
||||||
|
invokeObj = getter.invoke(payload);
|
||||||
|
getter = field.getType().getDeclaredMethod("get" + tokenEnum.propertyName().substring(0, 1).toUpperCase() + tokenEnum.propertyName().substring(1));
|
||||||
|
}
|
||||||
|
|
||||||
// Build Claims
|
// Build Claims
|
||||||
addClaim(builder, field.getName(), field.get(payload));
|
addClaim(builder, fieldName, getter.invoke(invokeObj));
|
||||||
} catch (IllegalAccessException e) {
|
} catch (IllegalAccessException e) {
|
||||||
log.error("Cannot access field {}!", field.getName());
|
log.error("Cannot access field {}!", field.getName());
|
||||||
|
} catch (NoSuchMethodException e) {
|
||||||
|
log.error("Unable to find setter according to given field name.", e);
|
||||||
|
} catch (InvocationTargetException e) {
|
||||||
|
log.info("Cannot invoke method.", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,46 +427,69 @@ public class AuthzeroTokenResolver implements TokenResolver<DecodedJWT> {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public <T extends TokenPayload> T extract(String token, Class<T> targetType) {
|
public <T extends TokenPayload> T extract(String token, Class<T> targetType) {
|
||||||
// Get claims from token.
|
|
||||||
var claims = resolve(token).getClaims();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Get claims from token.
|
||||||
|
var payloads = objectMapper.readValue(Base64Util.decode(resolve(token).getPayload()), new MapTypeReference());
|
||||||
// Get the no-argument constructor to create an instance.
|
// Get the no-argument constructor to create an instance.
|
||||||
T bean = targetType.getConstructor().newInstance();
|
var bean = targetType.getConstructor().newInstance();
|
||||||
|
|
||||||
var fields = targetType.getDeclaredFields();
|
for (var entry : payloads.entrySet()) {
|
||||||
for (var field : fields) {
|
// Jump all JWT pre-defined properties and the fields that are annotated to be excluded.
|
||||||
// Ignore the field annotated with @ExcludeFromPayload.
|
if (PredefinedKeys.KEYS.contains(entry.getKey()) || targetType.getDeclaredField(entry.getKey()).isAnnotationPresent(ExcludeFromPayload.class))
|
||||||
if (field.isAnnotationPresent(ExcludeFromPayload.class))
|
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// Get the name of this field.
|
var field = targetType.getDeclaredField(entry.getKey());
|
||||||
var fieldName = field.getName();
|
var setter = targetType.getDeclaredMethod("set" + entry.getKey().substring(0, 1).toUpperCase() + entry.getKey().substring(1), field.getType());
|
||||||
|
var fieldValue = entry.getValue();
|
||||||
|
if (field.isAnnotationPresent(TokenEnum.class)) {
|
||||||
|
var annotation = field.getAnnotation(TokenEnum.class);
|
||||||
|
var enumStaticLoader = field.getType().getDeclaredMethod("loadBy" + annotation.propertyName().substring(0, 1).toUpperCase() + annotation.propertyName().substring(1), annotation.dataType().getMappedClass());
|
||||||
|
fieldValue = enumStaticLoader.invoke(null, fieldValue);
|
||||||
|
}
|
||||||
|
|
||||||
// Prevent this class is annotated @Slf4j or added logger.
|
if (setter.canAccess(bean)) {
|
||||||
if ("log".equalsIgnoreCase(fieldName) || "logger".equalsIgnoreCase(fieldName))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// Get the value of this field.
|
|
||||||
var fieldValue = Optional.ofNullable(claims.get(fieldName))
|
|
||||||
.map(claim -> claim.as(field.getType()))
|
|
||||||
.orElse(null);
|
|
||||||
if (fieldValue != null) {
|
|
||||||
// Set the field value by invoking the setter method.
|
|
||||||
var setter = targetType.getDeclaredMethod("set" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1), fieldValue.getClass());
|
|
||||||
setter.invoke(bean, fieldValue);
|
setter.invoke(bean, fieldValue);
|
||||||
|
} else {
|
||||||
|
log.error("Setter for field {} can't be accessed.", entry.getKey());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return bean;
|
return bean;
|
||||||
} catch (NoSuchMethodException e) {
|
} catch (JsonProcessingException e) {
|
||||||
log.error("Unable to find a no-argument constructor declaration for class {}.", targetType.getCanonicalName());
|
log.error("Unable to read payload as a Map<String, Object>.", e);
|
||||||
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
|
} catch (InvocationTargetException | InstantiationException | IllegalAccessException |
|
||||||
log.error("Unable to create a new instance of class {}.", targetType.getCanonicalName());
|
NoSuchMethodException e) {
|
||||||
|
log.error("Unable to load the constructor or setter.", e);
|
||||||
|
} catch (NoSuchFieldException e) {
|
||||||
|
log.error("Unable to load the field.", e);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-generate a new token with the payload in the old one.
|
||||||
|
*
|
||||||
|
* @param oldToken the old token
|
||||||
|
* @param expireAfter how long the new token can be valid for
|
||||||
|
* @return re-generated token with the payload in the old one or
|
||||||
|
* {@code null} if an {@link JsonProcessingException} occurred.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String renew(String oldToken, Duration expireAfter) {
|
||||||
|
var resolved = resolve(oldToken);
|
||||||
|
|
||||||
|
try {
|
||||||
|
var payload = objectMapper.readValue(Base64Util.decode(resolved.getPayload()), ObjectNode.class);
|
||||||
|
payload.remove(PredefinedKeys.KEYS);
|
||||||
|
|
||||||
|
var payloadMap = objectMapper.convertValue(payload, new MapTypeReference());
|
||||||
|
return createToken(expireAfter, resolved.getAudience().get(0), resolved.getSubject(), payloadMap);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
log.error("Cannot read payload content, error details:", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renews the given expired token with the specified custom payload data.
|
* Renews the given expired token with the specified custom payload data.
|
||||||
*
|
*
|
||||||
@@ -509,4 +551,9 @@ public class AuthzeroTokenResolver implements TokenResolver<DecodedJWT> {
|
|||||||
public <T extends TokenPayload> String renew(String oldToken, T payload) {
|
public <T extends TokenPayload> String renew(String oldToken, T payload) {
|
||||||
return renew(oldToken, Duration.ofMinutes(30), payload);
|
return renew(oldToken, Duration.ofMinutes(30), payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class MapTypeReference extends TypeReference<Map<String, Object>> {
|
||||||
|
MapTypeReference() {
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>cn.org.codecrafters</groupId>
|
<groupId>cn.org.codecrafters</groupId>
|
||||||
<artifactId>jdevkit</artifactId>
|
<artifactId>jdevkit</artifactId>
|
||||||
<version>1.1.2</version>
|
<version>1.2.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>simple-jwt-facade</artifactId>
|
<artifactId>simple-jwt-facade</artifactId>
|
||||||
@@ -40,6 +40,11 @@
|
|||||||
<artifactId>devkit-core</artifactId>
|
<artifactId>devkit-core</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.org.codecrafters</groupId>
|
||||||
|
<artifactId>devkit-utils</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.org.codecrafters</groupId>
|
<groupId>cn.org.codecrafters</groupId>
|
||||||
<artifactId>guid</artifactId>
|
<artifactId>guid</artifactId>
|
||||||
|
|||||||
@@ -110,6 +110,26 @@ public interface TokenResolver<ResolvedTokenType> {
|
|||||||
*/
|
*/
|
||||||
<T extends TokenPayload> T extract(String token, Class<T> targetType);
|
<T extends TokenPayload> T extract(String token, Class<T> targetType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-generate a new token with the payload in the old one.
|
||||||
|
*
|
||||||
|
* @param oldToken the old token
|
||||||
|
* @param expireAfter how long the new token can be valid for
|
||||||
|
* @return re-generated token with the payload in the old one
|
||||||
|
*/
|
||||||
|
String renew(String oldToken, Duration expireAfter);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-generate a new token with the payload in the old one.
|
||||||
|
*
|
||||||
|
* @param oldToken the old token
|
||||||
|
* @return re-generated token with the payload in the old one
|
||||||
|
* @see #renew(String, Duration)
|
||||||
|
*/
|
||||||
|
default String renew(String oldToken) {
|
||||||
|
return renew(oldToken, Duration.ofMinutes(30));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renews the given expired token with the specified custom payload data.
|
* Renews the given expired token with the specified custom payload data.
|
||||||
*
|
*
|
||||||
@@ -129,7 +149,9 @@ public interface TokenResolver<ResolvedTokenType> {
|
|||||||
* token
|
* token
|
||||||
* @return the renewed token as a {@code String}
|
* @return the renewed token as a {@code String}
|
||||||
*/
|
*/
|
||||||
String renew(String oldToken, Map<String, Object> payload);
|
default String renew(String oldToken, Map<String, Object> payload) {
|
||||||
|
return renew(oldToken, Duration.ofMinutes(30), payload);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renews the given expired token with the specified strongly-typed
|
* Renews the given expired token with the specified strongly-typed
|
||||||
@@ -156,6 +178,8 @@ public interface TokenResolver<ResolvedTokenType> {
|
|||||||
* renewed token
|
* renewed token
|
||||||
* @return the renewed token as a {@code String}
|
* @return the renewed token as a {@code String}
|
||||||
*/
|
*/
|
||||||
<T extends TokenPayload> String renew(String oldToken, T payload);
|
default <T extends TokenPayload> String renew(String oldToken, T payload) {
|
||||||
|
return renew(oldToken, Duration.ofMinutes(30), payload);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 CodeCraftersCN.
|
||||||
|
*
|
||||||
|
* 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 cn.org.codecrafters.simplejwt.annotations;
|
||||||
|
|
||||||
|
import cn.org.codecrafters.simplejwt.constants.TokenDataType;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This annotation marks the enum field declared in payload class will be
|
||||||
|
* handled as basic data types in {@link TokenDataType}.
|
||||||
|
*
|
||||||
|
* @author Zihlu Wang
|
||||||
|
*/
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Target({ElementType.FIELD})
|
||||||
|
public @interface TokenEnum {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the field of the base data corresponding to the
|
||||||
|
* enumeration data.
|
||||||
|
*/
|
||||||
|
String propertyName();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attribute {@code dataType} specifies what base data type to treat
|
||||||
|
* this enum as.
|
||||||
|
*/
|
||||||
|
TokenDataType dataType();
|
||||||
|
|
||||||
|
}
|
||||||
+70
@@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 CodeCraftersCN.
|
||||||
|
*
|
||||||
|
* 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 cn.org.codecrafters.simplejwt.constants;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base data types used to process enum data.
|
||||||
|
*
|
||||||
|
* @author Zihlu Wang
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public enum TokenDataType {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks enumeration being processed as Boolean.
|
||||||
|
*/
|
||||||
|
BOOLEAN(Boolean.class),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks enumeration being processed as Double.
|
||||||
|
*/
|
||||||
|
DOUBLE(Long.class),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks enumeration being processed as Float.
|
||||||
|
*/
|
||||||
|
FLOAT(Float.class),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks enumeration being processed as Integer.
|
||||||
|
*/
|
||||||
|
INTEGER(Integer.class),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks enumeration being processed as Long.
|
||||||
|
*/
|
||||||
|
LONG(Long.class),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks enumeration being processed as String.
|
||||||
|
*/
|
||||||
|
STRING(String.class),
|
||||||
|
;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The mapped class to this mark.
|
||||||
|
*/
|
||||||
|
private final Class<?> mappedClass;
|
||||||
|
|
||||||
|
TokenDataType(Class<?> mappedClass) {
|
||||||
|
this.mappedClass = mappedClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>cn.org.codecrafters</groupId>
|
<groupId>cn.org.codecrafters</groupId>
|
||||||
<artifactId>jdevkit</artifactId>
|
<artifactId>jdevkit</artifactId>
|
||||||
<version>1.1.2</version>
|
<version>1.2.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>simple-jwt-jjwt</artifactId>
|
<artifactId>simple-jwt-jjwt</artifactId>
|
||||||
|
|||||||
+65
-7
@@ -23,9 +23,12 @@ import cn.org.codecrafters.simplejwt.SecretCreator;
|
|||||||
import cn.org.codecrafters.simplejwt.TokenPayload;
|
import cn.org.codecrafters.simplejwt.TokenPayload;
|
||||||
import cn.org.codecrafters.simplejwt.TokenResolver;
|
import cn.org.codecrafters.simplejwt.TokenResolver;
|
||||||
import cn.org.codecrafters.simplejwt.annotations.ExcludeFromPayload;
|
import cn.org.codecrafters.simplejwt.annotations.ExcludeFromPayload;
|
||||||
|
import cn.org.codecrafters.simplejwt.annotations.TokenEnum;
|
||||||
|
import cn.org.codecrafters.simplejwt.constants.PredefinedKeys;
|
||||||
import cn.org.codecrafters.simplejwt.constants.TokenAlgorithm;
|
import cn.org.codecrafters.simplejwt.constants.TokenAlgorithm;
|
||||||
import cn.org.codecrafters.simplejwt.exceptions.WeakSecretException;
|
import cn.org.codecrafters.simplejwt.exceptions.WeakSecretException;
|
||||||
import cn.org.codecrafters.simplejwt.jjwt.config.JjwtTokenResolverConfig;
|
import cn.org.codecrafters.simplejwt.jjwt.config.JjwtTokenResolverConfig;
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import io.jsonwebtoken.Claims;
|
import io.jsonwebtoken.Claims;
|
||||||
import io.jsonwebtoken.Jws;
|
import io.jsonwebtoken.Jws;
|
||||||
import io.jsonwebtoken.Jwts;
|
import io.jsonwebtoken.Jwts;
|
||||||
@@ -245,14 +248,24 @@ public class JjwtTokenResolver implements TokenResolver<Jws<Claims>> {
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
field.setAccessible(true);
|
var getter = payload.getClass().getDeclaredMethod("get" + field.getName().substring(0, 1).toUpperCase() + field.getName().substring(1));
|
||||||
// Build Claims
|
// Build Claims
|
||||||
/*
|
/*
|
||||||
* Note (17 Oct, 2023): The jjwt can only add a map to be added.
|
* Note (17 Oct, 2023): The jjwt can only add a map to be added.
|
||||||
*/
|
*/
|
||||||
payloadMap.put(field.getName(), field.get(payload));
|
var fieldValue = getter.invoke(payload);
|
||||||
} catch (IllegalAccessException e) {
|
|
||||||
|
// Handle enum fields.
|
||||||
|
if (field.isAnnotationPresent(TokenEnum.class)) {
|
||||||
|
var annotation = field.getAnnotation(TokenEnum.class);
|
||||||
|
var enumGetter = field.getType().getDeclaredMethod("get" + annotation.propertyName().substring(0, 1).toUpperCase() + annotation.propertyName().substring(1));
|
||||||
|
fieldValue = enumGetter.invoke(fieldValue);
|
||||||
|
}
|
||||||
|
payloadMap.put(field.getName(), fieldValue);
|
||||||
|
} catch (IllegalAccessException | NoSuchMethodException e) {
|
||||||
log.error("Cannot access field {}!", field.getName());
|
log.error("Cannot access field {}!", field.getName());
|
||||||
|
} catch (InvocationTargetException e) {
|
||||||
|
log.error("Cannot invoke getter.", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,20 +302,65 @@ public class JjwtTokenResolver implements TokenResolver<Jws<Claims>> {
|
|||||||
|
|
||||||
var claims = resolvedToken.getBody();
|
var claims = resolvedToken.getBody();
|
||||||
try {
|
try {
|
||||||
return MapUtil.mapToObject(claims, targetType);
|
var bean = targetType.getConstructor().newInstance();
|
||||||
|
|
||||||
|
for (var entry : claims.entrySet()) {
|
||||||
|
// Jump all JWT pre-defined properties and the fields that are annotated to be excluded.
|
||||||
|
if (PredefinedKeys.KEYS.contains(entry.getKey()) || targetType.getDeclaredField(entry.getKey()).isAnnotationPresent(ExcludeFromPayload.class))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var field = targetType.getDeclaredField(entry.getKey());
|
||||||
|
var fieldValue = entry.getValue();
|
||||||
|
if (field.isAnnotationPresent(TokenEnum.class)) {
|
||||||
|
var annotation = field.getAnnotation(TokenEnum.class);
|
||||||
|
var enumStaticLoader = field.getType().getDeclaredMethod("loadBy" + annotation.propertyName().substring(0, 1).toUpperCase() + annotation.propertyName().substring(1), annotation.dataType().getMappedClass());
|
||||||
|
fieldValue = enumStaticLoader.invoke(null, entry.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
var setter = targetType.getDeclaredMethod("set" + entry.getKey().substring(0, 1).toUpperCase() + entry.getKey().substring(1), fieldValue.getClass());
|
||||||
|
if (setter.canAccess(bean)) {
|
||||||
|
setter.invoke(bean, fieldValue);
|
||||||
|
} else {
|
||||||
|
log.error("Setter for field {} can't be accessed.", entry.getKey());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bean;
|
||||||
} catch (InvocationTargetException e) {
|
} catch (InvocationTargetException e) {
|
||||||
log.error("An error occurs while invoking the constructor of type {}.", targetType.getCanonicalName());
|
log.error("Target is not invokable.", e);
|
||||||
} catch (NoSuchMethodException e) {
|
} catch (NoSuchMethodException e) {
|
||||||
log.error("The constructor of the required type {} is not found.", targetType.getCanonicalName());
|
log.error("Cannot find method according to given data.", e);
|
||||||
} catch (InstantiationException e) {
|
} catch (InstantiationException e) {
|
||||||
log.error("The required type {} is abstract or an interface.", targetType.getCanonicalName());
|
log.error("The required type {} is abstract or an interface.", targetType.getCanonicalName());
|
||||||
} catch (IllegalAccessException e) {
|
} catch (IllegalAccessException e) {
|
||||||
log.error("An error occurs while accessing the fields of the object.");
|
log.error("An error occurs while accessing the fields of the object.", e);
|
||||||
|
} catch (NoSuchFieldException e) {
|
||||||
|
log.error("Cannot load field according to given field name.", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-generate a new token with the payload in the old one.
|
||||||
|
*
|
||||||
|
* @param oldToken the old token
|
||||||
|
* @param expireAfter how long the new token can be valid for
|
||||||
|
* @return re-generated token with the payload in the old one
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String renew(String oldToken, Duration expireAfter) {
|
||||||
|
var resolvedToken = resolve(oldToken);
|
||||||
|
var tokenPayloads = resolvedToken.getBody();
|
||||||
|
|
||||||
|
var audience = tokenPayloads.getAudience();
|
||||||
|
var subject = tokenPayloads.getSubject();
|
||||||
|
|
||||||
|
PredefinedKeys.KEYS.forEach(tokenPayloads::remove);
|
||||||
|
|
||||||
|
return createToken(expireAfter, audience, subject, tokenPayloads);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renews the given expired token with the specified custom payload data.
|
* Renews the given expired token with the specified custom payload data.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>cn.org.codecrafters</groupId>
|
<groupId>cn.org.codecrafters</groupId>
|
||||||
<artifactId>jdevkit</artifactId>
|
<artifactId>jdevkit</artifactId>
|
||||||
<version>1.1.2</version>
|
<version>1.2.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>simple-jwt-spring-boot-starter</artifactId>
|
<artifactId>simple-jwt-spring-boot-starter</artifactId>
|
||||||
|
|||||||
+8
-3
@@ -22,6 +22,7 @@ import cn.org.codecrafters.simplejwt.TokenResolver;
|
|||||||
import cn.org.codecrafters.simplejwt.authzero.AuthzeroTokenResolver;
|
import cn.org.codecrafters.simplejwt.authzero.AuthzeroTokenResolver;
|
||||||
import cn.org.codecrafters.simplejwt.autoconfiguration.properties.SimpleJwtProperties;
|
import cn.org.codecrafters.simplejwt.autoconfiguration.properties.SimpleJwtProperties;
|
||||||
import com.auth0.jwt.interfaces.DecodedJWT;
|
import com.auth0.jwt.interfaces.DecodedJWT;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||||
@@ -31,7 +32,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
|||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.DependsOn;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@code AuthzeroTokenResolverAutoConfiguration} is responsible for
|
* {@code AuthzeroTokenResolverAutoConfiguration} is responsible for
|
||||||
@@ -75,16 +75,20 @@ public class AuthzeroTokenResolverAutoConfiguration {
|
|||||||
*/
|
*/
|
||||||
private final SimpleJwtProperties simpleJwtProperties;
|
private final SimpleJwtProperties simpleJwtProperties;
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a new {@code SimpleJwtAutoConfiguration} instance with the
|
* Constructs a new {@code SimpleJwtAutoConfiguration} instance with the
|
||||||
* provided SimpleJwtProperties.
|
* provided SimpleJwtProperties.
|
||||||
*
|
*
|
||||||
* @param simpleJwtProperties the SimpleJwtProperties instance
|
* @param simpleJwtProperties the SimpleJwtProperties instance
|
||||||
|
* @param objectMapper Jackson JSON Handler
|
||||||
*/
|
*/
|
||||||
@Autowired
|
@Autowired
|
||||||
public AuthzeroTokenResolverAutoConfiguration(SimpleJwtProperties simpleJwtProperties, GuidCreator<?> jtiCreator) {
|
public AuthzeroTokenResolverAutoConfiguration(SimpleJwtProperties simpleJwtProperties, GuidCreator<?> jtiCreator, ObjectMapper objectMapper) {
|
||||||
this.jtiCreator = jtiCreator;
|
this.jtiCreator = jtiCreator;
|
||||||
this.simpleJwtProperties = simpleJwtProperties;
|
this.simpleJwtProperties = simpleJwtProperties;
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -102,7 +106,8 @@ public class AuthzeroTokenResolverAutoConfiguration {
|
|||||||
jtiCreator,
|
jtiCreator,
|
||||||
simpleJwtProperties.algorithm(),
|
simpleJwtProperties.algorithm(),
|
||||||
simpleJwtProperties.issuer(),
|
simpleJwtProperties.issuer(),
|
||||||
simpleJwtProperties.secret()
|
simpleJwtProperties.secret(),
|
||||||
|
objectMapper
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -23,7 +23,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>cn.org.codecrafters</groupId>
|
<groupId>cn.org.codecrafters</groupId>
|
||||||
<artifactId>jdevkit</artifactId>
|
<artifactId>jdevkit</artifactId>
|
||||||
<version>1.1.2</version>
|
<version>1.2.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>webcal</artifactId>
|
<artifactId>webcal</artifactId>
|
||||||
|
|||||||
Reference in New Issue
Block a user