Skip to content

Commit bef88eb

Browse files
committed
feat: 리프레쉬 토큰, 인증 코드 Redis로 이관 및 토큰 재발급 로직 수정
1 parent 895e267 commit bef88eb

10 files changed

Lines changed: 220 additions & 45 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.back.web7_9_codecrete_be.domain.auth.entity;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Getter;
5+
6+
@Getter
7+
@AllArgsConstructor
8+
public class RefreshToken {
9+
10+
private Long userId; // 사용자 ID
11+
private String refreshToken; // 실제 토큰 값
12+
private long expiration; // TTL (초 단위)
13+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.back.web7_9_codecrete_be.domain.auth.repository;
2+
3+
import com.back.web7_9_codecrete_be.domain.auth.entity.RefreshToken;
4+
import lombok.RequiredArgsConstructor;
5+
import org.springframework.data.redis.core.RedisTemplate;
6+
import org.springframework.stereotype.Repository;
7+
8+
import java.util.concurrent.TimeUnit;
9+
10+
@Repository
11+
@RequiredArgsConstructor
12+
public class RefreshTokenRedisRepository {
13+
private final RedisTemplate<String, String> redisTemplate;
14+
15+
private static final String REFRESH_TOKEN_PREFIX = "refreshToken: ";
16+
17+
public void save(RefreshToken refreshToken) {
18+
redisTemplate.opsForValue().set(
19+
REFRESH_TOKEN_PREFIX + refreshToken.getUserId(),
20+
refreshToken.getRefreshToken(),
21+
refreshToken.getExpiration(),
22+
TimeUnit.SECONDS
23+
);
24+
}
25+
26+
public String findByUserId(Long userId) {
27+
return (String) redisTemplate.opsForValue().get(REFRESH_TOKEN_PREFIX + userId);
28+
}
29+
30+
public void deleteByUserId(Long userId){
31+
redisTemplate.delete(REFRESH_TOKEN_PREFIX + userId);
32+
}
33+
34+
public boolean existsByUserId(Long userId) {
35+
return redisTemplate.hasKey(REFRESH_TOKEN_PREFIX + userId);
36+
}
37+
}

src/main/java/com/back/web7_9_codecrete_be/domain/auth/repository/RefreshTokenRepository.java

Lines changed: 0 additions & 9 deletions
This file was deleted.

src/main/java/com/back/web7_9_codecrete_be/domain/auth/service/AuthService.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ public void signUp(SignupRequest req) {
5050
.build();
5151

5252
userRepository.save(user);
53+
54+
emailService.clearVerifiedEmail(req.getEmail());
5355
}
5456

5557
// 로그인

src/main/java/com/back/web7_9_codecrete_be/domain/auth/service/TokenService.java

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package com.back.web7_9_codecrete_be.domain.auth.service;
22

3+
import com.back.web7_9_codecrete_be.domain.auth.entity.RefreshToken;
4+
import com.back.web7_9_codecrete_be.domain.auth.repository.RefreshTokenRedisRepository;
35
import com.back.web7_9_codecrete_be.domain.users.entity.User;
6+
import com.back.web7_9_codecrete_be.domain.users.repository.UserRepository;
47
import com.back.web7_9_codecrete_be.global.error.code.AuthErrorCode;
58
import com.back.web7_9_codecrete_be.global.error.exception.BusinessException;
69
import com.back.web7_9_codecrete_be.global.rq.Rq;
@@ -16,6 +19,8 @@ public class TokenService {
1619
private final JwtTokenProvider jwtTokenProvider;
1720
private final JwtProperties jwtProperties;
1821
private final Rq rq;
22+
private final UserRepository userRepository;
23+
private final RefreshTokenRedisRepository refreshTokenRedisRepository;
1924

2025
// 로그인 시 실행 시 쿠키에 토큰 발급
2126
public void issueTokens(User user) {
@@ -24,13 +29,21 @@ public void issueTokens(User user) {
2429

2530
rq.setCookie("ACCESS_TOKEN", access, (int) jwtProperties.getAccessTokenExpiration());
2631
rq.setCookie("REFRESH_TOKEN", refresh, (int) jwtProperties.getRefreshTokenExpiration());
32+
33+
refreshTokenRedisRepository.save(
34+
new RefreshToken(
35+
user.getId(),
36+
refresh,
37+
jwtProperties.getRefreshTokenExpiration()
38+
)
39+
);
2740
}
2841

2942
// 로그아웃 시 실행 시 쿠키 삭제
3043
public void removeTokens(User user) {
3144
rq.removeCookie("ACCESS_TOKEN");
3245
rq.removeCookie("REFRESH_TOKEN");
33-
46+
refreshTokenRedisRepository.deleteByUserId(user.getId());
3447
}
3548

3649
public String reissueAccessToken() {
@@ -47,6 +60,16 @@ public String reissueAccessToken() {
4760
// RefreshToken에서 email(Subject) 추출
4861
String email = jwtTokenProvider.getEmailFromToken(refresh);
4962

63+
User user = userRepository.findByEmail(email)
64+
.orElseThrow(() -> new BusinessException(AuthErrorCode.USER_NOT_FOUND));
65+
66+
String savedRefresh =
67+
refreshTokenRedisRepository.findByUserId(user.getId());
68+
69+
if (savedRefresh == null || !savedRefresh.equals(refresh)) {
70+
throw new BusinessException(AuthErrorCode.INVALID_TOKEN);
71+
}
72+
5073
// AccessToken 재발급
5174
String newAccess = jwtTokenProvider.generateAccessToken(email);
5275

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.back.web7_9_codecrete_be.domain.email.repository;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import org.springframework.data.redis.core.RedisTemplate;
5+
import org.springframework.stereotype.Repository;
6+
7+
import java.util.concurrent.TimeUnit;
8+
9+
@Repository
10+
@RequiredArgsConstructor
11+
public class VerificationCodeRedisRepository {
12+
13+
private final RedisTemplate<String, String> redisTemplate;
14+
15+
private static final String PREFIX = "email:verify:";
16+
17+
public void save(String email, String code, long ttlSeconds) {
18+
redisTemplate.opsForValue().set(
19+
PREFIX + email,
20+
code,
21+
ttlSeconds,
22+
TimeUnit.SECONDS
23+
);
24+
}
25+
26+
public String findByEmail(String email) {
27+
return redisTemplate.opsForValue().get(PREFIX + email);
28+
}
29+
30+
public void deleteByEmail(String email) {
31+
redisTemplate.delete(PREFIX + email);
32+
}
33+
34+
public boolean existsByEmail(String email) {
35+
return redisTemplate.hasKey(PREFIX + email);
36+
}
37+
}

src/main/java/com/back/web7_9_codecrete_be/domain/email/repository/VerifiedEmailRepository.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@
55

66
public interface VerifiedEmailRepository extends JpaRepository<VerifiedEmail, String> {
77
boolean existsByEmail(String email);
8+
void deleteByEmail(String email);
89
}

src/main/java/com/back/web7_9_codecrete_be/domain/email/service/EmailService.java

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
package com.back.web7_9_codecrete_be.domain.email.service;
22

3-
import com.back.web7_9_codecrete_be.domain.email.entity.VerificationCode;
43
import com.back.web7_9_codecrete_be.domain.email.entity.VerifiedEmail;
5-
import com.back.web7_9_codecrete_be.domain.email.repository.VerificationCodeRepository;
4+
import com.back.web7_9_codecrete_be.domain.email.repository.VerificationCodeRedisRepository;
65
import com.back.web7_9_codecrete_be.domain.email.repository.VerifiedEmailRepository;
76
import com.back.web7_9_codecrete_be.global.error.code.MailErrorCode;
87
import com.back.web7_9_codecrete_be.global.error.exception.BusinessException;
@@ -17,14 +16,13 @@
1716
import org.springframework.web.reactive.function.client.WebClient;
1817

1918
import java.security.SecureRandom;
20-
import java.time.LocalDateTime;
2119

2220
@Slf4j
2321
@Service
2422
@RequiredArgsConstructor
2523
public class EmailService {
2624

27-
private final VerificationCodeRepository verificationCodeRepository;
25+
private final VerificationCodeRedisRepository verificationCodeRedisRepository;
2826
private final VerifiedEmailRepository verifiedEmailRepository;
2927
private final WebClient mailgunClient;
3028

@@ -66,16 +64,10 @@ public void createAndSendVerificationCode(String email) {
6664
String code = generateVerificationCode();
6765

6866
// 기존 코드 있으면 삭제
69-
verificationCodeRepository.deleteByEmail(email);
67+
verificationCodeRedisRepository.deleteByEmail(email);
7068

71-
// DB 저장
72-
VerificationCode entity = VerificationCode.builder()
73-
.email(email)
74-
.code(code)
75-
.expireAt(LocalDateTime.now().plusSeconds(TTL_SECONDS))
76-
.build();
77-
78-
verificationCodeRepository.save(entity);
69+
// Redis 저장 (TTL 5분)
70+
verificationCodeRedisRepository.save(email, code, TTL_SECONDS);
7971

8072
String content = """
8173
안녕하세요. NCB 입니다.
@@ -103,23 +95,20 @@ private String generateVerificationCode() {
10395
// 인증코드 검증
10496
@Transactional
10597
public void verifyCode(String email, String inputCode) {
106-
VerificationCode saved = verificationCodeRepository.findByEmail(email)
107-
.orElseThrow(() -> new BusinessException(MailErrorCode.VERIFICATION_CODE_EXPIRED));
98+
String savedCode = verificationCodeRedisRepository.findByEmail(email);
10899

109-
if (saved.isExpired()) {
110-
verificationCodeRepository.deleteByEmail(email);
100+
if (savedCode == null) {
111101
throw new BusinessException(MailErrorCode.VERIFICATION_CODE_EXPIRED);
112102
}
113103

114-
if (!saved.getCode().equals(inputCode)) {
104+
if (!savedCode.equals(inputCode)) {
115105
throw new BusinessException(MailErrorCode.VERIFICATION_CODE_MISMATCH);
116106
}
117107

118-
// 성공 시 삭제
119-
verificationCodeRepository.deleteByEmail(email);
120-
108+
// 성공 시 Redis에서 삭제
109+
verificationCodeRedisRepository.deleteByEmail(email);
121110

122-
// 인증 완료 상태 저장
111+
// 인증 완료 상태 저장 (DB)
123112
verifiedEmailRepository.save(new VerifiedEmail(email));
124113

125114
log.info("[이메일 인증 성공] {}", email);
@@ -142,4 +131,9 @@ public void sendNewPassword(String email, String newPassword) {
142131

143132
sendEmail(email, "[NCB] 임시 비밀번호 안내", content);
144133
}
134+
135+
@Transactional
136+
public void clearVerifiedEmail(String email) {
137+
verifiedEmailRepository.deleteByEmail(email);
138+
}
145139
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package com.back.web7_9_codecrete_be.global.config;
2+
3+
import org.springframework.beans.factory.annotation.Value;
4+
import org.springframework.context.annotation.Bean;
5+
import org.springframework.context.annotation.Configuration;
6+
import org.springframework.data.redis.connection.RedisConnectionFactory;
7+
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
8+
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
9+
import org.springframework.data.redis.core.RedisTemplate;
10+
import org.springframework.data.redis.core.StringRedisTemplate;
11+
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
12+
import org.springframework.data.redis.serializer.StringRedisSerializer;
13+
14+
@Configuration
15+
public class RedisConfig {
16+
@Value("${spring.data.redis.host}")
17+
private String host;
18+
19+
@Value("${spring.data.redis.port}")
20+
private int port;
21+
22+
//Redis와 연결을 위한 Connection 생성
23+
@Bean
24+
public RedisConnectionFactory redisConnectionFactory() {
25+
final RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration(host,port);
26+
return new LettuceConnectionFactory(configuration);
27+
}
28+
29+
//문자열만 사용할 때(refreshToken)
30+
@Bean
31+
public StringRedisTemplate stringRedisTemplate() {
32+
return new StringRedisTemplate(redisConnectionFactory());
33+
}
34+
35+
//Json 직렬화 필요할 때(복잡한 객체 저장 등)
36+
@Bean
37+
public RedisTemplate<String, Object> redisTemplate() {
38+
RedisTemplate<String, Object> template = new RedisTemplate<>();
39+
40+
template.setKeySerializer(new StringRedisSerializer());
41+
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
42+
43+
template.setHashKeySerializer(new StringRedisSerializer());
44+
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
45+
46+
template.setConnectionFactory(redisConnectionFactory());
47+
return template;
48+
}
49+
}

0 commit comments

Comments
 (0)