-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat : 이메일 인증 기능 구현 #57
Changes from 13 commits
75b649a
f1476e3
3d88172
0ab8ff8
70a7737
59b6610
e4570e1
5a2a58f
686f0d8
7361094
3f94458
bc80e49
fc4c5f9
94b67dc
ce7e858
bae67e8
fbba831
983477e
4acedc7
cccc127
612d335
98ddd9e
d86de05
039e782
2146989
eb2eada
54a1c91
2f43161
a52fd1e
4ee13e6
2f88bf1
28ec39a
85cd859
d2eacbf
69368a5
3f0a076
683fb9f
8f22a34
c051e9a
cd2e358
d733e00
1400caa
9e65bbc
993b055
adcc718
1737e57
3a8f639
6ebde20
4f5033c
bc7137e
53aa8a6
6224339
a28566a
6796d1b
5c7cb87
647a340
2400cd9
6f3d127
ab3fde7
31b3f49
1c5c19f
33b1d3a
619f34f
1f3d01c
230dc9b
b711279
f0e81be
ad7ee96
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package com.gdschongik.gdsc.domain.integration; | ||
|
||
import lombok.AllArgsConstructor; | ||
import lombok.Getter; | ||
import org.springframework.data.annotation.Id; | ||
import org.springframework.data.redis.core.RedisHash; | ||
import org.springframework.data.redis.core.TimeToLive; | ||
|
||
@Getter | ||
@AllArgsConstructor | ||
@RedisHash(value = "emailVerificationCode") | ||
public class EmailVerificationCode { | ||
|
||
@Id | ||
private String email; | ||
|
||
private String verificationCode; | ||
|
||
@TimeToLive | ||
private long timeToLiveInSeconds; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package com.gdschongik.gdsc.domain.integration; | ||
|
||
import org.springframework.data.repository.CrudRepository; | ||
|
||
public interface EmailVerificationCodeRepository | ||
extends CrudRepository<EmailVerificationCode, String> { | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package com.gdschongik.gdsc.domain.integration; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 유틸리티로 빼는 게 맞을 것 같습니다~ |
||
|
||
import com.gdschongik.gdsc.global.exception.CustomException; | ||
import com.gdschongik.gdsc.global.exception.ErrorCode; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.stereotype.Component; | ||
|
||
@Component | ||
@RequiredArgsConstructor | ||
public class HongikUnivEmailValidator { | ||
|
||
|
||
private static final String HONGIK_UNIV_MAIL_DOMAIN = "@g.hongik.ac.kr"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Constant로 빼는 게 좋을 것 같습니다 |
||
|
||
private static final String EMAIL_REGEX = "^[^\\W&=_'-+,<>]+(\\.[^\\W&=_'-+,<>]+)*@g\\.hongik\\.ac\\.kr$"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 다른 정규표현식을 모두 RegexConstant에서 관리하고 있는데 이 경우에는 RegexConstant에서 관리하지 않는 특별한 이유가 있을까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 여기서 필요해서 가지고 있었어요! |
||
|
||
public void validate(String email) { | ||
if (!email.contains(HONGIK_UNIV_MAIL_DOMAIN)) { | ||
throw new CustomException(ErrorCode.UNIV_EMAIL_DOMAIN_MISMATCH); | ||
} | ||
|
||
if (!email.matches(EMAIL_REGEX)) { | ||
throw new CustomException(ErrorCode.UNIV_EMAIL_FORMAT_MISMATCH); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
package com.gdschongik.gdsc.domain.integration; | ||
|
||
import jakarta.mail.Message.RecipientType; | ||
import jakarta.mail.MessagingException; | ||
import jakarta.mail.internet.InternetAddress; | ||
import jakarta.mail.internet.MimeMessage; | ||
import java.io.UnsupportedEncodingException; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.stereotype.Service; | ||
|
||
@Service | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 서비스 계층이라고 보는 것이 맞나? 라는 의문이 드네요. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 그게 맞는 것 같아요! 말씀 감사합니다 ㅎㅎ |
||
@RequiredArgsConstructor | ||
public class JavaMailSender implements MailSender { | ||
|
||
private static final String SENDER_PERSONAL = "GDSC Hongik 운영팀"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
private static final String SENDER_ADDRESS = "[email protected]"; | ||
private static final String MESSAGE_SUBJECT = "GDSC Hongik 이메일 인증 코드입니다."; | ||
|
||
private final org.springframework.mail.javamail.JavaMailSender javaMailSender; | ||
|
||
@Override | ||
public void send(String recipient, String content) { | ||
try { | ||
MimeMessage message = writeMimeMessage(recipient, content); | ||
javaMailSender.send(message); | ||
} catch (MessagingException e) { | ||
throw new RuntimeException(e); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 커스텀 예외로 변경해주시면 좋을 것 같아요~ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 헉 감사합니다!! |
||
} | ||
} | ||
|
||
private MimeMessage writeMimeMessage(String recipient, String content) | ||
throws MessagingException { | ||
MimeMessage message = javaMailSender.createMimeMessage(); | ||
|
||
message.addRecipients(RecipientType.TO, recipient); | ||
message.setSubject(MESSAGE_SUBJECT); | ||
message.setText(content, "utf-8", "html"); | ||
message.setFrom(getInternetAddress()); | ||
return message; | ||
} | ||
|
||
private InternetAddress getInternetAddress() { | ||
try { | ||
return new InternetAddress(SENDER_ADDRESS, SENDER_PERSONAL); | ||
} catch (UnsupportedEncodingException unsupportedEncodingException) { | ||
return new InternetAddress(); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
package com.gdschongik.gdsc.domain.integration; | ||
|
||
import java.util.Properties; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.mail.javamail.JavaMailSender; | ||
import org.springframework.mail.javamail.JavaMailSenderImpl; | ||
|
||
@Configuration | ||
public class JavaMailSenderConfig { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. config 패키지로 관리해줘야 할 것 같습니다. |
||
|
||
@Value("${gmail.id}") | ||
private String id; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
@Value("${gmail.password}") | ||
private String password; | ||
|
||
@Bean | ||
public JavaMailSender javaMailSender() { | ||
JavaMailSenderImpl javaMailSender = new JavaMailSenderImpl(); | ||
javaMailSender.setHost("smtp.gmail.com"); | ||
javaMailSender.setPort(456); | ||
javaMailSender.setUsername(id); | ||
javaMailSender.setPassword(password); | ||
javaMailSender.setJavaMailProperties(getMailProperties()); | ||
javaMailSender.setDefaultEncoding("UTF-8"); | ||
return javaMailSender; | ||
} | ||
|
||
private Properties getMailProperties() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yml 파일에 작성하지 않고 Properties 객체에 하드코딩하신 이유가 궁금합니다~ |
||
Properties properties = new Properties(); | ||
properties.put("mail.smtp.socketFactory.port", 456); | ||
properties.put("mail.smtp.auth", true); | ||
properties.put("mail.smtp.starttls.enable", true); | ||
properties.put("mail.smtp.starttls.required", true); | ||
properties.put("mail.smtp.socketFactory.fallback", false); | ||
properties.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory"); | ||
return properties; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package com.gdschongik.gdsc.domain.integration; | ||
|
||
public interface MailSender { | ||
|
||
void send(String recipient, String content); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 메일 전송 성공/실패를 전달받을 수 있는 방법은 존재하지 않나요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 확인해보겠습니다! |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
package com.gdschongik.gdsc.domain.integration; | ||
|
||
import com.gdschongik.gdsc.global.exception.CustomException; | ||
import com.gdschongik.gdsc.global.exception.ErrorCode; | ||
import java.time.Duration; | ||
import java.util.Objects; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.stereotype.Service; | ||
|
||
@Service | ||
@RequiredArgsConstructor | ||
public class UnivEmailVerificationService { | ||
|
||
private final HongikUnivEmailValidator hongikUnivEmailValidator; | ||
private final VerificationCodeGenerator verificationCodeGenerator; | ||
private final VerificationMailContentWriter verificationMailContentWriter; | ||
|
||
private final MailSender mailSender; | ||
private final EmailVerificationCodeRepository emailVerificationCodeRepository; | ||
|
||
public static final Duration VERIFICATION_CODE_TIME_TO_LIVE = Duration.ofMinutes(10); | ||
|
||
public void sendEmailVerificationMail(String email) { | ||
hongikUnivEmailValidator.validate(email); | ||
|
||
String verificationCode = verificationCodeGenerator.generate(); | ||
sendMail(email, verificationCode); | ||
|
||
saveVerificationCode(email, verificationCode); | ||
} | ||
|
||
private void sendMail(String email, String verificationCode) { | ||
String mailContent = verificationMailContentWriter | ||
.writeContentWithVerificationCode(verificationCode, VERIFICATION_CODE_TIME_TO_LIVE); | ||
|
||
mailSender.send(email, mailContent); | ||
} | ||
|
||
private void saveVerificationCode(String email, String verificationCode) { | ||
EmailVerificationCode emailVerificationCode = new EmailVerificationCode( | ||
email, verificationCode, VERIFICATION_CODE_TIME_TO_LIVE.toSeconds() | ||
); | ||
|
||
emailVerificationCodeRepository.save(emailVerificationCode); | ||
} | ||
|
||
public void validateCodeMatch(String email, String userInputCode) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 사용처가 없는 것 같습니다. |
||
String verificationCode = getVerificationCodeByEmail(email); | ||
|
||
if (!Objects.equals(verificationCode, userInputCode)) { | ||
throw new CustomException(ErrorCode.UNIV_EMAIL_VERIFICATION_CODE_NOT_MATCH); | ||
} | ||
} | ||
|
||
private String getVerificationCodeByEmail(String email) { | ||
return emailVerificationCodeRepository.findById(email) | ||
.orElseThrow(() -> new CustomException(ErrorCode.VERIFICATION_CODE_NOT_FOUND)) | ||
.getVerificationCode(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package com.gdschongik.gdsc.domain.integration; | ||
|
||
import org.apache.commons.lang3.RandomStringUtils; | ||
import org.springframework.stereotype.Component; | ||
|
||
@Component | ||
public class VerificationCodeGenerator { | ||
|
||
private static final int VERIFICATION_CODE_LENGTH = 16; | ||
private static final char RANGE_START_CHAR = '0'; | ||
private static final char RANGE_END_CHAR = 'z'; | ||
|
||
public String generate() { | ||
return RandomStringUtils.random(VERIFICATION_CODE_LENGTH, RANGE_START_CHAR, RANGE_END_CHAR, true, true); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package com.gdschongik.gdsc.domain.integration; | ||
|
||
import java.time.Duration; | ||
import org.springframework.stereotype.Component; | ||
|
||
@Component | ||
public class VerificationMailContentWriter { | ||
|
||
private static final String NOTIFICATION_MESSAGE = "<div style='margin:20px;'>" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 인증 코드 입력방식 말고, 인증 URL 클릭하는 방식으로 처리하는게 어떨까 싶습니다~ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 죄송합니다 확인해보겠습니다! |
||
+ "<h1> 안녕하세요 GDSC Hongik 이메일 인증 메일입니다. </h1> <br>" | ||
+ "<h3> 아래의 인증 코드를 %d분 안에 입력해주세요. </h3> <br>" | ||
+ "<h3> 감사힙니다. </h3> <br>" | ||
+ "CODE : <strong>"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 놓쳐서 죄송합니다! |
||
|
||
public String writeContentWithVerificationCode(String verificationCode, Duration codeAliveTime) { | ||
return String.format(NOTIFICATION_MESSAGE, codeAliveTime.toMinutes()) | ||
+ verificationCode; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,3 +13,7 @@ spring: | |
logging: | ||
level: | ||
com.gdschongik.gdsc.domain.*.api.*: debug | ||
|
||
gmail: | ||
id: ${GMAIL_ID} | ||
password: ${GMAIL_PASSWORD} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. redis, storage처럼 이메일은 yaml 파일을 분리하지 않는게 일반적인가요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 일반적이라기 보다는 상황에 따라 다를거 같아요 ㅎㅎ |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
package com.gdschongik.gdsc.domain.integration; | ||
|
||
import static org.assertj.core.api.Assertions.assertThatCode; | ||
import static org.assertj.core.api.Assertions.assertThatThrownBy; | ||
|
||
import com.gdschongik.gdsc.global.exception.CustomException; | ||
import com.gdschongik.gdsc.global.exception.ErrorCode; | ||
import org.junit.jupiter.api.DisplayName; | ||
import org.junit.jupiter.api.Test; | ||
import org.junit.jupiter.params.ParameterizedTest; | ||
import org.junit.jupiter.params.provider.ValueSource; | ||
import org.springframework.beans.factory.annotation.Autowired; | ||
import org.springframework.boot.test.context.SpringBootTest; | ||
|
||
@SpringBootTest | ||
public class HongikUnivEmailValidatorTest { | ||
|
||
@Autowired | ||
private HongikUnivEmailValidator hongikUnivEmailValidator; | ||
|
||
@Test | ||
@DisplayName("'g.hongik.ac.kr' 도메인을 가진 이메일을 검증할 수 있다.") | ||
public void validateEmailDomainTest() { | ||
String hongikDomainEmail = "[email protected]"; | ||
|
||
assertThatCode( | ||
() -> hongikUnivEmailValidator.validate(hongikDomainEmail) | ||
).doesNotThrowAnyException(); | ||
} | ||
|
||
@ParameterizedTest | ||
@ValueSource(strings = {"[email protected]", "[email protected]", "[email protected]", | ||
"[email protected]"}) | ||
@DisplayName("'g.hongik.ac.kr'가 아닌 도메인을 가진 이메일을 입력하면 예외를 발생시킨다.") | ||
public void validateEmailDomainMismatchTest(String email) { | ||
assertThatThrownBy(() -> hongikUnivEmailValidator.validate(email)) | ||
.isInstanceOf(CustomException.class) | ||
.hasMessage(ErrorCode.UNIV_EMAIL_DOMAIN_MISMATCH.getMessage()); | ||
} | ||
|
||
@Test | ||
@DisplayName("Email의 '@' 앞 부분에는 연속되지 않은 점이 포함될 수 있다.") | ||
public void validateEmailFormatWithDotsTest() { | ||
String email = "[email protected]"; | ||
|
||
assertThatCode( | ||
() -> hongikUnivEmailValidator.validate(email) | ||
).doesNotThrowAnyException(); | ||
} | ||
|
||
@ParameterizedTest | ||
@ValueSource(strings = {"te&[email protected]", "[email protected]", "[email protected]", | ||
"te'[email protected]", "[email protected]", "[email protected]", | ||
"te,[email protected]", "te<[email protected]", "te>[email protected]"}) | ||
@DisplayName("Email의 '@' 앞 부분에 '&', '=', '_', ''', '-', '+', ',', '<', '>'가 포함되는 경우 예외를 발생시킨다.") | ||
public void validateEmailFormatMismatchTest(String email) { | ||
assertThatThrownBy(() -> hongikUnivEmailValidator.validate(email)) | ||
.isInstanceOf(CustomException.class) | ||
.hasMessage(ErrorCode.UNIV_EMAIL_FORMAT_MISMATCH.getMessage()); | ||
} | ||
|
||
@Test | ||
@DisplayName("Email의 '@' 앞 부분에 '.'이 2개 연속 오는 경우 예외를 발생시킨다.") | ||
public void validateEmailFormatMismatchWithDotsTest() { | ||
String email = "[email protected]"; | ||
assertThatThrownBy(() -> hongikUnivEmailValidator.validate(email)) | ||
.isInstanceOf(CustomException.class) | ||
.hasMessage(ErrorCode.UNIV_EMAIL_FORMAT_MISMATCH.getMessage()); | ||
} | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
email 패키지로 이름을 지정하지 않은 이유가 있으신가요?