-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #61 from isyoudwn/develop
[FEATURE] : JWT 토큰 생성 및 검증
- Loading branch information
Showing
12 changed files
with
325 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -35,3 +35,6 @@ out/ | |
|
||
### VS Code ### | ||
.vscode/ | ||
|
||
### yml file ### | ||
/src/main/resources/application-jwt.yml |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
14 changes: 14 additions & 0 deletions
14
src/main/java/com/tasksprints/auction/common/config/ClockConfig.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package com.tasksprints.auction.common.config; | ||
|
||
import java.time.Clock; | ||
import java.time.ZoneId; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
|
||
@Configuration | ||
public class ClockConfig { | ||
@Bean | ||
public Clock clock() { | ||
return Clock.system(ZoneId.of("Asia/seoul")); | ||
} | ||
} |
27 changes: 27 additions & 0 deletions
27
src/main/java/com/tasksprints/auction/common/jwt/JwtProperties.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package com.tasksprints.auction.common.jwt; | ||
|
||
import lombok.Getter; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.stereotype.Component; | ||
|
||
@Component | ||
@Getter | ||
public class JwtProperties { | ||
@Value("${jwt.header}") | ||
private String header; | ||
|
||
@Value("${jwt.prefix}") | ||
private String prefix; | ||
|
||
@Value("${jwt.expire-ms}") | ||
private Long expireMs; | ||
|
||
@Value("${jwt.expire-ms}") | ||
private Long refreshExpireMs; | ||
|
||
@Value("${jwt.issuer}") | ||
private String issuer; | ||
|
||
@Value("${jwt.secret}") | ||
private String secretKey; | ||
} |
59 changes: 59 additions & 0 deletions
59
src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
package com.tasksprints.auction.common.jwt; | ||
|
||
import static com.tasksprints.auction.common.util.TimeUtil.*; | ||
|
||
import com.tasksprints.auction.common.jwt.dto.response.JwtResponse; | ||
import io.jsonwebtoken.Claims; | ||
import io.jsonwebtoken.Jwts; | ||
import io.jsonwebtoken.SignatureAlgorithm; | ||
import java.time.Clock; | ||
import java.time.LocalDateTime; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.stereotype.Component; | ||
|
||
import java.util.Date; | ||
|
||
@Component | ||
@RequiredArgsConstructor | ||
public class JwtProvider { | ||
|
||
private final JwtProperties jwtProperties; | ||
private final Clock clock; | ||
|
||
public JwtResponse generateToken(Long userId, String userRole) { | ||
return JwtResponse.of(createAccessToken(userId, userRole), createRefreshToken()); | ||
} | ||
|
||
public String createAccessToken(Long userId, String userRole) { | ||
|
||
Date now = localDateTimeToDate(LocalDateTime.now(clock)); | ||
|
||
return Jwts.builder().setIssuer(jwtProperties.getIssuer()).claim("userId", userId).claim("userRole", userRole) | ||
.setIssuedAt(now).setExpiration(new Date(now.getTime() + jwtProperties.getExpireMs())) | ||
.signWith(SignatureAlgorithm.HS256, JwtUtil.encodeSecretKey(jwtProperties.getSecretKey())).compact(); | ||
} | ||
|
||
public String createRefreshToken() { | ||
|
||
Date now = localDateTimeToDate(LocalDateTime.now(clock)); | ||
|
||
return Jwts.builder().setIssuer(jwtProperties.getIssuer()).setIssuedAt(now) | ||
.setExpiration(new Date(now.getTime() + jwtProperties.getRefreshExpireMs())) | ||
.signWith(SignatureAlgorithm.HS256, JwtUtil.encodeSecretKey(jwtProperties.getSecretKey())).compact(); | ||
} | ||
|
||
public boolean verifyToken(String token) { | ||
|
||
Date now = localDateTimeToDate(LocalDateTime.now(clock)); | ||
|
||
Claims claims = getClaims(token); | ||
|
||
return !claims.getExpiration().before(now); | ||
} | ||
|
||
public Claims getClaims(String token) { | ||
return Jwts.parser().setSigningKey(JwtUtil.encodeSecretKey(jwtProperties.getSecretKey())) | ||
.parseClaimsJws(token) | ||
.getBody(); | ||
} | ||
} |
13 changes: 13 additions & 0 deletions
13
src/main/java/com/tasksprints/auction/common/jwt/JwtUtil.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package com.tasksprints.auction.common.jwt; | ||
|
||
import java.nio.charset.StandardCharsets; | ||
|
||
public class JwtUtil { | ||
|
||
/** | ||
* secret-key 를 인코딩 합니다. | ||
* */ | ||
public static byte[] encodeSecretKey(String secretKey) { | ||
return secretKey.getBytes(StandardCharsets.UTF_8); | ||
} | ||
} |
19 changes: 19 additions & 0 deletions
19
src/main/java/com/tasksprints/auction/common/jwt/dto/response/JwtResponse.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package com.tasksprints.auction.common.jwt.dto.response; | ||
|
||
import lombok.*; | ||
|
||
@Data | ||
@Builder | ||
@NoArgsConstructor | ||
@AllArgsConstructor | ||
public class JwtResponse { | ||
private String refreshToken; | ||
private String accessToken; | ||
|
||
public static JwtResponse of(String accessToken, String refreshToken) { | ||
return JwtResponse.builder() | ||
.accessToken(accessToken) | ||
.refreshToken(refreshToken) | ||
.build(); | ||
} | ||
} |
13 changes: 13 additions & 0 deletions
13
src/main/java/com/tasksprints/auction/common/util/TimeUtil.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package com.tasksprints.auction.common.util; | ||
|
||
import java.time.LocalDateTime; | ||
import java.time.ZoneId; | ||
import java.util.Date; | ||
|
||
public class TimeUtil { | ||
private static final String zoneId = "Asia/Seoul"; | ||
|
||
public static Date localDateTimeToDate(LocalDateTime localDateTime) { | ||
return Date.from(localDateTime.atZone(ZoneId.of(zoneId)).toInstant()); | ||
} | ||
} |
13 changes: 13 additions & 0 deletions
13
src/main/java/com/tasksprints/auction/domain/user/model/UserRole.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package com.tasksprints.auction.domain.user.model; | ||
|
||
import lombok.Getter; | ||
import lombok.RequiredArgsConstructor; | ||
|
||
@Getter | ||
@RequiredArgsConstructor | ||
public enum UserRole { | ||
ADMIN("admin"), | ||
USER("user"); | ||
|
||
private final String userRole; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,6 +29,9 @@ spring: | |
mode: HTML | ||
encoding: UTF-8 | ||
cache: false | ||
profiles: | ||
include: | ||
-jwt | ||
|
||
springdoc: | ||
api-docs: | ||
|
114 changes: 114 additions & 0 deletions
114
src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
package com.tasksprints.auction.common.jwt; | ||
|
||
import com.tasksprints.auction.common.jwt.dto.response.JwtResponse; | ||
import io.jsonwebtoken.ExpiredJwtException; | ||
import java.time.Clock; | ||
import java.time.Instant; | ||
import java.time.ZoneId; | ||
import org.junit.jupiter.api.Assertions; | ||
import org.junit.jupiter.api.BeforeEach; | ||
import org.junit.jupiter.api.DisplayName; | ||
import org.junit.jupiter.api.Test; | ||
import org.junit.jupiter.api.extension.ExtendWith; | ||
import org.mockito.InjectMocks; | ||
import org.mockito.Mock; | ||
import org.mockito.junit.jupiter.MockitoExtension; | ||
|
||
import static org.assertj.core.api.Assertions.*; | ||
import static org.junit.jupiter.api.Assertions.assertNotNull; | ||
import static org.mockito.Mockito.when; | ||
|
||
@ExtendWith(MockitoExtension.class) | ||
class JwtProviderTest { | ||
@Mock | ||
private JwtProperties jwtProperties; | ||
@Mock | ||
private Clock clock; | ||
@InjectMocks | ||
private JwtProvider jwtProvider; | ||
|
||
private final Long VALID_EXPIRE_MS = 36000000L; | ||
private final Long REFRESH_EXPIRE_MS = 72000000L; | ||
private final Long EXPIRED_EXPIRE_MS = 0L; | ||
private final String ISSUER = "testIssuer"; | ||
private final String SECRET_KEY = "testSecretKey"; | ||
private final ZoneId ZONE_ID = ZoneId.of("Asia/Seoul"); | ||
|
||
@BeforeEach | ||
public void setUp() { | ||
when(clock.instant()).thenReturn(Instant.now()); | ||
when(clock.getZone()).thenReturn(ZONE_ID); | ||
when(jwtProperties.getIssuer()).thenReturn(ISSUER); | ||
when(jwtProperties.getSecretKey()).thenReturn(SECRET_KEY); | ||
} | ||
|
||
private void stubAccessTokenExpiration(Long expireMs) { | ||
when(jwtProperties.getExpireMs()).thenReturn(expireMs); | ||
} | ||
|
||
private void stubRefreshTokenExpiration(Long expireMs) { | ||
when(jwtProperties.getRefreshExpireMs()).thenReturn(expireMs); | ||
} | ||
|
||
@Test | ||
@DisplayName("token generator 을 통한 access token, refresh token 발급 테스트") | ||
void generateToken() { | ||
JwtResponse jwtResponse = jwtProvider.generateToken(1L, "admin"); | ||
|
||
assertNotNull(jwtResponse.getAccessToken(), "access token 이 발급되어야 합니다."); | ||
assertNotNull(jwtResponse.getRefreshToken(), "refresh token 이 발급되어야 합니다."); | ||
} | ||
|
||
@Test | ||
@DisplayName("access token 발급 테스트") | ||
void createAccessToken() { | ||
stubAccessTokenExpiration(VALID_EXPIRE_MS); | ||
|
||
String token = jwtProvider.createAccessToken(1L, "admin"); | ||
|
||
assertNotNull(token, "access token 이 발급되어야 합니다."); | ||
} | ||
|
||
@Test | ||
@DisplayName("refresh token 발급 테스트") | ||
void createRefreshToken() { | ||
stubRefreshTokenExpiration(REFRESH_EXPIRE_MS); | ||
String token = jwtProvider.createRefreshToken(); | ||
assertNotNull(token, "refresh token 이 발급되어야 합니다."); | ||
} | ||
|
||
@Test | ||
@DisplayName("유효한 토큰 테스트") | ||
void verifyToken_valid() { | ||
stubAccessTokenExpiration(VALID_EXPIRE_MS); | ||
|
||
String token = jwtProvider.createAccessToken(1L, "admin"); | ||
|
||
Assertions.assertTrue(jwtProvider.verifyToken(token)); | ||
} | ||
|
||
@Test | ||
@DisplayName("만료된 토큰 테스트") | ||
void verifyToken_expired() { | ||
stubAccessTokenExpiration(EXPIRED_EXPIRE_MS); | ||
|
||
String token = jwtProvider.createAccessToken(1L, "admin"); | ||
|
||
Assertions.assertThrows(ExpiredJwtException.class, () -> { | ||
jwtProvider.verifyToken(token); | ||
}, "토큰이 즉시 만료되어야 합니다."); | ||
} | ||
|
||
@Test | ||
@DisplayName("디코딩 된 페이로드 정확성 테스트") | ||
void getClaims() { | ||
stubAccessTokenExpiration(VALID_EXPIRE_MS); | ||
|
||
String token = jwtProvider.createAccessToken(1L, "admin"); | ||
Long decodedUserId = jwtProvider.getClaims(token).get("userId", Long.class); | ||
String decodedUserRole = jwtProvider.getClaims(token).get("userRole", String.class); | ||
|
||
assertThat(decodedUserId).isEqualTo(1L); | ||
assertThat(decodedUserRole).isEqualTo("admin"); | ||
} | ||
} |
43 changes: 43 additions & 0 deletions
43
src/test/java/com/tasksprints/auction/common/util/TimeUtilTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
package com.tasksprints.auction.common.util; | ||
|
||
import static org.junit.jupiter.api.Assertions.assertEquals; | ||
import static org.mockito.Mockito.when; | ||
|
||
import java.time.Clock; | ||
import java.time.Instant; | ||
import java.time.LocalDateTime; | ||
import java.time.ZoneId; | ||
import java.util.Date; | ||
import org.junit.jupiter.api.BeforeEach; | ||
import org.junit.jupiter.api.DisplayName; | ||
import org.junit.jupiter.api.Test; | ||
import org.junit.jupiter.api.extension.ExtendWith; | ||
import org.mockito.Mock; | ||
import org.mockito.junit.jupiter.MockitoExtension; | ||
|
||
@ExtendWith(MockitoExtension.class) | ||
class TimeUtilTest { | ||
private final ZoneId zoneId = ZoneId.of("Asia/Seoul"); | ||
private final Instant fixedInstant = Instant.parse("2024-10-22T10:00:00Z"); | ||
@Mock | ||
private Clock clock; | ||
|
||
@BeforeEach | ||
void setUp() { | ||
when(clock.instant()).thenReturn(fixedInstant); | ||
when(clock.getZone()).thenReturn(zoneId); | ||
} | ||
|
||
@Test | ||
@DisplayName("LocalDateTime -> Date 변환 테스트") | ||
void localDateTimeToDate() { | ||
// given | ||
LocalDateTime localDateTime = LocalDateTime.now(clock); | ||
|
||
// when | ||
Date date = TimeUtil.localDateTimeToDate(localDateTime); | ||
|
||
// then | ||
assertEquals(localDateTime.atZone(zoneId).toInstant(), date.toInstant()); | ||
} | ||
} |