From 7496060ff1cec0c418c99e194b756f79a57cd04c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=A9=E1=84=8B=E1=85=B2=E1=84=8E=E1=85=A1?= =?UTF-8?q?=E1=86=AB?= Date: Tue, 14 Jan 2025 11:50:10 +0900 Subject: [PATCH 01/14] =?UTF-8?q?:wrench:=20redis=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 55c06dd..f801c69 100644 --- a/build.gradle +++ b/build.gradle @@ -46,6 +46,7 @@ dependencies { // JPA & DB implementation 'org.springframework.boot:spring-boot-starter-data-jpa' runtimeOnly 'com.mysql:mysql-connector-j' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' // Lombok compileOnly 'org.projectlombok:lombok' From 74386e1597fe7ab9b57dbb5deb82008d923a9eb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=A9=E1=84=8B=E1=85=B2=E1=84=8E=E1=85=A1?= =?UTF-8?q?=E1=86=AB?= Date: Tue, 14 Jan 2025 11:51:05 +0900 Subject: [PATCH 02/14] =?UTF-8?q?:truck:=20Properties=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=20=EC=9C=84=EC=B9=98=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/mycom/socket/auth/{jwt => config}/JWTProperties.java | 2 +- src/main/java/com/mycom/socket/auth/config/SecurityConfig.java | 1 - src/main/java/com/mycom/socket/auth/jwt/JWTFilter.java | 1 + src/main/java/com/mycom/socket/auth/jwt/JWTUtil.java | 1 + src/main/java/com/mycom/socket/auth/security/CookieUtil.java | 2 +- .../com/mycom/socket/member/service/LoginIntegrationTest.java | 2 +- 6 files changed, 5 insertions(+), 4 deletions(-) rename src/main/java/com/mycom/socket/auth/{jwt => config}/JWTProperties.java (92%) diff --git a/src/main/java/com/mycom/socket/auth/jwt/JWTProperties.java b/src/main/java/com/mycom/socket/auth/config/JWTProperties.java similarity index 92% rename from src/main/java/com/mycom/socket/auth/jwt/JWTProperties.java rename to src/main/java/com/mycom/socket/auth/config/JWTProperties.java index ad1e8d6..f35a579 100644 --- a/src/main/java/com/mycom/socket/auth/jwt/JWTProperties.java +++ b/src/main/java/com/mycom/socket/auth/config/JWTProperties.java @@ -1,4 +1,4 @@ -package com.mycom.socket.auth.jwt; +package com.mycom.socket.auth.config; import lombok.Getter; import lombok.Setter; import org.springframework.boot.context.properties.ConfigurationProperties; diff --git a/src/main/java/com/mycom/socket/auth/config/SecurityConfig.java b/src/main/java/com/mycom/socket/auth/config/SecurityConfig.java index 14bbcdf..0ecda28 100644 --- a/src/main/java/com/mycom/socket/auth/config/SecurityConfig.java +++ b/src/main/java/com/mycom/socket/auth/config/SecurityConfig.java @@ -1,7 +1,6 @@ package com.mycom.socket.auth.config; import com.mycom.socket.auth.jwt.JWTFilter; -import com.mycom.socket.auth.jwt.JWTProperties; import com.mycom.socket.auth.jwt.JWTUtil; import com.mycom.socket.auth.service.MemberDetailsService; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/mycom/socket/auth/jwt/JWTFilter.java b/src/main/java/com/mycom/socket/auth/jwt/JWTFilter.java index f6ca338..97f27d9 100644 --- a/src/main/java/com/mycom/socket/auth/jwt/JWTFilter.java +++ b/src/main/java/com/mycom/socket/auth/jwt/JWTFilter.java @@ -1,5 +1,6 @@ package com.mycom.socket.auth.jwt; +import com.mycom.socket.auth.config.JWTProperties; import com.mycom.socket.auth.service.MemberDetailsService; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; diff --git a/src/main/java/com/mycom/socket/auth/jwt/JWTUtil.java b/src/main/java/com/mycom/socket/auth/jwt/JWTUtil.java index b7e3a3b..a8b963b 100644 --- a/src/main/java/com/mycom/socket/auth/jwt/JWTUtil.java +++ b/src/main/java/com/mycom/socket/auth/jwt/JWTUtil.java @@ -1,5 +1,6 @@ package com.mycom.socket.auth.jwt; +import com.mycom.socket.auth.config.JWTProperties; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/com/mycom/socket/auth/security/CookieUtil.java b/src/main/java/com/mycom/socket/auth/security/CookieUtil.java index 422e6c1..1826b31 100644 --- a/src/main/java/com/mycom/socket/auth/security/CookieUtil.java +++ b/src/main/java/com/mycom/socket/auth/security/CookieUtil.java @@ -1,6 +1,6 @@ package com.mycom.socket.auth.security; -import com.mycom.socket.auth.jwt.JWTProperties; +import com.mycom.socket.auth.config.JWTProperties; import jakarta.servlet.http.Cookie; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; diff --git a/src/test/java/com/mycom/socket/member/service/LoginIntegrationTest.java b/src/test/java/com/mycom/socket/member/service/LoginIntegrationTest.java index 86f7f46..bcff139 100644 --- a/src/test/java/com/mycom/socket/member/service/LoginIntegrationTest.java +++ b/src/test/java/com/mycom/socket/member/service/LoginIntegrationTest.java @@ -2,7 +2,7 @@ import com.mycom.socket.auth.dto.request.LoginRequest; import com.mycom.socket.auth.dto.response.LoginResponse; -import com.mycom.socket.auth.jwt.JWTProperties; +import com.mycom.socket.auth.config.JWTProperties; import com.mycom.socket.auth.service.AuthService; import com.mycom.socket.go_socket.entity.Member; import com.mycom.socket.go_socket.entity.enums.MemberRole; From 72969d82485e09a8cffd1a4836025f16cea75df3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=A9=E1=84=8B=E1=85=B2=E1=84=8E=E1=85=A1?= =?UTF-8?q?=E1=86=AB?= Date: Tue, 14 Jan 2025 11:51:10 +0900 Subject: [PATCH 03/14] =?UTF-8?q?:truck:=20Properties=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=20=EC=9C=84=EC=B9=98=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/mycom/socket/member/controller/AuthControllerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/mycom/socket/member/controller/AuthControllerTest.java b/src/test/java/com/mycom/socket/member/controller/AuthControllerTest.java index 2452fd3..b964102 100644 --- a/src/test/java/com/mycom/socket/member/controller/AuthControllerTest.java +++ b/src/test/java/com/mycom/socket/member/controller/AuthControllerTest.java @@ -5,7 +5,7 @@ import com.mycom.socket.auth.controller.AuthController; import com.mycom.socket.auth.dto.request.RegisterRequest; import com.mycom.socket.auth.dto.response.RegisterResponse; -import com.mycom.socket.auth.jwt.JWTProperties; +import com.mycom.socket.auth.config.JWTProperties; import com.mycom.socket.auth.jwt.JWTUtil; import com.mycom.socket.auth.service.AuthService; import com.mycom.socket.auth.service.MailService; From ecc236bf878b6e4b7969aadb9a507fe112ee5f61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=A9=E1=84=8B=E1=85=B2=E1=84=8E=E1=85=A1?= =?UTF-8?q?=E1=86=AB?= Date: Tue, 14 Jan 2025 11:51:34 +0900 Subject: [PATCH 04/14] =?UTF-8?q?:fire:=20Redis=20=EB=8F=84=EC=9E=85?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20RateLimiter=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../socket/auth/service/RateLimiter.java | 47 ------------------- 1 file changed, 47 deletions(-) delete mode 100644 src/main/java/com/mycom/socket/auth/service/RateLimiter.java diff --git a/src/main/java/com/mycom/socket/auth/service/RateLimiter.java b/src/main/java/com/mycom/socket/auth/service/RateLimiter.java deleted file mode 100644 index 9864a48..0000000 --- a/src/main/java/com/mycom/socket/auth/service/RateLimiter.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.mycom.socket.auth.service; - -import com.mycom.socket.global.exception.BaseException; -import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; - -import java.time.Duration; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -@Component -@RequiredArgsConstructor -public class RateLimiter { - private final Map> requestMap = new ConcurrentHashMap<>(); - private static final int MAX_REQUESTS = 3; // 1분당 최대 3번 - private static final Duration WINDOW_SIZE = Duration.ofMinutes(1); // 1분의 시간 간격 - - @Scheduled(fixedRate = 3600000) // 1시간마다 실행 - public void cleanup() { - LocalDateTime threshold = LocalDateTime.now().minus(WINDOW_SIZE); - requestMap.entrySet().removeIf(entry -> - entry.getValue().stream().allMatch(time -> time.isBefore(threshold))); - } - - public void checkRateLimit(String email) { - List requests = requestMap.computeIfAbsent(email, k -> new ArrayList<>()); - LocalDateTime now = LocalDateTime.now(); - - requests.removeIf(requestTime -> - requestTime.plus(WINDOW_SIZE).isBefore(now)); - - if (requests.size() >= MAX_REQUESTS) { - LocalDateTime oldestRequest = requests.get(0); - Duration waitTime = WINDOW_SIZE.minus(Duration.between(oldestRequest, now)); - throw new BaseException( - String.format("너무 많은 요청입니다. %d초 후에 다시 시도해주세요.",waitTime.getSeconds()), - HttpStatus.TOO_MANY_REQUESTS); - } - - requests.add(now); - } -} From 4391b23446dd5c0c0ccad0c4737e8ffddbcbf343 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=A9=E1=84=8B=E1=85=B2=E1=84=8E=E1=85=A1?= =?UTF-8?q?=E1=86=AB?= Date: Tue, 14 Jan 2025 11:51:49 +0900 Subject: [PATCH 05/14] =?UTF-8?q?:sparkles:=20Redis=20=EB=8F=84=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../socket/global/config/RedisConfig.java | 29 ++++++++++++++ .../socket/global/config/RedisProperties.java | 16 ++++++++ .../socket/global/service/RedisService.java | 39 +++++++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 src/main/java/com/mycom/socket/global/config/RedisConfig.java create mode 100644 src/main/java/com/mycom/socket/global/config/RedisProperties.java create mode 100644 src/main/java/com/mycom/socket/global/service/RedisService.java diff --git a/src/main/java/com/mycom/socket/global/config/RedisConfig.java b/src/main/java/com/mycom/socket/global/config/RedisConfig.java new file mode 100644 index 0000000..a5d35a6 --- /dev/null +++ b/src/main/java/com/mycom/socket/global/config/RedisConfig.java @@ -0,0 +1,29 @@ +package com.mycom.socket.global.config; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +@RequiredArgsConstructor +@EnableRedisRepositories +public class RedisConfig { + private final RedisProperties redisProperties; + + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory( + new LettuceConnectionFactory(redisProperties.getHost(), redisProperties.getPort()) + ); + + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new StringRedisSerializer()); + return redisTemplate; + } + +} diff --git a/src/main/java/com/mycom/socket/global/config/RedisProperties.java b/src/main/java/com/mycom/socket/global/config/RedisProperties.java new file mode 100644 index 0000000..5add8bb --- /dev/null +++ b/src/main/java/com/mycom/socket/global/config/RedisProperties.java @@ -0,0 +1,16 @@ +package com.mycom.socket.global.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Getter +@Setter +@Component +@ConfigurationProperties(prefix = "spring.data.redis") +public class RedisProperties { + + private String host; + private int port; +} diff --git a/src/main/java/com/mycom/socket/global/service/RedisService.java b/src/main/java/com/mycom/socket/global/service/RedisService.java new file mode 100644 index 0000000..d161cbf --- /dev/null +++ b/src/main/java/com/mycom/socket/global/service/RedisService.java @@ -0,0 +1,39 @@ +package com.mycom.socket.global.service; + +import com.mycom.socket.global.exception.BaseException; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; + +import java.util.concurrent.TimeUnit; + +@Service +@RequiredArgsConstructor +public class RedisService { + private final RedisTemplate redisTemplate; + private static final long VERIFICATION_TTL = 180; // 3분 + + // 인증 코드 저장 + public void saveCode(String email, String code) { + redisTemplate.opsForValue().set(email, code, VERIFICATION_TTL, TimeUnit.SECONDS); + } + + // 인증 코드 조회 + public String getCode(String email) { + Object code = redisTemplate.opsForValue().get(email); + if (code == null) { + throw new BaseException("인증 코드가 만료되었거나 존재하지 않습니다.", HttpStatus.BAD_REQUEST); + } + return code.toString(); + } + + // 요청 횟수 증가 (rate limiting) + public Long incrementCount(String email) { + Long count = redisTemplate.opsForValue().increment(email); + if (count == 1) { + redisTemplate.expire(email, 60, TimeUnit.SECONDS); + } + return count; + } +} From d16583223cb6894f685b64e57d2c66028c24d0ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=A9=E1=84=8B=E1=85=B2=E1=84=8E=E1=85=A1?= =?UTF-8?q?=E1=86=AB?= Date: Tue, 14 Jan 2025 11:52:02 +0900 Subject: [PATCH 06/14] =?UTF-8?q?:recycle:=20api=20url=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/mycom/socket/auth/controller/AuthController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/mycom/socket/auth/controller/AuthController.java b/src/main/java/com/mycom/socket/auth/controller/AuthController.java index b9c624c..bb7c70b 100644 --- a/src/main/java/com/mycom/socket/auth/controller/AuthController.java +++ b/src/main/java/com/mycom/socket/auth/controller/AuthController.java @@ -38,12 +38,12 @@ public void logout(HttpServletResponse response) { authService.logout(response); } - @PostMapping("/verification") + @PostMapping("/verify-email") public EmailVerificationResponse sendVerificationEmail(@Valid @RequestBody EmailRequest request) { return mailService.sendMail(request.email()); } - @PostMapping("/email/verify") + @PostMapping("/verification-code") public EmailVerificationResponse verifyEmail(@Valid @RequestBody EmailVerificationRequest request) { return mailService.verifyCode(request.email(), request.code()); } From fa72e44b5e7ae441ac683d8a65925f0bcabd9fd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=A9=E1=84=8B=E1=85=B2=E1=84=8E=E1=85=A1?= =?UTF-8?q?=E1=86=AB?= Date: Tue, 14 Jan 2025 11:52:17 +0900 Subject: [PATCH 07/14] =?UTF-8?q?:recycle:=20MailService=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../socket/auth/config/MailProperties.java | 21 +++++++ .../socket/auth/service/MailService.java | 56 ++++++++----------- 2 files changed, 44 insertions(+), 33 deletions(-) create mode 100644 src/main/java/com/mycom/socket/auth/config/MailProperties.java diff --git a/src/main/java/com/mycom/socket/auth/config/MailProperties.java b/src/main/java/com/mycom/socket/auth/config/MailProperties.java new file mode 100644 index 0000000..1c1e8ca --- /dev/null +++ b/src/main/java/com/mycom/socket/auth/config/MailProperties.java @@ -0,0 +1,21 @@ +package com.mycom.socket.auth.config; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Getter +@Setter +@Component +@ConfigurationProperties(prefix = "mail") +public class MailProperties { + private String host; + private int port; + private String protocol; + private String username; + private String password; + private String senderEmail; + private String subject; + private String bodyTemplate; +} \ No newline at end of file diff --git a/src/main/java/com/mycom/socket/auth/service/MailService.java b/src/main/java/com/mycom/socket/auth/service/MailService.java index 6a2d1ec..dd9f112 100644 --- a/src/main/java/com/mycom/socket/auth/service/MailService.java +++ b/src/main/java/com/mycom/socket/auth/service/MailService.java @@ -1,13 +1,16 @@ package com.mycom.socket.auth.service; +import com.mycom.socket.auth.config.MailProperties; import com.mycom.socket.auth.dto.response.EmailVerificationResponse; import com.mycom.socket.auth.service.data.VerificationData; import com.mycom.socket.global.exception.BaseException; +import com.mycom.socket.global.service.RedisService; import jakarta.mail.MessagingException; import jakarta.mail.internet.MimeMessage; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; +import org.springframework.mail.MailSendException; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; @@ -21,12 +24,9 @@ public class MailService { private final JavaMailSender javaMailSender; - private final RateLimiter rateLimiter; // 인증 번호 요청 제한 + private final RedisService redisService; + private final MailProperties mailProperties; - private final Map verificationDataMap = new ConcurrentHashMap<>(); - - @Value("${spring.mail.username}") - private String senderEmail; /** * 6자리 난수 인증번호 생성 @@ -48,14 +48,10 @@ private String createVerificationCode() { public MimeMessage createMail(String email, String verificationCode) { MimeMessage message = javaMailSender.createMimeMessage(); try { - message.setFrom(senderEmail); + message.setFrom(mailProperties.getUsername()); message.setRecipients(MimeMessage.RecipientType.TO, email); message.setSubject("이메일 인증"); - String body = String.format(""" -

요청하신 인증 번호입니다.

-

%s

-

감사합니다.

- """, verificationCode); + String body = String.format(mailProperties.getBodyTemplate(), verificationCode); message.setText(body, "UTF-8", "html"); } catch (MessagingException e) { throw new BaseException("이메일 생성 중 오류가 발생했습니다: " + e.getMessage(), @@ -70,16 +66,24 @@ public MimeMessage createMail(String email, String verificationCode) { * @return 생성된 인증번호 */ public EmailVerificationResponse sendMail(String email) { - rateLimiter.checkRateLimit(email); + if (redisService.incrementCount(email) > 3) { + throw new BaseException("너무 많은 요청입니다. 1분 후에 다시 시도해주세요.", + HttpStatus.TOO_MANY_REQUESTS); + } String verificationCode = createVerificationCode(); - verificationDataMap.put(email, new VerificationData(verificationCode)); + redisService.saveCode(email, verificationCode); MimeMessage message = createMail(email, verificationCode); try { javaMailSender.send(message); - return EmailVerificationResponse.of("이메일 전송 성공"); - } catch (Exception e) { + return EmailVerificationResponse.of("이메일 인증이 완료되었습니다."); + } catch (MailSendException e) { + // 메일 전송 실패 시 재시도 로직 추가 + throw new BaseException("이메일 발송 중 오류가 발생했습니다: " + e.getMessage(), + HttpStatus.INTERNAL_SERVER_ERROR); + } + catch (Exception e) { throw new BaseException("이메일 발송 중 오류가 발생했습니다: " + e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); } @@ -93,31 +97,17 @@ public EmailVerificationResponse sendMail(String email) { * @return 인증번호 일치 여부 */ public EmailVerificationResponse verifyCode(String email, String code) { - validateVerificationCode(code); - - VerificationData data = verificationDataMap.get(email); - if (data == null || data.isExpired()) { - throw new BaseException("인증 코드가 만료되었거나 존재하지 않습니다.", HttpStatus.BAD_REQUEST); + if (!code.matches("\\d{6}") || !StringUtils.hasText(code)) { + throw new BaseException("유효하지 않은 인증 코드 형식입니다.", HttpStatus.BAD_REQUEST); } - if (!data.code().equals(code)) { + String savedCode = redisService.getCode(email); + if (!code.equals(savedCode)) { throw new BaseException("인증 코드가 일치하지 않습니다.", HttpStatus.BAD_REQUEST); } - verificationDataMap.put(email, data.withVerified()); return EmailVerificationResponse.of("이메일 인증이 완료되었습니다."); } - private void validateVerificationCode(String code) { - if (!StringUtils.hasText(code) || !code.matches("\\d{6}")) { - throw new BaseException("유효하지 않은 인증 코드 형식입니다.", HttpStatus.BAD_REQUEST); - } - } - - public boolean isEmailVerified(String email) { - VerificationData data = verificationDataMap.get(email); - return data != null && !data.isExpired() && data.verified(); - } - } From 81dbc82b2359e13fbbf87ee987e80c1c6cd35b9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=A9=E1=84=8B=E1=85=B2=E1=84=8E=E1=85=A1?= =?UTF-8?q?=E1=86=AB?= Date: Tue, 14 Jan 2025 14:36:13 +0900 Subject: [PATCH 08/14] =?UTF-8?q?:recycle:=20MailService=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../socket/auth/config/MailProperties.java | 3 +- .../socket/auth/service/MailService.java | 40 ++++------ .../socket/global/config/RedisConfig.java | 9 ++- .../socket/global/service/RedisService.java | 74 +++++++++++++++---- 4 files changed, 84 insertions(+), 42 deletions(-) diff --git a/src/main/java/com/mycom/socket/auth/config/MailProperties.java b/src/main/java/com/mycom/socket/auth/config/MailProperties.java index 1c1e8ca..0d76b70 100644 --- a/src/main/java/com/mycom/socket/auth/config/MailProperties.java +++ b/src/main/java/com/mycom/socket/auth/config/MailProperties.java @@ -8,7 +8,7 @@ @Getter @Setter @Component -@ConfigurationProperties(prefix = "mail") +@ConfigurationProperties(prefix = "spring.mail") public class MailProperties { private String host; private int port; @@ -16,6 +16,7 @@ public class MailProperties { private String username; private String password; private String senderEmail; + private String senderName; private String subject; private String bodyTemplate; } \ No newline at end of file diff --git a/src/main/java/com/mycom/socket/auth/service/MailService.java b/src/main/java/com/mycom/socket/auth/service/MailService.java index dd9f112..7504fc3 100644 --- a/src/main/java/com/mycom/socket/auth/service/MailService.java +++ b/src/main/java/com/mycom/socket/auth/service/MailService.java @@ -2,22 +2,16 @@ import com.mycom.socket.auth.config.MailProperties; import com.mycom.socket.auth.dto.response.EmailVerificationResponse; -import com.mycom.socket.auth.service.data.VerificationData; import com.mycom.socket.global.exception.BaseException; import com.mycom.socket.global.service.RedisService; import jakarta.mail.MessagingException; import jakarta.mail.internet.MimeMessage; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; -import org.springframework.mail.MailSendException; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.stereotype.Service; -import org.springframework.util.StringUtils; import java.security.SecureRandom; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; @Service @RequiredArgsConstructor @@ -27,17 +21,17 @@ public class MailService { private final RedisService redisService; private final MailProperties mailProperties; - /** - * 6자리 난수 인증번호 생성 - * SecureRandom 사용하여 보안성 향상 - * @return 100000~999999 범위의 인증번호 + * 6자리 인증번호 생성 (100000-999999) */ private String createVerificationCode() { // Math.random()은 예측 가능한 난수를 생성할 수 있어 보안에 취약 // SecureRandom은 암호학적으로 안전한 난수를 생성하므로 인증번호 생성에 더 적합 - SecureRandom secureRandom = new SecureRandom(); - return String.format("%06d", secureRandom.nextInt(1000000)); + return String.format("%06d", new SecureRandom().nextInt(1000000)); + } + + public boolean isEmailVerified(String email) { + return redisService.isEmailVerified(email); } /** @@ -72,18 +66,13 @@ public EmailVerificationResponse sendMail(String email) { } String verificationCode = createVerificationCode(); - redisService.saveCode(email, verificationCode); + redisService.saveCode(verificationCode); MimeMessage message = createMail(email, verificationCode); try { javaMailSender.send(message); - return EmailVerificationResponse.of("이메일 인증이 완료되었습니다."); - } catch (MailSendException e) { - // 메일 전송 실패 시 재시도 로직 추가 - throw new BaseException("이메일 발송 중 오류가 발생했습니다: " + e.getMessage(), - HttpStatus.INTERNAL_SERVER_ERROR); - } - catch (Exception e) { + return EmailVerificationResponse.of("이메일 전송 성공"); // 메시지 수정 + } catch (Exception e) { throw new BaseException("이메일 발송 중 오류가 발생했습니다: " + e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); } @@ -97,16 +86,17 @@ public EmailVerificationResponse sendMail(String email) { * @return 인증번호 일치 여부 */ public EmailVerificationResponse verifyCode(String email, String code) { - if (!code.matches("\\d{6}") || !StringUtils.hasText(code)) { + if (!code.matches("\\d{6}")) { throw new BaseException("유효하지 않은 인증 코드 형식입니다.", HttpStatus.BAD_REQUEST); } - String savedCode = redisService.getCode(email); - if (!code.equals(savedCode)) { + try { + redisService.getCode(code); // 인증코드 검증 + redisService.saveVerifiedEmail(email); // 인증된 이메일 저장 + return EmailVerificationResponse.of("이메일 인증이 완료되었습니다."); + } catch (Exception e) { throw new BaseException("인증 코드가 일치하지 않습니다.", HttpStatus.BAD_REQUEST); } - - return EmailVerificationResponse.of("이메일 인증이 완료되었습니다."); } } diff --git a/src/main/java/com/mycom/socket/global/config/RedisConfig.java b/src/main/java/com/mycom/socket/global/config/RedisConfig.java index a5d35a6..fd706e4 100644 --- a/src/main/java/com/mycom/socket/global/config/RedisConfig.java +++ b/src/main/java/com/mycom/socket/global/config/RedisConfig.java @@ -14,12 +14,15 @@ public class RedisConfig { private final RedisProperties redisProperties; + @Bean + public LettuceConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(redisProperties.getHost(), redisProperties.getPort()); + } + @Bean public RedisTemplate redisTemplate() { RedisTemplate redisTemplate = new RedisTemplate<>(); - redisTemplate.setConnectionFactory( - new LettuceConnectionFactory(redisProperties.getHost(), redisProperties.getPort()) - ); + redisTemplate.setConnectionFactory(redisConnectionFactory()); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new StringRedisSerializer()); diff --git a/src/main/java/com/mycom/socket/global/service/RedisService.java b/src/main/java/com/mycom/socket/global/service/RedisService.java index d161cbf..48101b6 100644 --- a/src/main/java/com/mycom/socket/global/service/RedisService.java +++ b/src/main/java/com/mycom/socket/global/service/RedisService.java @@ -6,34 +6,82 @@ import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; -import java.util.concurrent.TimeUnit; +import java.time.Duration; @Service @RequiredArgsConstructor public class RedisService { private final RedisTemplate redisTemplate; - private static final long VERIFICATION_TTL = 180; // 3분 - // 인증 코드 저장 - public void saveCode(String email, String code) { - redisTemplate.opsForValue().set(email, code, VERIFICATION_TTL, TimeUnit.SECONDS); + /** + * Redis Key Prefix 상수 + */ + private static final String VERIFIED_EMAIL_PREFIX = "verified:email:"; + private static final String RATE_LIMIT_PREFIX = "rate-limit:"; + + /** + * Redis TTL 상수 + */ + private static final Duration VERIFICATION_TTL = Duration.ofMinutes(3); // 인증번호 유효시간 + private static final Duration VERIFIED_EMAIL_TTL = Duration.ofMinutes(30); // 인증된 이메일 유효시간 + private static final Duration RATE_LIMIT_TTL = Duration.ofMinutes(1); // 요청 제한 시간 + + + /** + * 인증번호를 Redis에 저장 + * Key와 Value로 동일한 인증번호를 사용 + * 3분 후 자동 삭제 + */ + public void saveCode(String code) { + redisTemplate.opsForValue().set(code, code, VERIFICATION_TTL); } - // 인증 코드 조회 - public String getCode(String email) { - Object code = redisTemplate.opsForValue().get(email); - if (code == null) { + /** + * Redis에서 인증번호 조회 + * 인증번호가 존재하지 않거나 만료된 경우 예외 발생 + * @throws BaseException 인증번호가 만료되었거나 존재하지 않는 경우 + */ + public String getCode(String code) { + Object savedCode = redisTemplate.opsForValue().get(code); + if (savedCode == null) { throw new BaseException("인증 코드가 만료되었거나 존재하지 않습니다.", HttpStatus.BAD_REQUEST); } - return code.toString(); + return savedCode.toString(); } - // 요청 횟수 증가 (rate limiting) + /** + * 이메일별 요청 횟수 증가 (Rate Limiting) + * 첫 요청시 1분 후 자동 삭제되도록 설정 + * @return 현재 요청 횟수 + */ public Long incrementCount(String email) { - Long count = redisTemplate.opsForValue().increment(email); + String key = RATE_LIMIT_PREFIX + email; + Long count = redisTemplate.opsForValue().increment(key); if (count == 1) { - redisTemplate.expire(email, 60, TimeUnit.SECONDS); + redisTemplate.expire(key, RATE_LIMIT_TTL); } return count; } + + /** + * 인증된 이메일 정보 저장 + * 30분 동안 유효 + */ + public void saveVerifiedEmail(String email) { + redisTemplate.opsForValue().set( + VERIFIED_EMAIL_PREFIX + email, + "true", + VERIFIED_EMAIL_TTL + ); + } + + /** + * 이메일 인증 여부 확인 + * @return 이메일이 인증되었으면 true, 아니면 false + */ + public boolean isEmailVerified(String email) { + Object verified = redisTemplate.opsForValue().get(VERIFIED_EMAIL_PREFIX + email); + return "true".equals(verified); + } + } From f307fc3e2c5f373962c34ff11145c9bfbb9b48ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=A9=E1=84=8B=E1=85=B2=E1=84=8E=E1=85=A1?= =?UTF-8?q?=E1=86=AB?= Date: Tue, 14 Jan 2025 15:08:59 +0900 Subject: [PATCH 09/14] =?UTF-8?q?:recycle:=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EB=B3=B4=EC=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/mycom/socket/auth/service/MailService.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/mycom/socket/auth/service/MailService.java b/src/main/java/com/mycom/socket/auth/service/MailService.java index 7504fc3..f3ce2ca 100644 --- a/src/main/java/com/mycom/socket/auth/service/MailService.java +++ b/src/main/java/com/mycom/socket/auth/service/MailService.java @@ -42,7 +42,7 @@ public boolean isEmailVerified(String email) { public MimeMessage createMail(String email, String verificationCode) { MimeMessage message = javaMailSender.createMimeMessage(); try { - message.setFrom(mailProperties.getUsername()); + message.setFrom(mailProperties.getSenderEmail()); message.setRecipients(MimeMessage.RecipientType.TO, email); message.setSubject("이메일 인증"); String body = String.format(mailProperties.getBodyTemplate(), verificationCode); @@ -91,8 +91,10 @@ public EmailVerificationResponse verifyCode(String email, String code) { } try { - redisService.getCode(code); // 인증코드 검증 - redisService.saveVerifiedEmail(email); // 인증된 이메일 저장 + String saveCode = redisService.getCode(code); // 인증코드 검증 + if(!saveCode.equals(code)) { + throw new BaseException("인증 코드가 일치하지 않습니다.", HttpStatus.BAD_REQUEST); + } return EmailVerificationResponse.of("이메일 인증이 완료되었습니다."); } catch (Exception e) { throw new BaseException("인증 코드가 일치하지 않습니다.", HttpStatus.BAD_REQUEST); From 5e12d71bb736dfc22484b7a8c130414f61de8406 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=A9=E1=84=8B=E1=85=B2=E1=84=8E=E1=85=A1?= =?UTF-8?q?=E1=86=AB?= Date: Tue, 14 Jan 2025 15:09:23 +0900 Subject: [PATCH 10/14] =?UTF-8?q?:recycle:=20Redis=20=EC=97=B0=EA=B2=B0=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/mycom/socket/global/config/RedisConfig.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/mycom/socket/global/config/RedisConfig.java b/src/main/java/com/mycom/socket/global/config/RedisConfig.java index fd706e4..a4ca4a3 100644 --- a/src/main/java/com/mycom/socket/global/config/RedisConfig.java +++ b/src/main/java/com/mycom/socket/global/config/RedisConfig.java @@ -3,11 +3,15 @@ import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; import org.springframework.data.redis.serializer.StringRedisSerializer; +import java.time.Duration; + @Configuration @RequiredArgsConstructor @EnableRedisRepositories @@ -16,7 +20,14 @@ public class RedisConfig { @Bean public LettuceConnectionFactory redisConnectionFactory() { - return new LettuceConnectionFactory(redisProperties.getHost(), redisProperties.getPort()); + LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder() + .commandTimeout(Duration.ofSeconds(1)) + .build(); + RedisStandaloneConfiguration serverConfig = new RedisStandaloneConfiguration( + redisProperties.getHost(), + redisProperties.getPort() + ); + return new LettuceConnectionFactory(serverConfig, clientConfig); } @Bean From 4121cf62df52a3251b650713165325b8bf1a92d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=A9=E1=84=8B=E1=85=B2=E1=84=8E=E1=85=A1?= =?UTF-8?q?=E1=86=AB?= Date: Tue, 14 Jan 2025 15:09:42 +0900 Subject: [PATCH 11/14] =?UTF-8?q?:recycle:=20=ED=82=A4=20=EC=B6=A9?= =?UTF-8?q?=EB=8F=8C=20=EC=84=A4=EC=A0=95=20=EB=B0=A9=EC=A7=80=EB=A5=BC=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20=ED=82=A4=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/mycom/socket/global/service/RedisService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/mycom/socket/global/service/RedisService.java b/src/main/java/com/mycom/socket/global/service/RedisService.java index 48101b6..b37d397 100644 --- a/src/main/java/com/mycom/socket/global/service/RedisService.java +++ b/src/main/java/com/mycom/socket/global/service/RedisService.java @@ -17,6 +17,7 @@ public class RedisService { * Redis Key Prefix 상수 */ private static final String VERIFIED_EMAIL_PREFIX = "verified:email:"; + private static final String VERIFICATION_CODE_PREFIX = "verification:code:"; private static final String RATE_LIMIT_PREFIX = "rate-limit:"; /** @@ -33,7 +34,7 @@ public class RedisService { * 3분 후 자동 삭제 */ public void saveCode(String code) { - redisTemplate.opsForValue().set(code, code, VERIFICATION_TTL); + redisTemplate.opsForValue().set(VERIFICATION_CODE_PREFIX +code, code, VERIFICATION_TTL); } /** From c3ea46e7b742b20723fd8d7fe7e46ad255c3af8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=A9=E1=84=8B=E1=85=B2=E1=84=8E=E1=85=A1?= =?UTF-8?q?=E1=86=AB?= Date: Tue, 14 Jan 2025 15:14:22 +0900 Subject: [PATCH 12/14] =?UTF-8?q?:recycle:=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/mycom/socket/global/service/RedisService.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/mycom/socket/global/service/RedisService.java b/src/main/java/com/mycom/socket/global/service/RedisService.java index b37d397..d754dbc 100644 --- a/src/main/java/com/mycom/socket/global/service/RedisService.java +++ b/src/main/java/com/mycom/socket/global/service/RedisService.java @@ -33,8 +33,8 @@ public class RedisService { * Key와 Value로 동일한 인증번호를 사용 * 3분 후 자동 삭제 */ - public void saveCode(String code) { - redisTemplate.opsForValue().set(VERIFICATION_CODE_PREFIX +code, code, VERIFICATION_TTL); + public void saveCode(String email,String code) { + redisTemplate.opsForValue().set(VERIFICATION_CODE_PREFIX +email, code, VERIFICATION_TTL); } /** @@ -42,8 +42,8 @@ public void saveCode(String code) { * 인증번호가 존재하지 않거나 만료된 경우 예외 발생 * @throws BaseException 인증번호가 만료되었거나 존재하지 않는 경우 */ - public String getCode(String code) { - Object savedCode = redisTemplate.opsForValue().get(code); + public String getCode(String email) { + Object savedCode = redisTemplate.opsForValue().get(VERIFICATION_CODE_PREFIX + email); if (savedCode == null) { throw new BaseException("인증 코드가 만료되었거나 존재하지 않습니다.", HttpStatus.BAD_REQUEST); } From f7ec6d270a3c16312e2d562c3cf20729b381f97a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=A9=E1=84=8B=E1=85=B2=E1=84=8E=E1=85=A1?= =?UTF-8?q?=E1=86=AB?= Date: Tue, 14 Jan 2025 15:15:58 +0900 Subject: [PATCH 13/14] =?UTF-8?q?:recycle:=20email=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/mycom/socket/auth/service/MailService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/mycom/socket/auth/service/MailService.java b/src/main/java/com/mycom/socket/auth/service/MailService.java index f3ce2ca..18bfda4 100644 --- a/src/main/java/com/mycom/socket/auth/service/MailService.java +++ b/src/main/java/com/mycom/socket/auth/service/MailService.java @@ -66,7 +66,7 @@ public EmailVerificationResponse sendMail(String email) { } String verificationCode = createVerificationCode(); - redisService.saveCode(verificationCode); + redisService.saveCode(email, verificationCode); MimeMessage message = createMail(email, verificationCode); try { From 371a66b41ee9e6ff75e25bd9961ece1e637380c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=A9=E1=84=8B=E1=85=B2=E1=84=8E=E1=85=A1?= =?UTF-8?q?=E1=86=AB?= Date: Tue, 14 Jan 2025 15:19:43 +0900 Subject: [PATCH 14/14] =?UTF-8?q?:recycle:=20Redis=20=EB=8F=99=EC=8B=9C?= =?UTF-8?q?=EC=84=B1=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../socket/global/service/RedisService.java | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/mycom/socket/global/service/RedisService.java b/src/main/java/com/mycom/socket/global/service/RedisService.java index d754dbc..09e8f9e 100644 --- a/src/main/java/com/mycom/socket/global/service/RedisService.java +++ b/src/main/java/com/mycom/socket/global/service/RedisService.java @@ -2,6 +2,7 @@ import com.mycom.socket.global.exception.BaseException; import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; @@ -57,11 +58,18 @@ public String getCode(String email) { */ public Long incrementCount(String email) { String key = RATE_LIMIT_PREFIX + email; - Long count = redisTemplate.opsForValue().increment(key); - if (count == 1) { - redisTemplate.expire(key, RATE_LIMIT_TTL); - } - return count; + // redisTemplate.execute를 사용하여 Redis 명령어를 트랜잭션으로 실행 + return redisTemplate.execute( + (RedisConnection connection) -> { + // Redis의 INCR 명령어를 실행하여 값을 증가시키고 반환 + Long count = connection.incr(key.getBytes()); + if (count == 1) { + // 처음 호출된 경우, 만료 시간 설정 + connection.expire(key.getBytes(), RATE_LIMIT_TTL.getSeconds()); + } + return count; + } + ); } /**