Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
22 changes: 22 additions & 0 deletions src/main/java/com/mycom/socket/auth/config/MailProperties.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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 = "spring.mail")
public class MailProperties {
private String host;
private int port;
private String protocol;
private String username;
private String password;
private String senderEmail;
private String senderName;
private String subject;
private String bodyTemplate;
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Comment on lines +41 to 44
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

๐Ÿ’ก Codebase verification

API ๋ฌธ์„œํ™”๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค

์—”๋“œํฌ์ธํŠธ ๋ณ€๊ฒฝ์‚ฌํ•ญ(/verify-email)์ด ๋ฌธ์„œํ™”๋˜์–ด ์žˆ์ง€ ์•Š์€ ๊ฒƒ์œผ๋กœ ๋ณด์ž…๋‹ˆ๋‹ค. ๋‹ค์Œ ์‚ฌํ•ญ๋“ค์„ ์ถ”๊ฐ€ํ•ด์ฃผ์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค:

  • API ์—”๋“œํฌ์ธํŠธ์— ๋Œ€ํ•œ ๋ช…์„ธ ๋ฌธ์„œ ์ž‘์„ฑ
  • ํด๋ผ์ด์–ธํŠธ ์ฝ”๋“œ์—์„œ ์ƒˆ๋กœ์šด ์—”๋“œํฌ์ธํŠธ ์‚ฌ์šฉ ์—ฌ๋ถ€ ํ™•์ธ
  • README.md์— API ์‚ฌ์šฉ ๋ฐฉ๋ฒ• ์ถ”๊ฐ€
๐Ÿ”— Analysis chain

์—”๋“œํฌ์ธํŠธ ์ด๋ฆ„์ด ๊ฐœ์„ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!

์ƒˆ๋กœ์šด ์—”๋“œํฌ์ธํŠธ ์ด๋ฆ„ /verify-email์ด ๊ธฐ๋Šฅ์„ ๋” ๋ช…ํ™•ํ•˜๊ฒŒ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค.

API ๋ฌธ์„œ๊ฐ€ ์—…๋ฐ์ดํŠธ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด ๋‹ค์Œ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‹คํ–‰ํ•˜์„ธ์š”:

๐Ÿ Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# API ๋ฌธ์„œ์—์„œ ์ด์ „ ์—”๋“œํฌ์ธํŠธ ์ฐธ์กฐ ํ™•์ธ
rg -i "/verification" --type md
rg -i "/verify-email" --type md

Length of output: 63


Script:

#!/bin/bash
# Search for API documentation files
echo "=== Documentation files ==="
fd -e yaml -e yml -e json . | grep -i "api\|swagger\|openapi"

echo -e "\n=== Searching for endpoint references ==="
# Search in all text files for both old and new endpoints
rg -t txt -t yaml -t yml -t json -t java "/verification|/verify-email"

echo -e "\n=== README files ==="
fd README

Length of output: 407


@PostMapping("/email/verify")
@PostMapping("/verification-code")
public EmailVerificationResponse verifyEmail(@Valid @RequestBody EmailVerificationRequest request) {
return mailService.verifyCode(request.email(), request.code());
}
Expand Down
1 change: 1 addition & 0 deletions src/main/java/com/mycom/socket/auth/jwt/JWTFilter.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/main/java/com/mycom/socket/auth/jwt/JWTUtil.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
72 changes: 27 additions & 45 deletions src/main/java/com/mycom/socket/auth/service/MailService.java
Original file line number Diff line number Diff line change
@@ -1,43 +1,37 @@
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.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
public class MailService {

private final JavaMailSender javaMailSender;
private final RateLimiter rateLimiter; // ์ธ์ฆ ๋ฒˆํ˜ธ ์š”์ฒญ ์ œํ•œ

private final Map<String, VerificationData> verificationDataMap = new ConcurrentHashMap<>();

@Value("${spring.mail.username}")
private String senderEmail;
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);
}

/**
Expand All @@ -48,14 +42,10 @@ private String createVerificationCode() {
public MimeMessage createMail(String email, String verificationCode) {
MimeMessage message = javaMailSender.createMimeMessage();
try {
message.setFrom(senderEmail);
message.setFrom(mailProperties.getSenderEmail());
message.setRecipients(MimeMessage.RecipientType.TO, email);
message.setSubject("์ด๋ฉ”์ผ ์ธ์ฆ");
String body = String.format("""
<h3>์š”์ฒญํ•˜์‹  ์ธ์ฆ ๋ฒˆํ˜ธ์ž…๋‹ˆ๋‹ค.</h3>
<h1>%s</h1>
<h3>๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค.</h3>
""", verificationCode);
String body = String.format(mailProperties.getBodyTemplate(), verificationCode);
message.setText(body, "UTF-8", "html");
} catch (MessagingException e) {
throw new BaseException("์ด๋ฉ”์ผ ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: " + e.getMessage(),
Expand All @@ -70,15 +60,18 @@ 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);
}
Comment on lines +63 to +66
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ Potential issue

์š”์ฒญ ํšŸ์ˆ˜ ์ œํ•œ์— ๋Œ€ํ•œ ๋งŒ๋ฃŒ ์‹œ๊ฐ„ ์„ค์ • ํ•„์š”

redisService.incrementCount(email)๋ฅผ ํ†ตํ•ด ์ด๋ฉ”์ผ๋ณ„ ์š”์ฒญ ํšŸ์ˆ˜๋ฅผ ์ฆ๊ฐ€์‹œํ‚ค์ง€๋งŒ, ํ˜„์žฌ ์š”์ฒญ ํšŸ์ˆ˜๋ฅผ ์ดˆ๊ธฐํ™”ํ•˜๊ฑฐ๋‚˜ ๋งŒ๋ฃŒ์‹œํ‚ค๋Š” ๋กœ์ง์ด ์—†์Šต๋‹ˆ๋‹ค. ์ด๋กœ ์ธํ•ด ์‚ฌ์šฉ์ž๊ฐ€ ์˜๊ตฌ์ ์œผ๋กœ ์ด๋ฉ”์ผ ์ „์†ก์„ ํ•  ์ˆ˜ ์—†๊ฒŒ ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์š”์ฒญ ํšŸ์ˆ˜์— ๋Œ€ํ•œ Redis ํ‚ค์— TTL(๋งŒ๋ฃŒ ์‹œ๊ฐ„)์„ ์„ค์ •ํ•˜์—ฌ ์ผ์ • ์‹œ๊ฐ„ ํ›„์— ์ž๋™์œผ๋กœ ์ดˆ๊ธฐํ™”๋˜๋„๋ก ์ˆ˜์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.


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("์ด๋ฉ”์ผ ์ „์†ก ์„ฑ๊ณต");
return EmailVerificationResponse.of("์ด๋ฉ”์ผ ์ „์†ก ์„ฑ๊ณต"); // ๋ฉ”์‹œ์ง€ ์ˆ˜์ •
} catch (Exception e) {
throw new BaseException("์ด๋ฉ”์ผ ๋ฐœ์†ก ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: " + e.getMessage(),
HttpStatus.INTERNAL_SERVER_ERROR);
Expand All @@ -93,30 +86,19 @@ 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}")) {
throw new BaseException("์œ ํšจํ•˜์ง€ ์•Š์€ ์ธ์ฆ ์ฝ”๋“œ ํ˜•์‹์ž…๋‹ˆ๋‹ค.", HttpStatus.BAD_REQUEST);
}

if (!data.code().equals(code)) {
try {
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);
}

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();
}

}
Expand Down
47 changes: 0 additions & 47 deletions src/main/java/com/mycom/socket/auth/service/RateLimiter.java

This file was deleted.

43 changes: 43 additions & 0 deletions src/main/java/com/mycom/socket/global/config/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
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.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
public class RedisConfig {
private final RedisProperties redisProperties;

@Bean
public LettuceConnectionFactory redisConnectionFactory() {
LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
.commandTimeout(Duration.ofSeconds(1))
.build();
RedisStandaloneConfiguration serverConfig = new RedisStandaloneConfiguration(
redisProperties.getHost(),
redisProperties.getPort()
);
return new LettuceConnectionFactory(serverConfig, clientConfig);
}

@Bean
public RedisTemplate<?, ?> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());

redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
return redisTemplate;
}

}
16 changes: 16 additions & 0 deletions src/main/java/com/mycom/socket/global/config/RedisProperties.java
Original file line number Diff line number Diff line change
@@ -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;
}
Loading
Loading