Skip to content

Commit

Permalink
feat : 회원가입 이메일 인증 구현
Browse files Browse the repository at this point in the history
  • Loading branch information
JangYouJung committed Jun 10, 2024
1 parent bb2295d commit 55779b5
Show file tree
Hide file tree
Showing 13 changed files with 382 additions and 5 deletions.
5 changes: 4 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,11 @@ dependencies {
implementation 'jakarta.validation:jakarta.validation-api:3.0.2'
implementation 'org.springframework.security:spring-security-messaging'

// mail
implementation 'org.springframework.boot:spring-boot-starter-mail:3.0.5'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

// JWT
// implementation 'io.jsonwebtoken:jjwt:0.9.1'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package meltingpot.server.auth.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import meltingpot.server.auth.controller.dto.MailVerificationRequestDto;
import meltingpot.server.auth.controller.dto.VerificationCodeRequestDto;
import meltingpot.server.auth.service.MailService;
import meltingpot.server.exception.DuplicateException;
import meltingpot.server.exception.MailVerificationException;
import meltingpot.server.util.ResponseCode;
import meltingpot.server.util.ResponseData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor
@RestController
@RequestMapping("/api/v1/mail")
public class MailController {
private final MailService mailService;
private final Logger logger = LoggerFactory.getLogger(this.getClass());

// 정회원 인증 메일 전송
@PostMapping("")
@Operation(summary="이메일 인증번호 전송 [임시]", description="[이메일 인증 임시 구현 SMTP]\n 입력 받은 이메일로 인증 번호를 전송합니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "MAIL_VERIFICATION_SEND_SUCCESS", description = "이메일 인증번호 전송 성공"),
@ApiResponse(responseCode = "VERIFICATION_CODE_ALREADY_EXIST", description = "이미 생성한 인증 번호가 있습니다"),
@ApiResponse(responseCode = "MAIL_SEND_FAIL", description = "이메일 전송 실패")
})
public ResponseEntity<ResponseData> sendVerificationMail(
@RequestBody @Valid MailVerificationRequestDto request
) {
try{
logger.info("MAIL_VERIFICATION_SEND_SUCCESS (200 OK)");
return ResponseData.toResponseEntity(mailService.sendVerificationMail(request));

}catch(MailVerificationException e){
return ResponseData.toResponseEntity(e.getResponseCode());
}
}

@PostMapping("verification")
@Operation(summary="이메일 인증번호 확인", description="이메일 인증 번호를 입력 받고 올바른 번호인지 확인합니다." )
@ApiResponses(value = {
@ApiResponse(responseCode = "MAIL_VERIFICATION_CHECK_SUCCESS", description = "인증번호가 일치합니다"),
@ApiResponse(responseCode = "AUTHENTICATION_NOT_FOUND", description = "메일 인증 정보를 찾을 수 없습니다"),
@ApiResponse(responseCode = "AUTH_TIME_OUT", description = "인증 시간을 초과했습니다"),
@ApiResponse(responseCode = "AUTH_NUMBER_INCORRECT", description = "인증 번호가 틀렸습니다"),
})
public ResponseEntity<ResponseData> checkVerification(
@RequestBody @Valid VerificationCodeRequestDto request
) {
try{
logger.info("VERIFICATION_CHECK_SUCCESS (200 OK)");
return ResponseData.toResponseEntity( mailService.checkVerification(request));

}catch(MailVerificationException e){
return ResponseData.toResponseEntity(e.getResponseCode());
}
}

// 이메일 중복 확인
@PostMapping("duplication")
@Operation(summary="이메일 중복 체크", description="이미 가입한 이메일인지 확인하는 API 입니다.\n" )
public ResponseEntity<ResponseData> checkEmail(
@RequestBody @Valid MailVerificationRequestDto request) {
try{
mailService.checkUserName(request.email());
return ResponseData.toResponseEntity(ResponseCode.MAIL_AVAILABLE);
}catch (DuplicateException e){
return ResponseData.toResponseEntity(ResponseCode.EMAIL_DUPLICATION);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package meltingpot.server.auth.controller.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import meltingpot.server.util.VerificationUtil;

public record MailVerificationRequestDto(
@NotBlank(message = "email is required")
@Pattern(regexp = VerificationUtil.USERNAME_REGEXP)
String email
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package meltingpot.server.auth.controller.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import meltingpot.server.util.VerificationUtil;

public record VerificationCodeRequestDto(
@NotBlank(message = "email is required")
@Pattern(regexp = VerificationUtil.USERNAME_REGEXP)
String email,
@NotBlank
String code
) {
}
121 changes: 121 additions & 0 deletions src/main/java/meltingpot/server/auth/service/MailService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package meltingpot.server.auth.service;

import lombok.RequiredArgsConstructor;
import meltingpot.server.auth.controller.dto.MailVerificationRequestDto;
import meltingpot.server.auth.controller.dto.VerificationCodeRequestDto;
import meltingpot.server.domain.entity.Constants;
import meltingpot.server.domain.entity.MailVerification;
import meltingpot.server.domain.repository.AccountRepository;
import meltingpot.server.domain.repository.MailVerificationRepository;
import meltingpot.server.exception.DuplicateException;
import meltingpot.server.exception.MailVerificationException;
import meltingpot.server.util.MailUtil;
import meltingpot.server.util.ResponseCode;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.Optional;
import java.util.Random;

@RequiredArgsConstructor
@Service
public class MailService {
private final MailVerificationRepository mailVerificationRepository;
private final MailUtil mailUtil;
private final AccountRepository accountRepository;

// 인증 코드 생성
private String createCode() {
try {
Random rand = SecureRandom.getInstanceStrong();
StringBuilder code = new StringBuilder();

for (int i = 0; i < 6; i++) {
// 0~9까지 난수 생성
String num = Integer.toString(rand.nextInt(10));
code.append(num);
}

return code.toString();

} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return null;
}
}

// 인증 메일 발송
@Transactional
public ResponseCode sendVerificationMail(MailVerificationRequestDto request) {
String email = request.email();

// 이미 유효한 인증 정보가 있는 경우
Optional<MailVerification> oldMailVerification = mailVerificationRepository.findByEmailAndExpiredAtIsAfterNowAndVerifiedFalse(email,LocalDateTime.now());
if(oldMailVerification.isPresent()){
throw new MailVerificationException(ResponseCode.VERIFICATION_CODE_ALREADY_EXIST);
}

String code = createCode();
Map<String,String> mailValues = Map.of("code", code);

String title = "[멜팅팟] 이메일 인증을 위한 인증 번호 안내";

// 메일 전송
mailUtil.sendMimeMessageMailWithValues(title, email, "EmailAuthenticationForm.html", mailValues);

LocalDateTime expiredAt = LocalDateTime.now().plusMinutes(Constants.AUTH_TIME_LIMIT);

MailVerification mailVerification = MailVerification.builder()
.email(email)
.expiredAt(expiredAt)
.authenticationNumber(code)
.build();

mailVerificationRepository.save(mailVerification);

return ResponseCode.MAIL_VERIFICATION_SEND_SUCCESS;

}


// 인증 번호 확인
@Transactional
public ResponseCode checkVerification(VerificationCodeRequestDto request) {

// 현재 유효한 인증 정보 가져오기
MailVerification mailVerification = mailVerificationRepository.findByEmailAndExpiredAtIsAfterNowAndVerifiedFalse(request.email(), LocalDateTime.now())
.orElseThrow( // 인증 정보가 없는 경우
()->new MailVerificationException(ResponseCode.AUTHENTICATION_NOT_FOUND)
);

// 인증 번호 유효 기간을 초과한 경우
if(LocalDateTime.now().isAfter(mailVerification.getExpiredAt())){
throw new MailVerificationException(ResponseCode.AUTH_TIME_OUT);
}

// 인증 번호가 틀린 경우
if(!mailVerification.getAuthenticationNumber().equals(request.code())){
throw new MailVerificationException(ResponseCode.AUTH_NUMBER_INCORRECT);
}

// 인증에 성공한 경우: 제한 시간 내에 인증 번호를 올바르게 입력한 경우
mailVerification.setVerified(true);
mailVerificationRepository.save(mailVerification);

return ResponseCode.MAIL_VERIFICATION_CHECK_SUCCESS;

}

// 회원가입시 이메일 유효성 확인
@Transactional(readOnly = true)
public void checkUserName(String username) {
if(accountRepository.existsByUsername(username)){
throw new DuplicateException(ResponseCode.EMAIL_DUPLICATION);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package meltingpot.server.domain.repository;

import meltingpot.server.domain.entity.MailVerification;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.time.LocalDateTime;
import java.util.Optional;

public interface MailVerificationRepository extends JpaRepository<MailVerification, Long> {
@Query(value = "select * from mail_verification where email = :email and expired_at >= :now and verified = false", nativeQuery = true)
Optional<MailVerification> findByEmailAndExpiredAtIsAfterNowAndVerifiedFalse(String email, LocalDateTime now);

boolean existsByEmailAndVerifiedTrue(String email);

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,5 @@
@Getter
@RequiredArgsConstructor
public class DuplicateException extends RuntimeException {

private final ResponseCode responseCode;
}
12 changes: 12 additions & 0 deletions src/main/java/meltingpot/server/exception/MailSendException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package meltingpot.server.exception;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import meltingpot.server.util.ResponseCode;

@Getter
@AllArgsConstructor
public class MailSendException extends RuntimeException{
private final ResponseCode responseCode;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package meltingpot.server.exception;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import meltingpot.server.util.ResponseCode;

@Getter
@AllArgsConstructor
public class MailVerificationException extends RuntimeException{
private final ResponseCode responseCode;
}
51 changes: 51 additions & 0 deletions src/main/java/meltingpot/server/util/MailUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package meltingpot.server.util;

import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import meltingpot.server.exception.MailSendException;
import org.springframework.mail.MailException;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.thymeleaf.context.Context;
import org.thymeleaf.spring6.SpringTemplateEngine;

import java.util.Map;

@RequiredArgsConstructor
@Component
public class MailUtil {
private final JavaMailSender javaMailSender;
private final SpringTemplateEngine templateEngine;

@Async
public void sendMimeMessageMailWithValues(String title, String to, String templateName, Map<String, String> values) {
try {
MimeMessage message = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true);

// 메일 제목 설정
helper.setSubject(title);

// 수신자 설정
helper.setFrom("멜팅팟 <[email protected]>");
helper.setTo(to);

// 템플릿에 전달할 데이터 설정
Context context = new Context();
values.forEach(context::setVariable);

// 메일 내용 설정 : 템플릿 프로세스
String html = templateEngine.process(templateName, context);
helper.setText(html, true);

// 메일 보내기
javaMailSender.send(message);
} catch (MessagingException | MailException e) {
throw new MailSendException(ResponseCode.MAIL_SEND_FAIL);
}
}

}
Loading

0 comments on commit 55779b5

Please sign in to comment.