*리크루팅 사이트 제작 프로젝트
지원서를 작성할 때, 본인인증을 위한 이메일 인증을 구현하려고 한다. (로그인을 따로 구현하지 않음)
SMTP는 SMTP (Simple Mail Transfer Protocol)의 약자로 전자 메일 전송을위한 표준 프로토콜이다.
이메일을 전송하기 위해 google smtp(네이버를 사용하는 방법도 있다.)를 사용해주었는데, 사용 전에 몇 가지 설정을 해주어야한다.
사용할 계정에 구글 로그인 -> Google 계정 관리하기 -> 검색창에 '앱 비밀번호' 검색

앱 비밀번호가 뜨지 않는다면 '2단계 인증'을 하지 않은 계정일 수 있으니, 2단계 인증까지 마쳐준다.
앱 비밀번호 생성
생성된 비밀번호는 노출시키지 말고, 따로 저장해둔다.
Gmail 접속하여 IMAP 설정
Gmail에 접속하여 톱니바퀴 모양을 눌러주고 '모든 설정 보기'를 눌러준다.
위와 같이 "IMAP 사용", "자동 삭제 사용", "폴더 크기 제한 - 메일 수 제한 안함"으로 설정해준다.
smtp 설정이 완료되었으면 구현을 시작해보자
build.gradle
implementation 'org.springframework.boot:spring-boot-starter-mail'
spring:
mail:
host: smtp.gmail.com
port: 587 # Google STMP 포트 번호
username: your-email@gmail.com # 발신 계정 이메일 주소
password: ${EMAIL_PASSWORD} # 발신 계정 앱 비밀번호
properties:
mail:
smtp:
auth: true
starttls:
enable: true
connectiontimeout: 5000
timeout: 5000
writetimeout: 5000
auth-code-expiration-millis: 1800000 #인증 코드 만료시간
앱 비밀번호는 ${EMAIL_PASSWORD}로 환경변수 설정을 해주었다.
EmailConfig.java
JavaMailSender 인터페이스를 구현하는 클래스. yml파일에 있는 환경변수들을 사용해준다.
@Configuration
public class EmailConfig {
@Value("${spring.mail.host}")
private String host;
@Value("${spring.mail.port}")
private int port;
@Value("${spring.mail.username}")
private String username;
@Value("${spring.mail.password}")
private String password;
@Value("${spring.mail.properties.mail.smtp.auth}")
private boolean auth;
@Value("${spring.mail.properties.mail.smtp.starttls.enable}")
private boolean starttlsEnable;
@Value("${spring.mail.properties.mail.smtp.connectiontimeout}")
private int connectionTimeout;
@Value("${spring.mail.properties.mail.smtp.timeout}")
private int timeout;
@Value("${spring.mail.properties.mail.smtp.writetimeout}")
private int writeTimeout;
@Bean
public JavaMailSender javaMailSender() {
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
mailSender.setHost(host);
mailSender.setPort(port);
mailSender.setUsername(username);
mailSender.setPassword(password);
mailSender.setDefaultEncoding("UTF-8");
mailSender.setJavaMailProperties(getMailProperties());
return mailSender;
}
private Properties getMailProperties() {
Properties properties = new Properties();
properties.put("mail.smtp.auth", auth);
properties.put("mail.smtp.starttls.enable", starttlsEnable);
properties.put("mail.smtp.connectiontimeout", connectionTimeout);
properties.put("mail.smtp.timeout", timeout);
properties.put("mail.smtp.writetimeout", writeTimeout);
return properties;
}
}
EmailService.java
이메일을 발송하는 클래스이다.
@Slf4j
@Transactional(readOnly = true)
@Service
@RequiredArgsConstructor
public class EmailService {
private final JavaMailSender javaMailSender;
public void sendEmail(String toEmail, String title, String content) {
SimpleMailMessage emailForm = createEmailForm(toEmail, title, content);
try {
javaMailSender.send(emailForm);
log.info("이메일 발송 성공");
} catch (Exception e) {
log.error("이메일 발송 오류");
}
}
public SimpleMailMessage createEmailForm(String toEmail, String title, String text) {
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(toEmail);
message.setSubject(title);
message.setText(text);
return message;
}
}
ApplicantController.java
@RestController
@RequestMapping("/applicant")
@RequiredArgsConstructor
public class ApplicantController {
private final ApplicantService applicantService;
@Operation(summary = "이메일 인증 요청 API")
@PostMapping("/email/send")
public ApiResponse<EmailDTO> emailSend(
@RequestParam("email") String email
) throws NoSuchAlgorithmException {
String vCode = applicantService.sendCodeToEmail(email);
EmailDTO result = new EmailDTO(email, vCode);
return ApiResponse.onSuccess(result);
}
@Operation(summary = "이메일 인증코드 검증 API")
@PostMapping("/email/verify")
public ApiResponse<Void> emailVerify(
@RequestBody EmailDTO emailDTO
){
boolean isVerified = applicantService.verifiedCode(emailDTO.getEmail(), emailDTO.getVCode());
if (isVerified) {
return ApiResponse.onSuccess(null); // 성공 시 null 반환
} else {
throw new ExceptionHandler(INVALID_CODE); // 실패 시 실패 응답 반환 (예시)
}
}
}
EmailDTO.java
@Getter
public class EmailDTO {
private String email;
private String vCode;
public EmailDTO(String email, String vCode) {
this.email = email;
this.vCode = vCode;
}
}
ApplicantService.java
@Slf4j
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ApplicantService {
private final ApplicantRepository applicantRepository;
private final EmailService emailService;
private final RedisService redisService;
@Value("${spring.mail.auth-code-expiration-millis}")
private long authCodeExpirationMillis;
private static final String AUTH_CODE_PREFIX = "AuthCode ";
//이메일에 인증코드 전송
public String sendCodeToEmail(String toEmail) throws NoSuchAlgorithmException {
this.checkDuplicatedEmail(toEmail);
String title = "이메일 인증 번호";
String authCode = this.createCode();
emailService.sendEmail(toEmail, title, authCode);
// 인증 번호 Redis에 저장
redisService.setValues(AUTH_CODE_PREFIX + toEmail,
authCode, Duration.ofMillis(this.authCodeExpirationMillis));
return authCode;
}
//이메일 중복 검사
private void checkDuplicatedEmail(String email) {
Optional<Applicant> applicant = applicantRepository.findByEmail(email);
if (applicant.isPresent()) {
log.debug("checkDuplicatedEmail exception occur email: {}", email);
throw new ExceptionHandler(EMAIL_EXIST);
}
}
//랜덤 인증번호 생성
private String createCode() throws NoSuchAlgorithmException {
int lenth = 6;
try {
Random random = SecureRandom.getInstanceStrong();
StringBuilder builder = new StringBuilder();
for (int i = 0; i < lenth; i++) {
builder.append(random.nextInt(10));
}
return builder.toString();
} catch (NoSuchAlgorithmException e) {
log.debug("ApplicantService.createCode() exception occur");
throw new NoSuchAlgorithmException();
}
}
//인증번호 검증
public boolean verifiedCode(String email, String authCode) {
String redisAuthCode = redisService.getValues(AUTH_CODE_PREFIX + email);
return redisService.checkExistsValue(redisAuthCode) && redisAuthCode.equals(authCode);
}
}
ApplicationRepository.java
public interface ApplicantRepository extends JpaRepository<Applicant, Long> {
Optional<Applicant> findByEmail(String email);
}
Redis는 Remote Dictionary Server의 약자로, "키-값" 구조의 비정형 데이터를 저장하고 관리하기 위한 오픈 소스 기반의 비관계형 데이터베이스 관리 시스템이다.
이메일 인증 코드와 같은 일시적이고 빠른 액세스가 필요한 데이터의 경우, 디비보다는 Redis에 저장하는 것이 적합하다.
여기서 우리는 key에 AUTH_CODE_PREFIX + Email를, value에 authCode(인증코드)를 저장해준다.
redis mac에 설치
brew install redis
build.gradle 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-data-redis:3.2.0'
application.yml 작성
spring:
data:
redis:
port: 6379
host: localhost
RedisConfig.java
@RequiredArgsConstructor
@Configuration
@EnableRedisRepositories
public class RedisConfig {
private final RedisProperties redisProperties;
// RedisProperties로 yml에 저장한 host, post를 연결. @Value("${spring.data.redis.host}")이렇게 따로 변수를 설정하지 않아도 됨.
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(redisProperties.getHost(), redisProperties.getPort());
}
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory());
return redisTemplate;
}
}
RedisService.java
@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class RedisService {
private final RedisTemplate<String, Object> redisTemplate;
public void setValues(String key, String data) {
ValueOperations<String, Object> values = redisTemplate.opsForValue();
values.set(key, data);
}
public void setValues(String key, String data, Duration duration) {
ValueOperations<String, Object> values = redisTemplate.opsForValue();
values.set(key, data, duration);
}
@Transactional(readOnly = true)
public String getValues(String key) {
ValueOperations<String, Object> values = redisTemplate.opsForValue();
if (values.get(key) == null) {
return "false";
}
return (String) values.get(key);
}
public boolean checkExistsValue(String value) {
return !value.equals("false");
}
}

이렇게 메일도 왔다능
인증코드 검증도 성공 !!
*이메일에 HTML 형식을 보내는 건 나중에 다뤄볼 예정이다.
<참고사이트>
https://green-bin.tistory.com/83
https://velog.io/@kyungmin/Spring-이메일-인증Google
https://jasonoh22.tistory.com/174