Spring | 이메일 인증 구현하기

바다·2024년 6월 7일
0

Spring

목록 보기
10/13
post-thumbnail

이메일 인증 과정

  1. 회원가입을 한 사용자의 상태는 UNVERIFIED이다 (인증된 사용자가 아니라는 뜻)
  2. 로그인한 사용자가 /api/emails로 post 요청을 보낸다
  3. 서버측에서 무작위 난수로 인증코드를 만들고, 유효시간을 3분으로 한다
  4. 로그인한 사용자의 이메일(회원가입 시 입력한 이메일)에 인증코드를 전달한다
  5. 사용자가 이메일을 통해 전달받은 인증코드를 /api/emails의 body에 담아 get 요청을 보낸다
  6. 사용자가 입력한 인증코드와 서버측에서 저장한 인증코드가 일치하는지 확인한다
  7. 인증코드가 일치한다면, 사용자의 상태를 ACTIVE로 변경한다 (인증된 사용자라는 뜻)

미리 설정할 사항

이메일과 인증코드를 Redis에 저장할 예정이기 때문에 필요하다!
MySQL이나 다른 RDMBS를 사용할 것이라면 필요 없다

하지만, redis를 사용하면 빠른 응답 속도만료 기능 설정이 편리하기 때문에 이번 기회에 사용해보는 것을 추천한다!


구현 과정

1. Dependencies 추가

build.gradledependencies 부분에 해당 라이브러리들을 추가해준다

implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

2. application.yml 파일 추가

기존에 있던 application.yml에 해당 내용을 추가해준다
글쓴이는 env.properties로 환경변수를 관리하기 때문에 usernamepassword 그리고 redi의 port는 아래와 같이 처리해두었다

spring:
  mail:
    host: smtp.gmail.com
    port: 587
    username: ${MAIL_USERNAME}	//구글 SMTP 설정한 이메일
    password: ${MAIL_PASSWORD}	//구글 SMTP를 설정하고 받은 password
    properties:
      mail:
        debug: true
        smtp.auth: true
        smtp.timeout: 50000
        smtp.starttls.enable: true
  data:
    redis:
      mail:
        host: localhost
        port: ${REDIS_MAIL_PORT}	//보통 6379

3. RedisConfig

이메일 인증코드를 저장할 Redis에 대해서 설정합니다

@Configuration
public class RedisConfig {
	@Value("${spring.data.redis.mail.host}")
	private String host;
	
	@Value("${spring.data.redis.mail.port}")
	private int port;
	
	private LettuceConnectionFactory lettuceConnectionFactory;
	
	@PostConstruct
	public void init() {
		lettuceConnectionFactory = new LettuceConnectionFactory(host, port);
		lettuceConnectionFactory.start();
	}
	
	@Bean
	public RedisTemplate<?, ?> redisTemplate() {
		RedisTemplate<byte[], byte[]> redisTemplate = new RedisTemplate<>();
		redisTemplate.setConnectionFactory(lettuceConnectionFactory);
		return redisTemplate;
	}
}

4. CertificationNumberRepository

Redis에 인증코드를 CRUD 할 수 있는 코드를 작성합니다

@Repository
@RequiredArgsConstructor
public class CertificationNumberRepository {
	private final StringRedisTemplate redisTemplate;
	static final int EMAIL_VERIFICATION_LIMIT_IN_SECONDS = 180;
	
	//인증코드 저장
	public void saveCertificationNumber(String email, String certificationNumber) {
		redisTemplate.opsForValue()
			.set(email, certificationNumber, Duration.ofSeconds(EMAIL_VERIFICATION_LIMIT_IN_SECONDS));
	}
	
	//인증코드 가져오기
	public String getCertificationNumber(String email) {
		return redisTemplate.opsForValue().get(email);
	}
	
	//인증코드 삭제
	public void removeCertificationNumber(String email) {
		redisTemplate.delete(email);
	}
}

5. MailController

인증코드 요청은 Post, 이메일 인증은 Get을 사용하였다

이미 로그인한 사용자라고 가정하기 때문에 @AuthenticationPrincial UserDetailsImpl userDetails를 통해 유저 객체를 받아오고, 해당 객체에 설정되어 있는 이메일 값을 사용하였다.
이 부분은 원하는대로 변경해서 값을 받아오면 될 것 같다! (DTO를 사용하든, 사용자에게 이미 저장된 email을 활용하든)

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/mails")
public class MailController {
	private final MailService mailSendService;
	private final UserService userService;
	
	/**
	 * 이메일 인증 번호 요청
	 */
	@PostMapping
	public ResponseEntity<ResponseMessage<String>> sendCertificationNumber(@AuthenticationPrincipal UserDetailsImpl userDetails) throws MessagingException, NoSuchAlgorithmException {
		String email = mailSendService.sendEmailForCertification(userDetails.getUser().getEmail());
		
		ResponseMessage<String> responseMessage = ResponseMessage.<String>builder()
			.statusCode(HttpStatus.OK.value())
			.message("인증 코드 발송이 완료되었습니다.")
			.data(email)
			.build();
		
		return new ResponseEntity<>(responseMessage, HttpStatus.OK);
	}
	
	/**
	 * 이메일 인증
	 */
	@GetMapping
	public ResponseEntity<ResponseMessage<String>> verifyCertificationNumber(@Valid @RequestBody CertificationNumberRequestDTO requestDTO, @AuthenticationPrincipal UserDetailsImpl userDetails) {
		mailSendService.verifyEmail(userDetails.getUser().getEmail(), requestDTO.getCode());
		userService.updateUserActive(userDetails.getUser());
		
		ResponseMessage<String> responseMessage = ResponseMessage.<String>builder()
			.statusCode(HttpStatus.OK.value())
			.message("이메일 인증이 완료되었습니다.")
			.data(userDetails.getUser().getEmail())
			.build();
		
		return new ResponseEntity<>(responseMessage, HttpStatus.OK);
	}
		
}

6. CertificationNumberRequestDTO

/api/emailsGet 요청에는 사용자가 비밀번호를 전달해 주어야 하기 때문에, 해당 DTO를 생성해서 사용하였다

@NoArgsConstructor
@Getter
public class CertificationNumberRequestDTO {
	@NotBlank(message = "인증 코드를 입력해 주세요.")
	private String code;
}

7. MailService

이메일 인증과 관련된 로직들을 모아둔 서비스 클래스이다

sendEmailForCertification : 이메일을 작성하기 위한 기초 작업을 해준다
sendMail : 이메일을 보내는 작업을 한다
verifyEmail : 사용자가 입력한 인증번호가 Redis에 저장한 값과 일치하는지 확인한다
createCertificatonNumber : 사용자에게 전송한 인증번호를 만든다

@Service
@RequiredArgsConstructor
public class MailService {
	private final JavaMailSender mailSender;
	private final CertificationNumberRepository certificationNumberRepository;
	public static final String MAIL_TITLE_CERTIFICATION = "STUDY WITH ME 이메일 인증";
	
	//이메일 작성 폼
	public String sendEmailForCertification(String email) throws NoSuchAlgorithmException, MessagingException {
		String certificationNumber = createCertificationNumber();
		String content = String.format("인증 번호 : " + certificationNumber + "\n인증코드를 3분 이내에 입력해주세요.");
		certificationNumberRepository.saveCertificationNumber(email, certificationNumber);
		sendMail(email, content);
		return email;
	}
	
	//이메일 보내기
	private void sendMail(String email, String content) throws MessagingException {
		MimeMessage mimeMessage = mailSender.createMimeMessage();
		MimeMessageHelper helper = new MimeMessageHelper(mimeMessage);
		helper.setTo(email);
		helper.setSubject(MAIL_TITLE_CERTIFICATION);
		helper.setText(content);
		mailSender.send(mimeMessage);
	}
	
	//인증번호 검사
	public void verifyEmail(String email, String certificationNumber) {
		if (! certificationNumberRepository.getCertificationNumber(email).equals(certificationNumber)) {
			throw new EmailException("인증번호가 일치하지 않습니다.");
		}
		
		certificationNumberRepository.removeCertificationNumber(email);
	}
	
	//인증번호 만들기
	public String createCertificationNumber() throws NoSuchAlgorithmException {
		String result;
		
		do {
			int num = SecureRandom.getInstanceStrong().nextInt(999999);
			result = String.valueOf(num);
		} while (result.length() != 6);
		
		return result;
	}
}

8. UserService

UserService에 인증 회원으로 전환하는 로직 추가

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserService {
    private final UserRepository userRepository;

    /**
     * 인증 회원으로 전환
     */
    @Transactional
    public void updateUserActive(User user) {
        user.ActiveUser();			//유저 엔티티 내에서 회원 상태를 변경하는 로직 작성
        userRepository.save(user);
    }
}

9. PostMan으로 테스트

1) 회원가입

DB에도 잘 저장된 걸 확인할 수 있다

2) 로그인

3) 이메일 인증 요청

로그인 요청을 통해 response 받은 AccessToken과 함께, 이메일 인증 요청을 보낸다

회원가입 시 입력한 이메일이메일 인증 요청 메일이 전달된다

4) 인증번호로 이메일 인증

이메일을 통해 전달 받은 코드를 Body를 통해 전달하면 이메일 인증이 완료 된다

5) DB 변경 확인

DB에서도 UNVERIFIED였던 상태가 ACTIVE로 변경된 걸 확인할 수 있다

profile
ᴘʜɪʟɪᴘᴘɪᴀɴs 3:14

0개의 댓글