Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] : JWT 토큰 생성 및 검증 #61

Merged
merged 18 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,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;
}
81 changes: 81 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,81 @@
package com.tasksprints.auction.common.jwt;

import com.tasksprints.auction.common.jwt.dto.response.JwtResponse;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
@RequiredArgsConstructor
public class JwtProvider {

private final JwtProperties jwtProperties;

/**
* refreshToken 과 accessToken 을 생성하고
* 생성된 토큰을 반환합니다.
* */
public JwtResponse generateToken(Long userId, String userRole) {
return JwtResponse.of(
createAccessToken(userId, userRole),
createRefreshToken());
}

/**
* accessToken 을 생성합니다.
* */
public String createAccessToken(Long userId, String userRole) {

Date now = new Date(System.currentTimeMillis());

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();
}

/**
* refreshToken 을 생성합니다.
* */
public String createRefreshToken() {

Date now = new Date(System.currentTimeMillis());

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 = new Date(System.currentTimeMillis());

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();
}
}
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
108 changes: 108 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,108 @@
package com.tasksprints.auction.common.jwt;

import com.tasksprints.auction.common.jwt.dto.response.JwtResponse;
import io.jsonwebtoken.ExpiredJwtException;
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;
@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";


@BeforeEach
public void setUp() {
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");
}
}