본문 바로가기

Spring/spring boot

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로 변경
    }
}

위의 로직을 구현하면 이메일에 첨부된 링크를 통한 유저의 이메일 인증이 완료된다.

 

 

 

 

반응형