오늘은 이메일 인증을 구현해보자!
지난 프로젝트에서는 회원가입과정에서 이메일이 인증되어야 회원가입을 진행할 수 있게 만들었다.
이번엔 좀 더 가혹하게 회원가입은 시켜주고 이메일 인증이 안되었다면 어디로도 갈 수 없게 가둬버리겠다.
이메일 인증을 구현하려하니 큰 고민이 생겨버렸다.
지난 프로젝트에서는 회원의 이메일 인증 여부를 회원필드에 만들어버렸다.
가만... 생각해보니 아니, 이메일 인증은 회원가입하고 따악 한번 쓸 정보인데... 회원필드에 존재하는게 너무나 속상한 것이다..
그래서! Attr 이라는 Entity 를 새로 신설해줬다.
우리의 영웅 Attr 은 이메일 인증정보처럼 1회성에 한하는 데이터와 어디에 속해야 할까 애매~한 녀석들을 보관해줄 친절한 친구가 될 것이다.
우선 회원가입시에 각 회원에게 고유하고 랜덤한 코드를 만들어 부여해주고 그 코드를 포함한 url 을 만들어 회원의 이메일로 발송 해줬다.
@Transactional
public CompletableFuture<RsData> send(Member member) {
String subject = "[%s 이메일인증] 안녕하세요 %s님. 링크를 클릭하여 회원가입을 완료해주세요."
.formatted(
AppConfig.getSiteName(),
member.getUsername()
);
String body = genEmailVerificationUrl(member);
return emailService.sendAsync(member.getEmail(), subject, body);
}
private String genEmailVerificationUrl(long memberId) {
String code = genEmailVerificationCode(memberId);
String verificationUrl = AppConfig.getSiteBaseUrl() + "/emailVerification/verify?memberId=%d&code=%s".formatted(memberId, code);
// HTML 이메일 본문을 생성합니다.
String htmlEmailBody = String.format(
"<html>" +
" <body>" +
" <p>이메일 주소를 확인하려면 다음 링크를 클릭하세요: " +
" <a href='%s'>이메일 인증하기</a>" +
" </p>" +
" </body>" +
"</html>",
verificationUrl);
return htmlEmailBody;
}
private String genEmailVerificationCode(long memberId) {
String code = UUID.randomUUID().toString();
attrService.set("member__%d__extra__emailVerificationCode".formatted(memberId), code, LocalDateTime.now().plusSeconds(60 * 60));
return code;
}
@Transactional
public RsData verify(long memberId, String code) {
RsData checkVerificationCodeValidRs = checkVerificationCodeValid(memberId, code);
if (!checkVerificationCodeValidRs.isSuccess()) return checkVerificationCodeValidRs;
setEmailVerified(memberId);
return RsData.of("S-1", "이메일인증이 완료되었습니다.");
}
private RsData checkVerificationCodeValid(long memberId, String code) {
String foundCode = attrService.get("member__%d__extra__emailVerificationCode".formatted(memberId), "");
if (!foundCode.equals(code)) return RsData.of("F-1", "만료 되었거나 유효하지 않은 코드입니다.");
return RsData.of("S-1", "인증된 코드 입니다.");
}
마지막으로 Spring Security 를 통해 우리 사이트에서 회원의 모든 이동을 통제해주겠다.
.requestMatchers(
requestMatchersOf("/", "/usr/**")
).access(accessOf("isAnonymous() or @memberController.assertCurrentMemberVerified()"))
당연하게도 assertCurrentMemberVerified() 는 attr 을 조사하여 이 멤버가 email verified 에 대해 true 값을 가지고 있는지 조사해준다.
attr 은 이런 구조로 만들었고 (고유 코드값과 email verified 가 true 인지에 대해 각 각 저장되어있다)
실제로 실행해보면!
이메일 인증을 안하니 갇혀버렸고!
인증을 위한 이메일도 안정감 있게 잘 날라와 주었다! 하하!!
오늘도 성공적이다.