From 340a5c213c26787555cfffb22e488380a7a29062 Mon Sep 17 00:00:00 2001 From: devmizz Date: Fri, 31 May 2024 03:33:05 +0900 Subject: [PATCH] feat: JWT generator --- .../java/org/example/config/ApiConfig.java | 3 + .../org/example/property/TokenProperty.java | 12 +++ .../org/example/security/JWTGenerator.java | 40 +++++++++ .../security/dto/AuthenticatedUser.java | 11 +++ .../org/example/security/dto/TokenParam.java | 11 +++ .../org/example/security/dto/UserParam.java | 18 ++++ .../java/org/example/vo/UserRoleApiType.java | 5 ++ .../example/security/JWTGeneratorTest.java | 88 +++++++++++++++++++ .../main/java/org/example/vo/UserRole.java | 5 ++ app/src/main/resources/application-local.yml | 5 ++ 10 files changed, 198 insertions(+) create mode 100644 app/api/src/main/java/org/example/property/TokenProperty.java create mode 100644 app/api/src/main/java/org/example/security/JWTGenerator.java create mode 100644 app/api/src/main/java/org/example/security/dto/AuthenticatedUser.java create mode 100644 app/api/src/main/java/org/example/security/dto/TokenParam.java create mode 100644 app/api/src/main/java/org/example/security/dto/UserParam.java create mode 100644 app/api/src/main/java/org/example/vo/UserRoleApiType.java create mode 100644 app/api/src/test/java/org/example/security/JWTGeneratorTest.java create mode 100644 app/domain/user-domain/src/main/java/org/example/vo/UserRole.java diff --git a/app/api/src/main/java/org/example/config/ApiConfig.java b/app/api/src/main/java/org/example/config/ApiConfig.java index 60cdde5f..08a676b4 100644 --- a/app/api/src/main/java/org/example/config/ApiConfig.java +++ b/app/api/src/main/java/org/example/config/ApiConfig.java @@ -1,11 +1,14 @@ package org.example.config; +import org.example.property.TokenProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @Configuration @Import(UserApiConfig.class) +@EnableConfigurationProperties(TokenProperty.class) @ComponentScan(basePackages = "org.example") public class ApiConfig { diff --git a/app/api/src/main/java/org/example/property/TokenProperty.java b/app/api/src/main/java/org/example/property/TokenProperty.java new file mode 100644 index 00000000..3537f3df --- /dev/null +++ b/app/api/src/main/java/org/example/property/TokenProperty.java @@ -0,0 +1,12 @@ +package org.example.property; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "token") +public record TokenProperty( + String secretKey, + Long accessTokenExpirationSeconds, + Long refreshTokenExpirationSeconds +) { + +} diff --git a/app/api/src/main/java/org/example/security/JWTGenerator.java b/app/api/src/main/java/org/example/security/JWTGenerator.java new file mode 100644 index 00000000..d7e42a7c --- /dev/null +++ b/app/api/src/main/java/org/example/security/JWTGenerator.java @@ -0,0 +1,40 @@ +package org.example.security; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import java.util.Date; +import lombok.RequiredArgsConstructor; +import org.example.property.TokenProperty; +import org.example.security.dto.TokenParam; +import org.example.security.dto.UserParam; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class JWTGenerator { + + private final TokenProperty tokenProperty; + + public TokenParam generate(UserParam userParam, Date from) { + return TokenParam.builder() + .accessToken(createAccessToken(userParam, from)) + .refreshToken(createRefreshToken(userParam, from)) + .build(); + } + + private String createAccessToken(UserParam userParam, Date from) { + return JWT.create().withSubject("AccessToken") + .withClaim("claim", userParam.getTokenClaim()) + .withExpiresAt( + new Date(from.getTime() + tokenProperty.accessTokenExpirationSeconds()) + ).sign(Algorithm.HMAC512(tokenProperty.secretKey())); + } + + private String createRefreshToken(UserParam userParam, Date from) { + return JWT.create().withSubject("RefreshToken") + .withClaim("claim", userParam.getTokenClaim()) + .withExpiresAt( + new Date(from.getTime() + tokenProperty.refreshTokenExpirationSeconds()) + ).sign(Algorithm.HMAC512(tokenProperty.secretKey())); + } +} diff --git a/app/api/src/main/java/org/example/security/dto/AuthenticatedUser.java b/app/api/src/main/java/org/example/security/dto/AuthenticatedUser.java new file mode 100644 index 00000000..de7d8a1a --- /dev/null +++ b/app/api/src/main/java/org/example/security/dto/AuthenticatedUser.java @@ -0,0 +1,11 @@ +package org.example.security.dto; + +import java.util.UUID; +import org.example.vo.UserRoleApiType; + +public record AuthenticatedUser( + UUID userId, + UserRoleApiType role +) { + +} diff --git a/app/api/src/main/java/org/example/security/dto/TokenParam.java b/app/api/src/main/java/org/example/security/dto/TokenParam.java new file mode 100644 index 00000000..e9a12b2e --- /dev/null +++ b/app/api/src/main/java/org/example/security/dto/TokenParam.java @@ -0,0 +1,11 @@ +package org.example.security.dto; + +import lombok.Builder; + +@Builder +public record TokenParam( + String accessToken, + String refreshToken +) { + +} diff --git a/app/api/src/main/java/org/example/security/dto/UserParam.java b/app/api/src/main/java/org/example/security/dto/UserParam.java new file mode 100644 index 00000000..3ac3b7a0 --- /dev/null +++ b/app/api/src/main/java/org/example/security/dto/UserParam.java @@ -0,0 +1,18 @@ +package org.example.security.dto; + +import java.util.Map; +import java.util.UUID; +import org.example.vo.UserRoleApiType; + +public record UserParam( + UUID userId, + UserRoleApiType role +) { + + public Map getTokenClaim() { + return Map.of( + "userId", userId.toString(), + "role", role.name() + ); + } +} diff --git a/app/api/src/main/java/org/example/vo/UserRoleApiType.java b/app/api/src/main/java/org/example/vo/UserRoleApiType.java new file mode 100644 index 00000000..01f61b2a --- /dev/null +++ b/app/api/src/main/java/org/example/vo/UserRoleApiType.java @@ -0,0 +1,5 @@ +package org.example.vo; + +public enum UserRoleApiType { + GUEST, USER, ADMIN +} diff --git a/app/api/src/test/java/org/example/security/JWTGeneratorTest.java b/app/api/src/test/java/org/example/security/JWTGeneratorTest.java new file mode 100644 index 00000000..f7682636 --- /dev/null +++ b/app/api/src/test/java/org/example/security/JWTGeneratorTest.java @@ -0,0 +1,88 @@ +package org.example.security; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.exceptions.TokenExpiredException; +import java.util.Date; +import java.util.UUID; +import org.example.property.TokenProperty; +import org.example.security.dto.TokenParam; +import org.example.security.dto.UserParam; +import org.example.vo.UserRoleApiType; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class JWTGeneratorTest { + + long hour = 3600000L; + long twoWeeks = 1209600000L; + + TokenProperty tokenProperty = new TokenProperty( + "wehfiuhewiuhfhweiuhfiuwehifueiuwhfiuw", + hour, + twoWeeks + ); + + JWTGenerator tokenGenerator = new JWTGenerator(tokenProperty); + UserParam userParam = new UserParam( + UUID.randomUUID(), + UserRoleApiType.USER + ); + + @Test + @DisplayName("1시간 전에 생성된 AccessToken은 유효하지 않다.") + void accessTokenInvalidBeforeHourAgo() { + Date beforeHour = new Date(new Date().getTime() - hour); + TokenParam token = tokenGenerator.generate(userParam, beforeHour); + + Assertions.assertThrowsExactly( + TokenExpiredException.class, + () -> JWT.require(Algorithm.HMAC512(tokenProperty.secretKey())) + .build() + .verify(token.accessToken()) + ); + } + + @Test + @DisplayName("1시간 내에 생성된 AccessToken은 유효하다.") + void accessTokenValidAfterHourAgo() { + long second = 1000L; + Date beforeHourPlusSecond = new Date(new Date().getTime() - hour + second); + TokenParam token = tokenGenerator.generate(userParam, beforeHourPlusSecond); + + Assertions.assertDoesNotThrow( + () -> JWT.require(Algorithm.HMAC512(tokenProperty.secretKey())) + .build() + .verify(token.accessToken()) + ); + } + + @Test + @DisplayName("2주 전에 생성된 RefreshToken은 유효하지 않다.") + void refreshTokenInvalidBeforeHourAgo() { + Date beforeTwoWeeks = new Date(new Date().getTime() - twoWeeks); + TokenParam token = tokenGenerator.generate(userParam, beforeTwoWeeks); + + Assertions.assertThrowsExactly( + TokenExpiredException.class, + () -> JWT.require(Algorithm.HMAC512(tokenProperty.secretKey())) + .build() + .verify(token.refreshToken()) + ); + } + + @Test + @DisplayName("2주 내에 생성된 RefreshToken은 유효하다.") + void refreshTokenValidAfterHourAgo() { + long second = 1000L; + Date beforeTwoWeeksPlusSecond = new Date(new Date().getTime() - twoWeeks + second); + TokenParam token = tokenGenerator.generate(userParam, beforeTwoWeeksPlusSecond); + + Assertions.assertDoesNotThrow( + () -> JWT.require(Algorithm.HMAC512(tokenProperty.secretKey())) + .build() + .verify(token.refreshToken()) + ); + } +} \ No newline at end of file diff --git a/app/domain/user-domain/src/main/java/org/example/vo/UserRole.java b/app/domain/user-domain/src/main/java/org/example/vo/UserRole.java new file mode 100644 index 00000000..92d23088 --- /dev/null +++ b/app/domain/user-domain/src/main/java/org/example/vo/UserRole.java @@ -0,0 +1,5 @@ +package org.example.vo; + +public enum UserRole { + GUEST, USER, ADMIN +} diff --git a/app/src/main/resources/application-local.yml b/app/src/main/resources/application-local.yml index de97bfa7..d588e9ad 100644 --- a/app/src/main/resources/application-local.yml +++ b/app/src/main/resources/application-local.yml @@ -10,3 +10,8 @@ spring: user: name: user password: password + +token: + secret-key: ahRhwlglftmrkwkfehlaussksmsdjraksrmadmfqjfrjtdlrhdkwnwlflsmswlqdptjgodqhrgkrptkftndlTDmfrjtdlek + access-token-expiration-seconds: 3600000 # 1hour = 1000(=1s) * 60 * 60 + refresh-token-expiration-seconds: 1209600000 # 2weeks = 1000(=1s) * 60 * 60 * 24 * 14