Skip to content

Commit 437a1da

Browse files
authored
Merge pull request #27 from prgrms-web-devcourse-final-project/feat#26
[feat] 리프레시 토큰 구현#1
2 parents 6f0bd32 + 63e5e3c commit 437a1da

File tree

8 files changed

+140
-10
lines changed

8 files changed

+140
-10
lines changed

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ dependencies {
3131
implementation("io.github.cdimascio:java-dotenv:5.2.2")
3232
implementation("org.springframework.boot:spring-boot-starter-security")
3333
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
34+
implementation("org.springframework.boot:spring-boot-starter-data-redis")
3435
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0")
3536
implementation("io.jsonwebtoken:jjwt-api:0.12.3")
3637
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.3")

src/main/java/com/back/BackApplication.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,7 @@
1010
public class BackApplication {
1111

1212
public static void main(String[] args) {
13-
Dotenv dotenv = Dotenv.load();
14-
System.out.println("KAKAO_OAUTH2_CLIENT_ID: " + dotenv.get("KAKAO_OAUTH2_CLIENT_ID"));
15-
System.out.println("GOOGLE_OAUTH2_CLIENT_ID: " + dotenv.get("GOOGLE_OAUTH2_CLIENT_ID"));
16-
System.out.println("NAVER_OAUTH2_CLIENT_ID: " + dotenv.get("NAVER_OAUTH2_CLIENT_ID"));
17-
13+
Dotenv.load();
1814
SpringApplication.run(BackApplication.class, args);
1915
}
2016

src/main/java/com/back/global/jwt/JwtUtil.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public class JwtUtil {
2020
private final SecretKey secretKey;
2121
private final long accessTokenExpiration;
2222
private static final String ACCESS_TOKEN_COOKIE_NAME = "accessToken";
23+
private static final String REFRESH_TOKEN_COOKIE_NAME = "refreshToken";
2324

2425
public JwtUtil(@Value("${custom.jwt.secretKey}") String secretKey,
2526
@Value("${custom.accessToken.expirationSeconds}") long accessTokenExpiration) {
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package com.back.global.jwt.refreshToken.entity;
2+
3+
import lombok.*;
4+
import org.springframework.data.annotation.Id;
5+
import org.springframework.data.redis.core.RedisHash;
6+
import org.springframework.data.redis.core.TimeToLive;
7+
import org.springframework.data.redis.core.index.Indexed;
8+
9+
import java.time.LocalDateTime;
10+
11+
@RedisHash("refresh_token")
12+
@Getter
13+
@Setter
14+
@NoArgsConstructor
15+
@AllArgsConstructor
16+
@Builder
17+
public class RefreshToken {
18+
19+
@Id
20+
private String token;
21+
22+
@Indexed
23+
private Long userId;
24+
25+
private String email;
26+
27+
private LocalDateTime createdAt;
28+
29+
@TimeToLive
30+
private Long ttl; // seconds
31+
32+
public static RefreshToken create(String token, Long userId, String email, long ttlSeconds) {
33+
return RefreshToken.builder()
34+
.token(token)
35+
.userId(userId)
36+
.email(email)
37+
.createdAt(LocalDateTime.now())
38+
.ttl(ttlSeconds)
39+
.build();
40+
}
41+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.back.global.jwt.refreshToken.repository;
2+
3+
4+
import com.back.global.jwt.refreshToken.entity.RefreshToken;
5+
import org.springframework.data.repository.CrudRepository;
6+
import org.springframework.stereotype.Repository;
7+
8+
import java.util.Optional;
9+
10+
@Repository
11+
public interface RefreshTokenRepository extends CrudRepository<RefreshToken, String> {
12+
13+
Optional<RefreshToken> findByToken(String token);
14+
15+
void deleteByToken(String token);
16+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.back.global.jwt.refreshToken.service;
2+
3+
4+
import com.back.global.jwt.refreshToken.entity.RefreshToken;
5+
import com.back.global.jwt.refreshToken.repository.RefreshTokenRepository;
6+
import lombok.RequiredArgsConstructor;
7+
import org.springframework.beans.factory.annotation.Value;
8+
import org.springframework.stereotype.Service;
9+
10+
import java.security.SecureRandom;
11+
import java.util.Base64;
12+
import java.util.Optional;
13+
14+
@Service
15+
@RequiredArgsConstructor
16+
public class RefreshTokenService {
17+
18+
private final RefreshTokenRepository refreshTokenRepository;
19+
private final SecureRandom secureRandom = new SecureRandom();
20+
21+
@Value("${custom.refreshToken.expirationSeconds}")
22+
private long refreshTokenExpiration;
23+
24+
// 기존 리프레시 토큰 삭제하고 생성
25+
public String generateRefreshToken(Long userId, String email) {
26+
String token = generateSecureToken();
27+
RefreshToken refreshToken = RefreshToken.create(token, userId, email, refreshTokenExpiration);
28+
refreshTokenRepository.save(refreshToken);
29+
30+
return token;
31+
}
32+
33+
//검증
34+
public boolean validateToken(String token) {
35+
return refreshTokenRepository.findByToken(token).isPresent();
36+
}
37+
38+
//기존 토큰 지우고 발급(회전)
39+
public String rotateToken(String oldToken) {
40+
Optional<RefreshToken> oldRefreshToken = refreshTokenRepository.findByToken(oldToken);
41+
42+
if (oldRefreshToken.isEmpty()) {
43+
throw new IllegalArgumentException("Invalid refresh token");
44+
}
45+
46+
RefreshToken tokenData = oldRefreshToken.get();
47+
revokeToken(oldToken);
48+
49+
return generateRefreshToken(tokenData.getUserId(), tokenData.getEmail());
50+
}
51+
52+
//삭제
53+
public void revokeToken(String token) {
54+
refreshTokenRepository.deleteByToken(token);
55+
}
56+
57+
//문자열 난수 조합
58+
private String generateSecureToken() {
59+
byte[] randomBytes = new byte[32];
60+
secureRandom.nextBytes(randomBytes);
61+
return Base64.getEncoder().withoutPadding().encodeToString(randomBytes);
62+
}
63+
}

src/main/resources/application-dev.yml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,6 @@ spring:
2222
format_sql: true
2323
show_sql: true
2424

25-
# 개발 환경 URL 설정
26-
FRONTEND_URL: http://localhost:3000
27-
BASE_URL: http://localhost:8080
28-
2925
# Swagger 설정
3026
springdoc:
3127
api-docs:

src/main/resources/application.yml

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,18 @@ spring:
55
config:
66
import: optional:file:.env[.properties]
77

8+
data:
9+
redis:
10+
host: ${REDIS_HOST}
11+
port: ${REDIS_PORT}
12+
password: ${REDIS_PASSWORD:}
13+
timeout: 2000ms
14+
lettuce:
15+
pool:
16+
max-active: 8
17+
max-idle: 8
18+
min-idle: 0
19+
820
security:
921
oauth2:
1022
client:
@@ -53,8 +65,12 @@ server:
5365
enabled: true
5466
force: true
5567

68+
69+
5670
custom:
5771
jwt:
5872
secretKey: ${JWT_SECRET_KEY}
5973
accessToken:
60-
expirationSeconds: "#{60*20}"
74+
expirationSeconds: "#{60*15}"
75+
refreshToken:
76+
expirationSeconds: "#{60*60*24*30}"

0 commit comments

Comments
 (0)