From d6919b36a6a5669c348cdd5d020b33098a7e7c6d Mon Sep 17 00:00:00 2001 From: seungwookc97 Date: Fri, 19 Sep 2025 12:11:36 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat=20:=20Redis=EC=9D=98=EC=A1=B4=EC=84=B1?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EB=A6=AC=ED=94=84=EB=A0=88?= =?UTF-8?q?=EC=8B=9C=20=ED=86=A0=ED=81=B0=20=EC=97=94=ED=8B=B0=ED=8B=B0,?= =?UTF-8?q?=20=EB=A0=88=ED=8F=AC=EC=A7=80=ED=84=B0=EB=A6=AC=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 1 + .../refreshToken/entity/RefreshToken.java | 41 +++++++++++++++++++ .../repository/RefreshTokenRepository.java | 9 ++++ src/main/resources/application.yml | 16 +++++++- 4 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/back/domain/auth/refreshToken/entity/RefreshToken.java create mode 100644 src/main/java/com/back/domain/auth/refreshToken/repository/RefreshTokenRepository.java diff --git a/build.gradle.kts b/build.gradle.kts index 7ef45fd0..e3ef2cd4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -31,6 +31,7 @@ dependencies { implementation("io.github.cdimascio:java-dotenv:5.2.2") implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.boot:spring-boot-starter-oauth2-client") + implementation("org.springframework.boot:spring-boot-starter-data-redis") implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0") implementation("io.jsonwebtoken:jjwt-api:0.12.3") runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.3") diff --git a/src/main/java/com/back/domain/auth/refreshToken/entity/RefreshToken.java b/src/main/java/com/back/domain/auth/refreshToken/entity/RefreshToken.java new file mode 100644 index 00000000..f520c6eb --- /dev/null +++ b/src/main/java/com/back/domain/auth/refreshToken/entity/RefreshToken.java @@ -0,0 +1,41 @@ +package com.back.domain.auth.refreshToken.entity; + +import lombok.*; +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.RedisHash; +import org.springframework.data.redis.core.TimeToLive; +import org.springframework.data.redis.core.index.Indexed; + +import java.time.LocalDateTime; + +@RedisHash("refresh_token") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class RefreshToken { + + @Id + private String token; + + @Indexed + private Long userId; + + private String email; + + private LocalDateTime createdAt; + + @TimeToLive + private Long ttl; // seconds + + public static RefreshToken create(String token, Long userId, String email, long ttlSeconds) { + return RefreshToken.builder() + .token(token) + .userId(userId) + .email(email) + .createdAt(LocalDateTime.now()) + .ttl(ttlSeconds) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/back/domain/auth/refreshToken/repository/RefreshTokenRepository.java b/src/main/java/com/back/domain/auth/refreshToken/repository/RefreshTokenRepository.java new file mode 100644 index 00000000..6025f67e --- /dev/null +++ b/src/main/java/com/back/domain/auth/refreshToken/repository/RefreshTokenRepository.java @@ -0,0 +1,9 @@ +package com.back.domain.auth.refreshToken.repository; + +import com.back.domain.auth.refreshToken.entity.RefreshToken; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface RefreshTokenRepository extends CrudRepository { +} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index cfcc301e..bead5ae4 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -53,8 +53,22 @@ server: enabled: true force: true + data: + redis: + host: ${REDIS_HOST:localhost} + port: ${REDIS_PORT:6379} + password: ${REDIS_PASSWORD:} + timeout: 2000ms + lettuce: + pool: + max-active: 8 + max-idle: 8 + min-idle: 0 + custom: jwt: secretKey: ${JWT_SECRET_KEY} accessToken: - expirationSeconds: "#{60*20}" \ No newline at end of file + expirationSeconds: "#{60*15}" + refreshToken: + expirationSeconds: "#{60*60*24*30}" \ No newline at end of file From d8116b9e8c0aba7fa61b10f93ff738a7416683bd Mon Sep 17 00:00:00 2001 From: seungwookc97 Date: Fri, 19 Sep 2025 12:40:47 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat=20:=20=EB=A6=AC=ED=94=84=EB=A0=88?= =?UTF-8?q?=EC=8B=9C=20=ED=86=A0=ED=81=B0=20=ED=95=B5=EC=8B=AC=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/RefreshTokenRepository.java | 6 ++ .../service/RefreshTokenService.java | 62 +++++++++++++++++++ .../java/com/back/global/jwt/JwtUtil.java | 1 + 3 files changed, 69 insertions(+) create mode 100644 src/main/java/com/back/domain/auth/refreshToken/service/RefreshTokenService.java diff --git a/src/main/java/com/back/domain/auth/refreshToken/repository/RefreshTokenRepository.java b/src/main/java/com/back/domain/auth/refreshToken/repository/RefreshTokenRepository.java index 6025f67e..973db57a 100644 --- a/src/main/java/com/back/domain/auth/refreshToken/repository/RefreshTokenRepository.java +++ b/src/main/java/com/back/domain/auth/refreshToken/repository/RefreshTokenRepository.java @@ -4,6 +4,12 @@ import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository; +import java.util.Optional; + @Repository public interface RefreshTokenRepository extends CrudRepository { + + Optional findByToken(String token); + + void deleteByToken(String token); } \ No newline at end of file diff --git a/src/main/java/com/back/domain/auth/refreshToken/service/RefreshTokenService.java b/src/main/java/com/back/domain/auth/refreshToken/service/RefreshTokenService.java new file mode 100644 index 00000000..8088689a --- /dev/null +++ b/src/main/java/com/back/domain/auth/refreshToken/service/RefreshTokenService.java @@ -0,0 +1,62 @@ +package com.back.domain.auth.refreshToken.service; + +import com.back.domain.auth.refreshToken.entity.RefreshToken; +import com.back.domain.auth.refreshToken.repository.RefreshTokenRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.security.SecureRandom; +import java.util.Base64; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class RefreshTokenService { + + private final RefreshTokenRepository refreshTokenRepository; + private final SecureRandom secureRandom = new SecureRandom(); + + @Value("${custom.refreshToken.expirationSeconds}") + private long refreshTokenExpiration; + + // 기존 리프레시 토큰 삭제하고 생성 + public String generateRefreshToken(Long userId, String email) { + String token = generateSecureToken(); + RefreshToken refreshToken = RefreshToken.create(token, userId, email, refreshTokenExpiration); + refreshTokenRepository.save(refreshToken); + + return token; + } + + //검증 + public boolean validateToken(String token) { + return refreshTokenRepository.findByToken(token).isPresent(); + } + + //기존 토큰 지우고 발급(회전) + public String rotateToken(String oldToken) { + Optional oldRefreshToken = refreshTokenRepository.findByToken(oldToken); + + if (oldRefreshToken.isEmpty()) { + throw new IllegalArgumentException("Invalid refresh token"); + } + + RefreshToken tokenData = oldRefreshToken.get(); + revokeToken(oldToken); + + return generateRefreshToken(tokenData.getUserId(), tokenData.getEmail()); + } + + //삭제 + public void revokeToken(String token) { + refreshTokenRepository.deleteByToken(token); + } + + //문자열 난수 조합 + private String generateSecureToken() { + byte[] randomBytes = new byte[32]; + secureRandom.nextBytes(randomBytes); + return Base64.getEncoder().withoutPadding().encodeToString(randomBytes); + } +} diff --git a/src/main/java/com/back/global/jwt/JwtUtil.java b/src/main/java/com/back/global/jwt/JwtUtil.java index c4eb7275..e869b58c 100644 --- a/src/main/java/com/back/global/jwt/JwtUtil.java +++ b/src/main/java/com/back/global/jwt/JwtUtil.java @@ -20,6 +20,7 @@ public class JwtUtil { private final SecretKey secretKey; private final long accessTokenExpiration; private static final String ACCESS_TOKEN_COOKIE_NAME = "accessToken"; + private static final String REFRESH_TOKEN_COOKIE_NAME = "refreshToken"; public JwtUtil(@Value("${custom.jwt.secretKey}") String secretKey, @Value("${custom.accessToken.expirationSeconds}") long accessTokenExpiration) { From 63e5e3cf60f787bef6aa4b2ea748ca5b5d6ffdb4 Mon Sep 17 00:00:00 2001 From: seungwookc97 Date: Fri, 19 Sep 2025 14:18:29 +0900 Subject: [PATCH 3/3] =?UTF-8?q?refactor=20:=20=EB=A6=AC=ED=94=84=EB=A0=88?= =?UTF-8?q?=EC=8B=9C=20=ED=86=A0=ED=81=B0=20=ED=8C=A8=ED=82=A4=EC=A7=80=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20=EC=BD=94=EB=93=9C=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/back/BackApplication.java | 6 +---- .../refreshToken/entity/RefreshToken.java | 2 +- .../repository/RefreshTokenRepository.java | 5 ++-- .../service/RefreshTokenService.java | 7 +++--- src/main/resources/application-dev.yml | 4 ---- src/main/resources/application.yml | 24 ++++++++++--------- 6 files changed, 22 insertions(+), 26 deletions(-) rename src/main/java/com/back/{domain/auth => global/jwt}/refreshToken/entity/RefreshToken.java (94%) rename src/main/java/com/back/{domain/auth => global/jwt}/refreshToken/repository/RefreshTokenRepository.java (74%) rename src/main/java/com/back/{domain/auth => global/jwt}/refreshToken/service/RefreshTokenService.java (90%) diff --git a/src/main/java/com/back/BackApplication.java b/src/main/java/com/back/BackApplication.java index 87ac32bb..fe1c2d37 100644 --- a/src/main/java/com/back/BackApplication.java +++ b/src/main/java/com/back/BackApplication.java @@ -10,11 +10,7 @@ public class BackApplication { public static void main(String[] args) { - Dotenv dotenv = Dotenv.load(); - System.out.println("KAKAO_OAUTH2_CLIENT_ID: " + dotenv.get("KAKAO_OAUTH2_CLIENT_ID")); - System.out.println("GOOGLE_OAUTH2_CLIENT_ID: " + dotenv.get("GOOGLE_OAUTH2_CLIENT_ID")); - System.out.println("NAVER_OAUTH2_CLIENT_ID: " + dotenv.get("NAVER_OAUTH2_CLIENT_ID")); - + Dotenv.load(); SpringApplication.run(BackApplication.class, args); } diff --git a/src/main/java/com/back/domain/auth/refreshToken/entity/RefreshToken.java b/src/main/java/com/back/global/jwt/refreshToken/entity/RefreshToken.java similarity index 94% rename from src/main/java/com/back/domain/auth/refreshToken/entity/RefreshToken.java rename to src/main/java/com/back/global/jwt/refreshToken/entity/RefreshToken.java index f520c6eb..874e6711 100644 --- a/src/main/java/com/back/domain/auth/refreshToken/entity/RefreshToken.java +++ b/src/main/java/com/back/global/jwt/refreshToken/entity/RefreshToken.java @@ -1,4 +1,4 @@ -package com.back.domain.auth.refreshToken.entity; +package com.back.global.jwt.refreshToken.entity; import lombok.*; import org.springframework.data.annotation.Id; diff --git a/src/main/java/com/back/domain/auth/refreshToken/repository/RefreshTokenRepository.java b/src/main/java/com/back/global/jwt/refreshToken/repository/RefreshTokenRepository.java similarity index 74% rename from src/main/java/com/back/domain/auth/refreshToken/repository/RefreshTokenRepository.java rename to src/main/java/com/back/global/jwt/refreshToken/repository/RefreshTokenRepository.java index 973db57a..a435ba67 100644 --- a/src/main/java/com/back/domain/auth/refreshToken/repository/RefreshTokenRepository.java +++ b/src/main/java/com/back/global/jwt/refreshToken/repository/RefreshTokenRepository.java @@ -1,6 +1,7 @@ -package com.back.domain.auth.refreshToken.repository; +package com.back.global.jwt.refreshToken.repository; -import com.back.domain.auth.refreshToken.entity.RefreshToken; + +import com.back.global.jwt.refreshToken.entity.RefreshToken; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository; diff --git a/src/main/java/com/back/domain/auth/refreshToken/service/RefreshTokenService.java b/src/main/java/com/back/global/jwt/refreshToken/service/RefreshTokenService.java similarity index 90% rename from src/main/java/com/back/domain/auth/refreshToken/service/RefreshTokenService.java rename to src/main/java/com/back/global/jwt/refreshToken/service/RefreshTokenService.java index 8088689a..600f79e1 100644 --- a/src/main/java/com/back/domain/auth/refreshToken/service/RefreshTokenService.java +++ b/src/main/java/com/back/global/jwt/refreshToken/service/RefreshTokenService.java @@ -1,7 +1,8 @@ -package com.back.domain.auth.refreshToken.service; +package com.back.global.jwt.refreshToken.service; -import com.back.domain.auth.refreshToken.entity.RefreshToken; -import com.back.domain.auth.refreshToken.repository.RefreshTokenRepository; + +import com.back.global.jwt.refreshToken.entity.RefreshToken; +import com.back.global.jwt.refreshToken.repository.RefreshTokenRepository; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index d8f8c3bf..875f3ea1 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -22,10 +22,6 @@ spring: format_sql: true show_sql: true -# 개발 환경 URL 설정 -FRONTEND_URL: http://localhost:3000 -BASE_URL: http://localhost:8080 - # Swagger 설정 springdoc: api-docs: diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index bead5ae4..75bc07f9 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -5,6 +5,18 @@ spring: config: import: optional:file:.env[.properties] + data: + redis: + host: ${REDIS_HOST} + port: ${REDIS_PORT} + password: ${REDIS_PASSWORD:} + timeout: 2000ms + lettuce: + pool: + max-active: 8 + max-idle: 8 + min-idle: 0 + security: oauth2: client: @@ -53,17 +65,7 @@ server: enabled: true force: true - data: - redis: - host: ${REDIS_HOST:localhost} - port: ${REDIS_PORT:6379} - password: ${REDIS_PASSWORD:} - timeout: 2000ms - lettuce: - pool: - max-active: 8 - max-idle: 8 - min-idle: 0 + custom: jwt: