SpringBoot
개인 프로젝트 - 회원가입 시 이메일 인증
똑똑한망치
2024. 3. 20. 16:22
728x90
반응형
🤔 고민
회원가입 시 이메일을 입력받고 이메일 인증을 위한 기능을 사용해보고 싶다.
사용 방법은 2가지 이다.
- 입력받은 이메일로 인증번호를 전송하고 인증번호를 입력받아 회원가입을 완료하는 방법
- 입력받은 이메일로 인증링크를 받아 클릭하여 이메일을 인증하는 방법
2번 방법을 선택하여 개발하기로 하였다 !!!
왜냐하면,
1. 보안적인 측면
사용자의 이메일 계정과 더 강력한 연결을 할 수 있고, 인증번호의 경우 유출의 위험이 있기 때문에 유일하게 액세스를 할 수 있는 링크 방식이 더 적합하다고 판단하였다.
2. 편리성
사용자의 입장에서 링크를 클릭하는 방식이 별도의 입력 과정이 없기 때문에 편리하다고 생각하였다!!
🔺 코드로 구현해보자
우선 큰 흐름을 살펴보자.
회원가입을 한 유저가 이메일 인증이 안된 회원일 경우 인증 메일을 보낸다 -> 인증 메일 안에 있는 URL을 클릭한다 -> 해당 URL에 접속 시 유저의 이메일 인증 관련 데이터베이스 값이 변경된다.
(1) SMTP 용 계정 세팅
- Google 계정 > 보안
- Google에 로그인하는 방법 > 2단계 인증
- 앱 비밀번호 생성하기
이 때, 생성된 비밀번호를 잊어버리지 않도록 중요한 곳에 메모해놓자!!!
(2) build.gradle 추가
dependencies {
// .. (생략) ..
// mail 전송을 위한 dependency 추가
implementation 'org.springframework.boot:spring-boot-starter-mail'
}
(3) application.yml 파일 작성
spring:
mail:
host: smtp.gmail.com
port: 587
username: 위의 과정에서 사용한 이메일 (Ex, test@gmail.com)
password: 위의 과정을 통해 생성된 앱 비밀번호
properties:
mail:
smtp:
starttls:
enable: true
requred: true
auth: true
connectiontimeout: 5000
timeout: 5000
writetimeout: 5000
application.properties 에 작성해도 무방하다.
application.yml / application.properties는 모두 사용 가능하지만 yml을 먼저 읽고 properties 파일을 읽기 때문에 중복된 내용이 있다면 yml 파일에 작성한 내용이 덮어씌어져 무용지물이 될 수 있다.
따라서 웬만하면 하나의 파일로 통일하는 것을 추천한다.
(4) 이메일 전송 Service 생성
JavaMailSender 객체를 사용하여 Async 방식으로 이메일을 보낸다.
@Service
@RequiredArgsConstructor
public class EmailSenderService {
private final JavaMailSender javaMailSender;
@Async
public void sendEmail(SimpleMailMessage email) {
javaMailSender.send(email);
}
}
(5) 토큰 Entity 생성
내가 사용하려는 토큰은 만료시간이 존재하고 한 번 사용되면 다시 사용하지 못하도록 할 것이다.
CONFIRMATION_TOKEN | |
id | CONFIRMATION_TOKEN 엔티티의 기본키 |
expiration_date | 토큰의 만료시간 |
expired | 토큰이 만료되었는지 판단 |
user_id | USER의 기본키 값 |
create_date | 생성 시간 |
last_modified_date | 마지막 수정 시간 |
이 엔티티를 JPA Entity로 구현해보자.
@Entity
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ConfirmationToken {
// 만료 시간은 5분으로 설정
private static final long EMAIL_TOKEN_EXPIRATION_TIME_VALUE = 5L;
@Id
@GeneratedValue(generator = "uuid2")
@GenericGenerator(name = "uuid2", strategy = "uuid2")
private String id;
// 우선 연관관계 매핑을 설정하지 않았다.
private String userId;
private LocalDateTime expirationDate;
private boolean expired;
@CreatedDate
@Column(updatable = false)
private LocalDateTime createDate;
public static ConfirmationToken createEmailConfirmationToken(String userId) {
ConfirmationToken confirmationToken = new ConfirmationToken();
confirmationToken.expirationDate = LocalDateTime.now().plusMinutes(EMAIL_TOKEN_EXPIRATION_TIME_VALUE);
confirmationToken.userId = userId;
confirmationToken.expired = false;
return confirmationToken;
}
public ConfirmationToken expiredTimeDone(ConfirmationToken confirmationToken) {
confirmationToken.setExpired(true);
return confirmationToken;
}
}
(6) 토큰 Service 생성
@Service
@RequiredArgsConstructor
public class ConfirmationTokenService {
private final ConfirmationTokenRepository confirmationTokenRepository;
private final EmailSenderService emailSenderService;
/**
* 이메일 인증 토큰 생성
* @param userId
* @param receiverEmail
* @return
*/
public String createEmailConfirmationToken(String userId, String receiverEmail) {
Assert.hasText(userId, "ID는 필수입니다.");
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:8080/confirm-email?token="+emailConfirmationToken.getId());
emailSenderService.sendEmail(mailMessage);
return emailConfirmationToken.getId();
}
// 유효한 토큰 가져오기
public ConfirmationToken findByIdAndExpirationDateafterAndExpired(String confirmationTokenId) {
Optional<ConfirmationToken> confirmationToken = confirmationTokenRepository.findByIdAndExpirationDateAfterAndExpired(
confirmationTokenId,
LocalDateTime.now(), false);
return confirmationToken.orElseThrow(RuntimeException::new);
}
}
(7) 토큰 JPA Repository 생성
@Repository
public interface ConfirmationTokenRepository extends JpaRepository<ConfirmationToken, String>{
Optional<ConfirmationToken> findByIdAndExpirationDateAfterAndExpired(String confirmationTokenId, LocalDateTime now, boolean expired);
}
(8) 인증 Controller 생성
@RestController
@RequiredArgsConstructor
@RequestMapping("/")
public class UserController {
private final UserService userService;
@GetMapping("confirm-email")
public ConfirmationToken confirmEmail(
@Validated @RequestParam String token
) {
return userService.confirmEmail(token);
}
(9) 인증 관련 로직은 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로 변경
}
}
필요한 부분은 추가적으로 더 구현한다면 유저의 이메일 인증이 완료된다!
반응형