기획단계
대부분의 사이트에서 회원가입 시 이메일 인증을 요구하고 있다.
인증 없이 회원가입이 가능하다면 존재하지 않는 이메일을 통해서도 회원가입이 가능하고 공격에 취약해지기 때문에 이메일 인증은 필수라고 생각된다.
이메일 인증을 구현하기 앞서 두 가지 방법을 고려하였다.
- 이메일로 인증번호를 보내서 다시 그 인증번호를 입력하는 방식
- 해당 이메일로 보낸 링크를 클릭하여 회원가입을 완료하는 방식
프론트와의 회의 끝에 이번 프로젝트에서는 1번째 방법 채택하여 구현하기로 하였다.
Email 준비
Gmail SMTP
- 구글 SMTP 서버를 사용해서 이메일 발송하기로 결정하였다.
- 보안 수준이 낮은 앱의 액세스를 활성화한 계정에서는 사용자 이름과 비밀번호를 사용해서 Gmail SMTP와 같은 서드 파티 앱에 인증할 수 있었지만 이제는 사용자 이름과 비밀번호를 사용해서 서드 파티 앱과 기기에 로그인 요청하는 것을 지원하지 않습니다.
- 보안 수준이 높은 Gmail 계정을 만들고 다른 방식으로 인증을 통해 SMTP을 사용하는 방식을 사용하면 되었습니다.
해결
- 2단계 인증 활성화
- 앱 비밀번호 추가 (Gmail SMTP)
더 자세한 방법을 하고 싶다면 https://kdevkr.github.io/gmail-smtp/ 위 링크를 통해 도움을 얻을 수 있습니다.
설정 정보
(1) Build.gradle 파일에 dependencies 추가
dependencies{
implementation 'org.springframework.boot:spring-boot-starter-mail'
}
(2) application-mail.yaml
spring:
mail:
host: smtp.gmail.com
port: 587
protocol: smtp
default-encoding: UTF-8
username: devcourse.linkbook@gmail.com
password:
properties:
mail:
smtp:
auth: true
starttls:
enable: true
required: true
(3) EmailConfig.class
@Configuration
@PropertySource("classpath:application-mail.yaml")
public class EmailConfig {
@Value("${spring.mail.host}")
private String host;
@Value("${spring.mail.port}")
private int port;
@Value("${spring.mail.protocol}")
private String protocol;
@Value("${spring.mail.default-encoding}")
private String defaultEncoding;
@Value("${spring.mail.username}")
private String username;
@Value("${spring.mail.password}")
private String password;
@Value("${spring.mail.properties.mail.smtp.starttls.enable}")
private boolean starttls;
@Value("${spring.mail.properties.mail.smtp.auth}")
private boolean auth;
@Bean
public JavaMailSenderImpl mailSender() {
JavaMailSenderImpl javaMailSender = new JavaMailSenderImpl();
Properties properties = new Properties();
properties.put("mail.smtp.starttls.enable", starttls);
properties.put("mail.smtp.auth", auth);
javaMailSender.setProtocol(protocol);
javaMailSender.setHost(host);
javaMailSender.setPort(port);
javaMailSender.setDefaultEncoding(defaultEncoding);
javaMailSender.setUsername(username);
javaMailSender.setPassword(password);
javaMailSender.setJavaMailProperties(properties);
return javaMailSender;
}
}
구현 로직
(1) 이메일을 Client로부터 입력받는다.
EmailController.class
@PostMapping("/api/emails")
public ResponseEntity<Void> sendEmail(HttpServletRequest request, @RequestBody EmailRequestDto requestDto) {
emailService.sendEmail(request.getSession(), requestDto);
return ResponseEntity.ok().build();
}
(2) 입력 받은 email을 Gmail SMTP 이용해서 이메일을 전송한다.
EmailService.class
public void sendEmail(HttpSession httpSession, EmailRequestDto requestDto) {
try {
MimeMessage mimeMessage = emailSender.createMimeMessage();
String userEmail = requestDto.getEmail();
String key = createKey();
String message = createMessage(userEmail, key);
httpSession.setAttribute(userEmail, Map.of(CERTIFICATION_KEY, key,IS_CERTIFICATION, false));
httpSession.setMaxInactiveInterval(60 * 30);
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage);
helper.setTo(userEmail);
helper.setSubject("회원가입 이메일 인증");
helper.setText(message, true);
emailSender.send(mimeMessage);
} catch (Exception e) {
throw new EmailSendFailureException();
}
}
// mail 생성
private String createMessage(String userEmail, String key) {
log.info("userEmail : {}", userEmail);
log.info("certificationNumber : {}", key);
String message="";
message+= "<div style='margin:100px;'>";
message+= "<h1> 안녕하세요 LinkBook입니다. </h1>";
message+= "<br>";
message+= "<p>아래 코드를 회원가입 창으로 돌아가 입력해주세요<p>";
message+= "<br>";
message+= "<div align='center' style='border:1px solid black; font-family:verdana';>";
message+= "<h3 style='color:blue;'>회원가입 인증 코드입니다.</h3>";
message+= "<div style='font-size:130%'>";
message+= "CODE : <strong>";
message+= key+"</strong><div><br/> ";
message+= "</div>";
return message;
}
// 암호키 생성
private String createKey() {
StringBuilder key = new StringBuilder();
Random random = new Random();
for (int i = 0; i < 8; i++) {
int index = random.nextInt(3);
switch (index) {
case 0: // a~z
key.append((char) ((int) (random.nextInt(26)) + 97));
break;
case 1: // A~Z
key.append((char) ((int) (random.nextInt(26)) + 65));
break;
case 2: // 0~9
key.append((random.nextInt(10)));
break;
}
}
return key.toString();
}
- 암호키 생성한다.
- html 파일 형식을 String 형식으로 만들고 위에서 생성한 암호키를 포함한 메세지를 생성한다.
- 해당 메세지를 emailSender를 통해서 메일을 전송한다.
- 전송한 이메일(Key), 암호키(Value)를 Session에 저장해 둔다.
- value에는 두가지 정보를 저장해둔다.
- CERITIFICATION_KEY : 암호키
- IS_CERTIFICATION : 인증된 이메일 인지 판단
- 이메일과 암호키를 입력받아서 검증에 통과한다면 true로 바뀐다.
- value에는 두가지 정보를 저장해둔다.
(3) Client로 부터 이메일과 암호키(이메일로 전송된)를 입력받는다.
EmailController.class
@PostMapping("/api/emails/certification")
public ResponseEntity<Void> emailCertification(HttpServletRequest request, @RequestBody EmailCertificationRequestDto requestDto) {
emailService.emailCertification(request.getSession(), requestDto);
return ResponseEntity.ok().build();
}
(4) 세션에 있는 값을 통해서 입력받은 이메일과 암호키가 일치하는지 검증한다.
EmailService.class
public void emailCertification(HttpSession httpSession, EmailCertificationRequestDto requestDto) {
String userEmail = requestDto.getEmail();
String userKey = requestDto.getKey();
Map value = (Map) httpSession.getAttribute(userEmail);
String key = (String) value.get(CERTIFICATION_KEY);
if(!key.equals(userKey)) {
throw new EmailCertificationFailureException();
}
httpSession.setAttribute(userEmail, Map.of(CERTIFICATION_KEY, key,IS_CERTIFICATION, true));
log.info("이메일 인증 성공");
}
- httpSession에 이메일(key)을 통해서 암호키를 가져와서 일치하는지 검증한다.
- 이메일 검증에 통과한다면 IS_CERTIFICATION value 값으로 true로 바꾸고 다시 session에 저장한다.
(5) 회원가입
UserController
@PostMapping("/api/users/register")
public ResponseEntity<Void> register(HttpServletRequest request, @RequestBody RegisterRequestDto requestDto) {
HttpSession httpSession = request.getSession();
userService.register(httpSession, requestDto);
return ResponseEntity.ok().build();
}
UserService
@Transactional
public void register(HttpSession httpSession, RegisterRequestDto requestDto) {
if (userRepository.existsByEmail(requestDto.getEmail())) {
throw new DuplicatedEmailException();
}
User user = requestDto.toEntity();
emailService.IsCertificatedEmail(httpSession, user.getEmail());
user.encodePassword(passwordEncoder.encode(requestDto.getPassword()));
User saveUser = userRepository.save(user);
}
EmailService
public void IsCertificatedEmail(HttpSession httpSession, String userEmail) {
Map value = (Map) httpSession.getAttribute(userEmail);
if(value == null) {
throw new EmailIsNotCertificatedException();
}
String key = (String) value.get(CERTIFICATION_KEY);
Boolean isCertification = (Boolean) value.get(IS_CERTIFICATION);
if(!isCertification) {
throw new EmailIsNotCertificatedException();
}
}
- 회원 가입 요청 시 해당 이메일이 인증된 이메일인지를 검증한다.
- httpSession을 통해서 IS_CERTIFICATION 값이 true일 때만 회원가입에 성공한다.
Reference
https://kdevkr.github.io/gmail-smtp/
반응형
'프로젝트 > LinkBook' 카테고리의 다른 글
RefreshToken 도입 (0) | 2022.08.23 |
---|---|
AccessToken 로그인 로직 (feat. Spring Security) (0) | 2022.08.23 |
토큰(JWT)기반 인증 도입 (0) | 2022.08.23 |
Batch Fetch Size를 통해 페이징 문제 해결 (0) | 2022.08.20 |
계층형 Category 구현 (Enum으로의 전환) (0) | 2022.08.19 |
댓글