UNVERIFIED
이다 (인증된 사용자가 아니라는 뜻)/api/emails
로 post 요청을 보낸다/api/emails
의 body에 담아 get 요청을 보낸다ACTIVE
로 변경한다 (인증된 사용자라는 뜻)구글 SMTP 계정 설정
참고 : https://kincoding.com/entry/Google-Gmail-SMTP-%EC%82%AC%EC%9A%A9%EC%9D%84-%EC%9C%84%ED%95%9C-%EC%84%B8%ED%8C%85
Redis 설치
mac : [https://velog.io/@wonizizi99/DB-맥북에서-redis-설치-및-사용](https://velog.i![](https://velog.velcdn.com/images/llocr/post/0b8386ea-9d8c-42ff-b254-96213f50689e/image.png)
o/@wonizizi99/DB-%EB%A7%A5%EB%B6%81%EC%97%90%EC%84%9C-redis-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EC%82%AC%EC%9A%A9)
windos : https://ittrue.tistory.com/318
공식 문서 : https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-on-mac-os/
이메일과 인증코드를 Redis에 저장할 예정이기 때문에 필요하다!
MySQL이나 다른 RDMBS를 사용할 것이라면 필요 없다
하지만, redis를 사용하면 빠른 응답 속도와 만료 기능 설정이 편리하기 때문에 이번 기회에 사용해보는 것을 추천한다!
build.gradle
의 dependencies
부분에 해당 라이브러리들을 추가해준다
implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
기존에 있던 application.yml에 해당 내용을 추가해준다
글쓴이는 env.properties로 환경변수를 관리하기 때문에 username
과 password
그리고 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
이메일 인증코드를 저장할 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;
}
}
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);
}
}
인증코드 요청은 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);
}
}
/api/emails
의 Get
요청에는 사용자가 비밀번호를 전달해 주어야 하기 때문에, 해당 DTO를 생성해서 사용하였다
@NoArgsConstructor
@Getter
public class CertificationNumberRequestDTO {
@NotBlank(message = "인증 코드를 입력해 주세요.")
private String code;
}
이메일 인증과 관련된 로직들을 모아둔 서비스 클래스이다
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;
}
}
UserService에 인증 회원으로 전환하는 로직 추가
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserService {
private final UserRepository userRepository;
/**
* 인증 회원으로 전환
*/
@Transactional
public void updateUserActive(User user) {
user.ActiveUser(); //유저 엔티티 내에서 회원 상태를 변경하는 로직 작성
userRepository.save(user);
}
}
DB에도 잘 저장된 걸 확인할 수 있다
로그인 요청을 통해 response 받은 AccessToken과 함께, 이메일 인증 요청을 보낸다
회원가입 시 입력한 이메일로 이메일 인증 요청 메일이 전달된다
이메일을 통해 전달 받은 코드를 Body를 통해 전달하면 이메일 인증이 완료 된다
DB에서도 UNVERIFIED
였던 상태가 ACTIVE
로 변경된 걸 확인할 수 있다