SpringBoot

개인 프로젝트 - 회원가입 시 이메일 인증

똑똑한망치 2024. 3. 20. 16:22
728x90
반응형

🤔 고민

회원가입 시 이메일을 입력받고 이메일 인증을 위한 기능을 사용해보고 싶다.

사용 방법은 2가지 이다.

  1. 입력받은 이메일로 인증번호를 전송하고 인증번호를 입력받아 회원가입을 완료하는 방법
  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로 변경
    }
}

 

 

 

필요한 부분은 추가적으로 더 구현한다면 유저의 이메일 인증이 완료된다!

반응형