개발스토리지😃

[스프링] 회원가입 이메일 인증 구현(구글 이메일 사용) 본문

프레임워크/스프링

[스프링] 회원가입 이메일 인증 구현(구글 이메일 사용)

believekim 2025. 8. 5. 11:02

 

회원가입을 할 때 구글 이메일을 사용하여 사용자에게 인증번호를 보내고
사용자가 인증번호를 인증하면 회원가입이 가능하도록 구현했습니다.

 

 

 

1. 결과 화면

  • 인증이 완료되지 않아 회원가입 불가

 

  • 인증번호 전송

 

  • 인증번호 확인

 

  • 레디스에 아래와 같이 저장
  • Key = EMAIL_CODE:<이메일>
  • Value = 인증번호 코드

 

  • 인증이 완료되면 Value값을 ture로 변환

 

  • 회원가입 완료


 

2. 구글 이메일 연동

  • GMail에 들어가 오른쪽 위에 설정 버튼을 눌러 모든 설정 보기를 클릭


  • 번호 순서대로 설정한 후 변경사항 저장


  • 구글 계정에 들어가 앱 비밀번호를 검색하여 클릭


 

  • 비밀번호를 생성하면 16글자의 문자열을 받을 수 있음


 

3. MailAuthCodeRequest

  • 메일 인증코드를 받기 위한 DTO
public record MailAuthCodeRequest(

        @NotBlank(message = "이메일은 필수 입력값입니다.")
        @Email(message = "올바른 이메일 형식이 아닙니다.")
        String email
) {

}

4. MailAuthCodeVerifyRequest

  • 전송받은 메일 인증코드를 검증하기 위한 DTO
public record MailAuthCodeVerifyRequest(

        @NotBlank(message = "이메일은 필수 입력값입니다.")
        @Email(message = "올바른 이메일 형식이 아닙니다.")
        String email,

        @NotBlank(message = "인증번호는 필수 입력값입니다.")
        String code

) {

}

 

5. MailController

  • 회원가입 시 이메일 인증 기능을 제공하는 컨트롤러
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/auth")
public class MailController {

    private final MailService mailService;

    /**
     * 회원가입용 인증 코드 메일 발송
     */
    @PostMapping("/send-code")
    public ResponseEntity<Void> sendCode(@RequestBody @Valid MailAuthCodeRequest request) {
        mailService.sendVerificationCode(request.email());
        return ResponseEntity.ok().build();
    }

    /**
     * 인증 코드 검증
     */
    @PostMapping("/verify-code")
    public ResponseEntity<Void> verifyCode(@RequestBody @Valid MailAuthCodeVerifyRequest request) {
        mailService.verifyCode(request.email(), request.code());
        return ResponseEntity.ok().build(); // 성공 시 200 OK (응답 바디 없음)
    }
}

 

6. MailService

  • 이메일 인증 코드를 발송하고, 해당 코드를 검증하여 사용자 이메일 인증 여부를 확인하는 서비스
@Slf4j
@Service("mailService")
@RequiredArgsConstructor
public class MailService {

    private final JavaMailSender javaMailSender;
    private final RedisRepository redisRepository;

    /**
     * 인증번호 메일 전송
     */
    public void sendVerificationCode(String email) {
        // 6자리 난수 생성
        String code = String.valueOf((int) (Math.random() * 900000) + 100000);

        // Redis에 3분간 유효한 인증 코드 저장
        redisRepository.saveMailAuthCode(email, code, Duration.ofMinutes(3));

        // HTML 본문 문자열 직접 작성
        String html = String.format("""
                <html>
                  <body style="font-family: Arial, sans-serif;">
                    <h2>회원가입 이메일 인증 코드</h2>
                    <p>아래 인증 코드를 입력해주세요:</p>
                    <p style="font-size: 24px; font-weight: bold;">%s</p>
                    <p>3분 이내에 입력하지 않으면 코드가 만료됩니다.</p>
                  </body>
                </html>
                """, code);

        try {
            MimeMessage message = javaMailSender.createMimeMessage();
            MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
            helper.setTo(email);
            helper.setSubject("회원가입 이메일 인증 코드");
            helper.setText(html, true);
            javaMailSender.send(message);
        } catch (MessagingException e) {
            log.error("이메일 발송 실패. 대상: {}, 원인: {}", email, e.getMessage(), e);
            throw new BizException(MailErrorCode.SEND_FAILED);
        }
    }

    /**
     * 인증 코드 검증
     */
    public void verifyCode(String email, String code) {
        String storedCode = redisRepository.getMailAuthCode(email);

        if (!storedCode.equals(code)) {
            throw new BizException(MailErrorCode.CODE_NOT_MATCHED);
        }

        // 인증 성공 → 인증 상태 30분간 유지
        redisRepository.save("EMAIL_VERIFIED:" + email, "true", Duration.ofMinutes(30));

        // 인증 코드 삭제
        redisRepository.deleteMailAuthCode(email);
    }
}

 

7. RedisRepository

  • 이메일 인증 코드 저장·검증 등 인증 관련 정보를 Redis를 활용해 관리
@Repository
@RequiredArgsConstructor
public class RedisRepository {

	private final RedisTemplate<String,String> redisTemplate;
    
	public void saveMailAuthCode(String email, String code, Duration ttl) {
		redisTemplate.opsForValue().set("EMAIL_CODE:" + email, code, ttl);
	}

	public String getMailAuthCode(String email) {
		String code = redisTemplate.opsForValue().get("EMAIL_CODE:" + email);
		if (code == null) {
			throw new BizException(MailErrorCode.EMAIL_CODE_NOT_FOUND);
		}
		return code;
	}

	public void deleteMailAuthCode(String email) {
		redisTemplate.delete("EMAIL_CODE:" + email);
	}

	public void save(String key, String value, Duration ttl) {
		try {
			redisTemplate.opsForValue().set(key, value, ttl);
		} catch (Exception e) {
			throw new BizException(MailErrorCode.SEND_FAILED);
		}
	}

	public boolean hasKey(String key) {
		return Boolean.TRUE.equals(redisTemplate.hasKey(key));
	}

	public void delete(String key) {
		redisTemplate.delete(key);
	}
}

 

8. MailErrorCode

@Getter
@RequiredArgsConstructor
public enum MailErrorCode implements ErrorCode {

    SEND_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "E001", "인증번호 발송에 실패했습니다."),
    EMAIL_CODE_NOT_FOUND(HttpStatus.BAD_REQUEST, "E002", "이메일 인증 코드가 존재하지 않습니다. 인증번호 발송을 요청해주세요."),
    CODE_NOT_MATCHED(HttpStatus.BAD_REQUEST, "E003", "이메일 인증 코드가 일치하지 않습니다."),
    EMAIL_NOT_VERIFIED(HttpStatus.BAD_REQUEST, "E004", "이메일 인증이 완료되지 않았습니다.");

    private final HttpStatus httpStatus;
    private final String code;
    private final String message;

    @Override
    public HttpStatus getStatus() {
        return httpStatus;
    }

    @Override
    public String getCode() {
        return code;
    }

    @Override
    public String getMessage() {
        return message;
    }
}

9. SecurityConfig

  • 스프링시큐리티 permitAll() 추가
"/api/auth/send-code"
"/api/auth/verify-code"

 

10. application.yml

  • 환경변수는 .env파일로 관리
spring:
  mail:
    host: smtp.gmail.com
    port: 587
    username: ${EMAIL_ID}
    password: ${EMAIL_PASSWORD}
    properties:
      mail:
        smtp:
          starttls:
            enable: true
            auth: true