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/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/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) { diff --git a/src/main/java/com/back/global/jwt/refreshToken/entity/RefreshToken.java b/src/main/java/com/back/global/jwt/refreshToken/entity/RefreshToken.java new file mode 100644 index 00000000..874e6711 --- /dev/null +++ b/src/main/java/com/back/global/jwt/refreshToken/entity/RefreshToken.java @@ -0,0 +1,41 @@ +package com.back.global.jwt.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/global/jwt/refreshToken/repository/RefreshTokenRepository.java b/src/main/java/com/back/global/jwt/refreshToken/repository/RefreshTokenRepository.java new file mode 100644 index 00000000..a435ba67 --- /dev/null +++ b/src/main/java/com/back/global/jwt/refreshToken/repository/RefreshTokenRepository.java @@ -0,0 +1,16 @@ +package com.back.global.jwt.refreshToken.repository; + + +import com.back.global.jwt.refreshToken.entity.RefreshToken; +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/global/jwt/refreshToken/service/RefreshTokenService.java b/src/main/java/com/back/global/jwt/refreshToken/service/RefreshTokenService.java new file mode 100644 index 00000000..600f79e1 --- /dev/null +++ b/src/main/java/com/back/global/jwt/refreshToken/service/RefreshTokenService.java @@ -0,0 +1,63 @@ +package com.back.global.jwt.refreshToken.service; + + +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; + +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/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 cfcc301e..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,8 +65,12 @@ server: enabled: true force: true + + 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