Spring Boot 이메일 인증 구현하기 - 삽질중인 개발자
- 회원가입 이메일 인증 구현하기 -
이메일 인증을 구현하는 방법에는 크게 2가지 방법이 있다.
첫 번째로 접속하면 이메일 인증이 되는 특정 URL 링크를 넘겨줘서 이메일 인증을 하는 방법이고
두 번째는 인증 코드를 이메일로 보내서 이메일 코드를 입력하게 해 인증하는 방법이다.
그중 해당 포스팅에서는 특정 URL 링크를 접속하여 이메일 인증이 되는 첫 번째 방식을 구현할 것이다.
인증 이메일을 보내는 방법으로는Gmail SMTP를 이용할 것이다. ( 네이버나 다른 서버도 설정만 바꾸면 바로 적용 가능하다. )
우선 큰 흐름을 살펴보자
회원가입을 한 유저가 이메일 인증이 안된 회원인 경우 인증 메일을 보내기 -> 인증 메일 안에 있는 URL을 클릭 -> 해당 URL에 접속 시 유저의 이메일 인증 관련 DB값 변경 순으로 진행이 될 것이다.
1. SMTP 용 계정 세팅
우선 SMTP용 계정을 세팅을 해야한다.
https://www.google.com/settings/security/lesssecureapps에 접속 후 액세스 허용을 해서 SMTP를 사용 가능하도록 세팅한다.
2. pom.xml 추가
Spring boot에서는 메일 관련 기능을 starter로 빼놔서 해당 starterspring-boot-starter-mail만 추가해주면 된다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
3. yml 설정 추가
username과 password를 세팅을 해준다.
만약 네이버나 다른 SMTP 사용한다면 host, port username, password 만 알맞게 변경해주면 된다.
spring:
mail:
host: smtp.gmail.com
port: 587
username: SMTP용 google 계정
password: SMTP용 google 계정 비밀번호
properties:
mail:
smtp:
starttls:
enable: true
required: true
auth: true
connectiontimeout: 5000
timeout: 5000
writetimeout: 5000
만약 Authentication failed; nested exception is javax.mail.AuthenticationFailedException라고 에러가 뜬다면 아래 어떤 블로거가 정리한 방법으로 yml에 있는 password를 바꿔준다. ( 링크 )
4. 이메일 Service 생성
JavaMailSender 객체를 사용하여 Async 방식으로 이메일을 보낸다.
@Service
@RequiredArgsConstructor
public class EmailSenderService {
private final JavaMailSender javaMailSender;
@Async
public void sendEmail(SimpleMailMessage email) {
javaMailSender.send(email);
}
}
5. 토큰 Entity 생성
이 포스팅에서 구현하려는 토큰의 정책은 만료시간이 있어야 하며 한번 사용되면 두 번은 사용 불가능하다.
우선 토큰의 DB 스키마를 살펴보면 아래와 같다.
CONFIRMATION_TOKEN | |
id | 토큰의 PK 값 |
expiration_date | 만료 시간 |
expired | 만료 여부 |
user_id | USER 의 PK 값 |
create_date | 생성 시간 |
last_modified_date | 마지막 변경 시간 |
이를 JPA Entity로 구현하면 아래와 같다.
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ConfirmationToken {
private static final long EMAIL_TOKEN_EXPIRATION_TIME_VALUE = 5L; //토큰 만료 시간
@Id
@GeneratedValue(generator = "uuid2")
@GenericGenerator(name = "uuid2", strategy = "uuid2")
@Column(length = 36)
private String id;
@Column
private LocalDateTime expirationDate;
@Column
private boolean expired;
//일부러 FK 사용 안함
@Column
private String userId;
@CreatedDate
@Column(updatable = false)
private LocalDateTime createDate;
@LastModifiedDate
private LocalDateTime lastModifiedDate;
/**
* 이메일 인증 토큰 생성
* @param userId
* @return
*/
public static ConfirmationToken createEmailConfirmationToken(String userId){
ConfirmationToken confirmationToken = new ConfirmationToken();
confirmationToken.expirationDate = LocalDateTime.now().plusMinutes(EMAIL_TOKEN_EXPIRATION_TIME_VALUE); // 5분후 만료
confirmationToken.userId = userId;
confirmationToken.expired = false;
return confirmationToken;
}
/**
* 토큰 사용으로 인한 만료
*/
public void useToken(){
expired = true;
}
}
6. 토큰 Service 생성
@RequiredArgsConstructor
@Service
public class ConfirmationTokenService {
private final ConfirmationTokenRepository confirmationTokenRepository;
private final EmailSenderService emailSenderService;
/**
* 이메일 인증 토큰 생성
* @return
*/
public String createEmailConfirmationToken(String userId, String receiverEmail){
Assert.hasText(userId,"userId는 필수 입니다.");
Assert.hasText(receiverEmail,"receiverEmail은 필수 입니다.");
ConfirmationToken emailConfirmationToken = ConfirmationToken.createEmailConfirmationToken(userId);
confirmationTokenRepository.save(emailConfirmationToken);
SimpleMailMessage mailMessage = new SimpleMailMessage();
mailMessage.setTo(receiverEmail);
mailMessage.setSubject("회원가입 이메일 인증");
mailMessage.setText("http://localhost:8090/confirm-email?token="+emailConfirmationToken.getId());
emailSenderService.sendEmail(mailMessage);
return emailConfirmationToken.getId();
}
/**
* 유효한 토큰 가져오기
* @param confirmationTokenId
* @return
*/
public ConfirmationToken findByIdAndExpirationDateAfterAndExpired(String confirmationTokenId){
Optional<ConfirmationToken> confirmationToken = confirmationTokenRepository.findByIdAndExpirationDateAfterAndExpired(confirmationTokenId, LocalDateTime.now(),false);
return confirmationToken.orElseThrow(()-> new BadRequestException(ValidationConstant.TOKEN_NOT_FOUND));
};
}
7. 토큰 JPA Repository 생성
public interface ConfirmationTokenRepository extends JpaRepository<ConfirmationToken,String> {
Optional<ConfirmationToken> findByIdAndExpirationDateAfterAndExpired(String confirmationTokenId, LocalDateTime now, boolean expired);
}
8. 인증 Controller 생성 및 인증 로직 구현
이메일 인증을 하기 위해서는 이메일 인증 URL에 들어온 경우 토큰 ID로 조회 후 해당 UserID를 가진 User의 이메일 인증 관련 컬럼 값을 변경해주는 로직이 필요하다.
해당 포스팅에서는 UserController에 /confirm-email 이라는 Controller에 들어온 경우 위의 로직이 실행되도록 구현하였다.
@Controller
@RequestMapping("/")
@RequiredArgsConstructor
public class UserController {
//
// 생략....
//
@GetMapping("confirm-email")
public String viewConfirmEmail(@Valid @RequestParam String token){
userService.confirmEmail(token);
return "redirect:/login";
}
}
${도메인}/confirm-email?token=${token의 ID값} 으로 접근 시 token 아이디 값을 이메일 인증 로직으로 넘겨준다.
이메일 인증 관련 로직은 User Service로 뺐다.
@RequiredArgsConstructor
@Service
@Slf4j
@Transactional
public class UserService {
private final UserInfoRepository userInfoRepository;
private final ConfirmationTokenService confirmationTokenService;
/**
* 이메일 인증 로직
* @param token
*/
public void confirmEmail(String token) {
ConfirmationToken findConfirmationToken = confirmationTokenService.findByIdAndExpirationDateAfterAndExpired(token);
UserInfo findUserInfo = findById(findConfirmationToken.getUserId());
findConfirmationToken.useToken(); // 토큰 만료 로직을 구현해주면 된다. ex) expired 값을 true로 변경
findUserInfo.emailVerifiedSuccess(); // 유저의 이메일 인증 값 변경 로직을 구현해주면 된다. ex) emailVerified 값을 true로 변경
}
}
위의 로직을 구현하면 이메일에 첨부된 링크를 통한 유저의 이메일 인증이 완료된다.