프로젝트 Motivation - Email Verified 와 attr

youngkyu MIn·2023년 11월 13일
0

오늘은 이메일 인증을 구현해보자!

지난 프로젝트에서는 회원가입과정에서 이메일이 인증되어야 회원가입을 진행할 수 있게 만들었다.
이번엔 좀 더 가혹하게 회원가입은 시켜주고 이메일 인증이 안되었다면 어디로도 갈 수 없게 가둬버리겠다.

이메일 인증을 구현하려하니 큰 고민이 생겨버렸다.
지난 프로젝트에서는 회원의 이메일 인증 여부를 회원필드에 만들어버렸다.
가만... 생각해보니 아니, 이메일 인증은 회원가입하고 따악 한번 쓸 정보인데... 회원필드에 존재하는게 너무나 속상한 것이다..

그래서! 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;
}

이메일 인증하기를 클릭하면 도달 할 엔드포인트에서는 회원에게 부여 된 코드와 url 에 심겨진 코드를 비교하고 성공시 Attr Entity 에 해당 회원의 email verified 상태가 true 임을 저장해주는 로직을 호출해준다.
@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 인지에 대해 각 각 저장되어있다)


실제로 실행해보면!

이메일 인증을 안하니 갇혀버렸고!

인증을 위한 이메일도 안정감 있게 잘 날라와 주었다! 하하!!

오늘도 성공적이다.

profile
한 줄 소개

0개의 댓글