회원가입 단계에서 사용자 인증을 받을 때 SMS 인증은 건당 요금이 나간다고 해서 이메일 인증을 채택했다. 먼저 이메일에 코드를 보내면 그 코드를 레디스에 저장하고 사용자가 코드값을 입력하면 레디스의 값과 비교해서 인증을 하는 순서로 진행된다. 레디스와 관련된 내용은 다음 글에서 다룰 것이다.
https://hihinote.tistory.com/91
이메일 인증에는 두 가지 방법이 있는데 인증번호 입력 방식과 링크 클릭 방식이다. 둘 중 인증번호 입력 방식을 사용했고 구글 이메일을 보내는 메일로 사용했다.
이메일 인증은 스프링에서 기본으로 제공하는 API를 사용해 쉽게 할 수 있었다.
admin이메일 계정 설정
구글의 2단계 인증을 활성화한 후 앱 비밀번호를 생성하면 gmail 비밀번호 대신 사용 가능한 비밀번호가 나온다.
gmail은 일일 전송 한도가 있는데, 1회에 100개 하루 500개로 제한되어 있다. 타 메일 발송 한도는 네이버는 1회 100개, 시간당 최대 30개를 보낼 수 있으며 다음은 1회 150개 하루 10000개로 제한되어 있다고 한다.
build.gradle - dependencies
implementation 'org.springframework.boot:spring-boot-starter-mail'
application-mail.properties
mail.smtp.auth=true
mail.smtp.starttls.required=true
mail.smtp.starttls.enable=true
mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory
mail.smtp.socketFactory.fallback=false
mail.smtp.port=465
mail.smtp.socketFactory.port=465
# admin
AdminMail.id = email 보낼 계정 id
AdminMail.password = 위에서 발급받았던 앱 비밀번호
builde.gradle의 dependency에 spring-boot-starter-mail을 추가한 후 application-mail.properties를 만들었다.
EmailConfig
@Configuration
@PropertySource("classpath:application-mail.properties")
public class EmailConfig {
@Value("${mail.smtp.port}")
private int port;
@Value("${mail.smtp.socketFactory.port}")
private int socketPort;
@Value("${mail.smtp.auth}")
private boolean auth;
@Value("${mail.smtp.starttls.enable}")
private boolean starttls;
@Value("${mail.smtp.starttls.required}")
private boolean startlls_required;
@Value("${mail.smtp.socketFactory.fallback}")
private boolean fallback;
@Value("${AdminMail.id}")
private String id;
@Value("${AdminMail.password}")
private String password;
@Bean
public JavaMailSender javaMailService() {
JavaMailSenderImpl javaMailSender = new JavaMailSenderImpl();
javaMailSender.setHost("smtp.gmail.com"); //smtp 서버 주소
javaMailSender.setUsername(id); //발신 메일 아이디
javaMailSender.setPassword(password);
javaMailSender.setPort(port); //smtp port
javaMailSender.setJavaMailProperties(getMailProperties()); //메일 인증 서버 정보 가져옴
javaMailSender.setDefaultEncoding("UTF-8");
return javaMailSender;
}
private Properties getMailProperties()
{
Properties pt = new Properties();
pt.put("mail.smtp.socketFactory.port", socketPort);
pt.put("mail.smtp.auth", auth); //smtp 인증
pt.put("mail.smtp.starttls.enable", starttls); //smtp starttls 사용
pt.put("mail.smtp.starttls.required", startlls_required);
pt.put("mail.smtp.socketFactory.fallback",fallback);
pt.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
return pt;
}
}
EmailService interface
public interface EmailService {
void sendMessage(String rcv) throws Exception;
boolean verifyEmailCode(String email, String code);
}
sendMessage와 verifyEmailCode 메소드를 정의해줬다.
sendMessage는 받는 사람의 주소를 받으면 해당 주소로 인증 코드를 보내고 verifyEmailCode는 redis에서 이메일과 code값이 맞는지 확인해준다. (인증)
EmailServiceImpl
@Slf4j
@Service
@RequiredArgsConstructor
public class EmailServiceImpl implements EmailService{ //email 인증 코드 관련 서비스
private final JavaMailSender emailSender;
private final RedisUtil redisUtil;
public static final String emailCode = createKey();
@Override
public void sendMessage(String rcv) throws Exception {
MimeMessage message = createMessage(rcv);
if (redisUtil.existData(rcv)) {
redisUtil.deleteData(rcv);
}
try {
emailSender.send(message);
} catch (MailException e) {
e.printStackTrace();
throw new IllegalArgumentException();
}
redisUtil.setDataExpire(rcv, emailCode, 60 * 5L);
}
@Override
public boolean verifyEmailCode(String email, String code) {
String codeFoundByEmail = redisUtil.getData(email);
if (codeFoundByEmail == null) {
return false;
}
return redisUtil.getData(email).equals(code);
}
private MimeMessage createMessage(String rcv) throws Exception {
MimeMessage message = emailSender.createMimeMessage();
message.addRecipients(Message.RecipientType.TO, rcv); //보내는 대상
message.setSubject("이메일 인증 코드"); //제목 설정
String mailMsg="";
mailMsg+="<div>";
mailMsg+="CODE: <strong>";
mailMsg+=emailCode;
mailMsg+="</strong></div>";
message.setText(mailMsg, "utf-8", "html");
message.setFrom(new InternetAddress("pepepongpo@gmail.com", "LimGaYoung")); //보내는 사람
return message;
}
private static String createKey(){
StringBuffer key = new StringBuffer();
Random random = new Random();
for(int i=0;i<6;i++){ //6자리 인증번호, 알파벳을 섞게 될 수도 있어서 한개씩 만들었음
int index = random.nextInt(10); //0~9까지 랜덤
key.append(index);
}
return key.toString();
}
}
EmailController
url을 딱 떨어지는 뭔가로 하고 싶었는데.. 딱히 생각나는게 없어서 그냥 /mail 과 /mail/auth로 정했다..
/mail 과 /mail/email주소?code=code값 으로 하는게 나았으려나..?
code 값은 보안상 중요하지 않은 것 같아서 requestParam으로 넣었다.
@Slf4j
@Controller
@RequiredArgsConstructor
@RequestMapping("/mail")
public class EmailController { //email 인증 코드
private final EmailServiceImpl emailService;
@GetMapping("")
@ResponseBody
public ResponseEntity sendMail(@RequestParam("rcv") String emailRcv) throws Exception { //email 인증 코드 보내기
log.info("rcv: "+ emailRcv);
emailService.sendMessage(emailRcv);
return new ResponseEntity(HttpStatus.OK);
}
@GetMapping("/auth")
@ResponseBody
public ResponseEntity checkMailCode(@RequestParam("rcv") String emailRcv, @RequestParam("code") String emailCode) throws Exception {
//email 인증 코드 확인
log.info("verify 전");
boolean authCode = emailService.verifyEmailCode(emailRcv, emailCode);
if (!authCode) {
return new ResponseEntity(HttpStatus.BAD_REQUEST);
}
return new ResponseEntity(HttpStatus.OK);
}
}