Skip to content

Commit

Permalink
Merge pull request #61 from isyoudwn/develop
Browse files Browse the repository at this point in the history
[FEATURE] : JWT 토큰 생성 및 검증
  • Loading branch information
KNU-K authored Dec 18, 2024
2 parents 1940d3a + 497897b commit 39f7b65
Show file tree
Hide file tree
Showing 12 changed files with 325 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,6 @@ out/

### VS Code ###
.vscode/

### yml file ###
/src/main/resources/application-jwt.yml
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ dependencies {

//thymeleaf
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

//jjwt
implementation 'io.jsonwebtoken:jjwt:0.9.1'
implementation 'javax.xml.bind:jaxb-api:2.3.1'
}

tasks.named('test') {
Expand Down
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"));
}
}
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 src/main/java/com/tasksprints/auction/common/jwt/JwtProvider.java
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 src/main/java/com/tasksprints/auction/common/jwt/JwtUtil.java
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);
}
}
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 src/main/java/com/tasksprints/auction/common/util/TimeUtil.java
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());
}
}
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;
}
3 changes: 3 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ spring:
mode: HTML
encoding: UTF-8
cache: false
profiles:
include:
-jwt

springdoc:
api-docs:
Expand Down
114 changes: 114 additions & 0 deletions src/test/java/com/tasksprints/auction/common/jwt/JwtProviderTest.java
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");
}
}
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());
}
}

0 comments on commit 39f7b65

Please sign in to comment.