프로젝트 정리/스프링과 JPA 기반 웹 애플리케이션 개발

10. 회원 가입: 인증 메일 확인

출처 

www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-JPA-%EC%9B%B9%EC%95%B1/dashboard

 

스프링과 JPA 기반 웹 애플리케이션 개발 - 인프런 | 강의

이 강좌에서 여러분은 실제로 운영 중인 서비스를 스프링, JPA 그리고 타임리프를 비롯한 여러 자바 기반의 여러 오픈 소스 기술을 사용하여 웹 애플리케이션을 개발하는 과정을 학습할 수 있습

www.inflearn.com

 

github.com/devjun63/whiteship-studyolle/commit/0480b970573e43c9a144008c7ed4f85c3195e120

 

10. 회원 가입: 인증 메일 확인 · devjun63/whiteship-studyolle@0480b97

GET "/check-email-token" token=${token} email=${email} 요청 처리 AccountService에서 saveNewAccount로 반환된 객체가 detached되어 DB에 싱크 되지 않는 에러 Transactional 어노테이션 추가하여 수정 토큰 TestCase 추가

github.com

 

 

GET "/check-email-token" token=${token} email=${email} 요청 처리

  • 이메일이 정확하지 않은 경우에 대한 에러 처리
  • 토큰이 정확하지 않은 경우에 대한 에러 처리
  • 이메일과 토큰이 정확한 경우 가입 완료 처리
    • 가입 일시 설정
    • 이메일 인증 여부 true로 설정

인증 확인 뷰

  • 입력값에 오류가 있는 경우 적절한 메시지 출력
  • 인증이 완료된 경우, 환영 문구와 함께 몇 번째 사용자인지 보여줄 것

이메일 인증을 하는 이유

이메일 인증을 하지 않는 경우 무작위 엉터리 이메일로 가입하는 경우가 많아진다.

실제 이메일 계정이 아닌 무작위 계정이 늘어나기에 실제 유저를 확보하기도 어렵다.

알림 메세지같은 경우 제대로 된 메시지를 보낼 수 없다.

서비스에서 의사소통에 문제가 생긴다. -> 서비스 장애

소셜 인증 적용 -> 커버가 된다. (facebook, kakaotalk, google등)

 

자체 이메일 검증 기능 보유

뷰에서 에러의 이유를 구체적으로 설명하지 않아도 된다.

보안과 관련된 응답 정보는 모호하게 제공하라 ( 해커의 대조시도 등 방지)

 

 

AccountController에 get방식으로 checkEmailToken Method

@GetMapping("/check-email-token")
    public String checkEmailToken(String token, String email, Model model) {
        Account account = accountRepository.findByEmail(email);
        String view = "account/checked-email";

        if (account == null){
            model.addAttribute("error","wrong email");
            return view;
        }
        if(!account.getEmailCheckToken().equals(token)){
            model.addAttribute("error","wrong email");
            return view;
        }

        account.setEmailVerified(true);
        account.setJoinedAt(LocalDateTime.now());
        model.addAttribute("numberOfUser", accountRepository.count());
        model.addAttribute("nickname", account.getNickname());
        return view;
    }

템플릿 checked-email.html 생성

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"><!--thymeleaf 사용하기 위한 xmlnamespace 설정-->
<head>
    <meta charset="UTF-8">
    <title>StudyOlle</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
    <!--style은 head에 위치시켜 rendering 적용시켜 렌더링 될 때 스타일 적용시켜 읽히고-->
    <style>
        .container {
            max-width: 100%:
        }
    </style>
</head>
<body class="bg-light">
    <nav class="navbar navbar-expand-sm navbar-dark bg-dark">
        <a class="navbar-brand" href="/" th:href="@{/}">    <!--thymeleaf로 렌더링 할때 href값을 이 값을 쓴다.-->
            <img src="/images/logo_sm.png" width="30" height="30">
        </a>
        <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent">
            <span class="navbar-toggler-icon"></span>
        </button>

        <div class="collapse navbar-collapse" id="navbarSupportedContent">
            <ul class="navbar-nav mr-auto">
                <li class="nav-item">
                    <form th:action="@{/search/study}" class="form-inline" method="get">
                        <input class="form-control mr-sm-2" name="keyword" type="search" placeholder="스터디 찾기">
                    </form>
                </li>
            </ul>

            <ul class="navbar-nav justify-content-end">
                <li class="nav-item">
                    <a class="nav-link" href="#" th:href="@{/login}">로그인</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="#" th:href="@{/signup}">가입</a>
                </li>
            </ul>
        </div>
    </nav>

    <div class="py-5 text-center" th:if="${error}">
        <p class="lead">스터디 올래 이메일 확인</p><!-- lead는 일반 글자체 보다 얅고 크다-->
        <div class="alert alert-danger" role="alert">
            이메일 확인 링크가 정확하지 않습니다.
        </div>
    </div>

    <div class="py-5 text-center" th:if="${error == null}"><!--p ->padding y -> top bottom py-5 (5el) -->
        <p class="lead">스터디 올래 이메일 확인</p>
        <h2>
            이메일을 확인 했습니다. <span th:text="${numberOfUser}">10</span>번째 회원,
            <span th:text="${nickname}">위스키</span>님 가입을 축하합니다.
        </h2>
        <small class="text-info">이제부터 가입할 때 사용한 이메일 또는 닉네임과 패스워드로 로그인 할 수 있습니다.</small>
    </div>
</body>
</html>

가입 후 나온 토큰과 이메일 값 

토큰값 누락
디버깅

 

 

AccountService의 이 부분이 문제다.

결과부터 말하자면 Transaction이 없어서 생성한 토큰이 DB에 저장되지 않았다.

public void processNewAccount(SignUpForm signUpForm) {
        Account newAccount = saveNewAccount(signUpForm);	// detached (분리된)상태
        newAccount.generateEmailCheckToken(); //UUID
        sendSignUpConfirmEmail(newAccount);
    }

    private Account saveNewAccount(@Valid SignUpForm signUpForm) {
        Account account = Account.builder()
                .email(signUpForm.getEmail())
                .nickname(signUpForm.getNickname())
                .password(passwordEncoder.encode(signUpForm.getPassword()))
                .studyCreateByWeb(true)
                .studyEnrollmentResultByWeb(true)
                .studyUpdateByWeb(true)
                .build();
        return accountRepository.save(account);
        // jpa entity lifecycle persist 상태
    }
    
    ->
    
    @Transactional
    public void processNewAccount(SignUpForm signUpForm) {
        Account newAccount = saveNewAccount(signUpForm);	//persist 유지
        newAccount.generateEmailCheckToken(); //UUID
        sendSignUpConfirmEmail(newAccount);
        -> transaction상태 해제되면서 DB에 싱크함
    }

    private Account saveNewAccount(@Valid SignUpForm signUpForm) {
        Account account = Account.builder()
                .email(signUpForm.getEmail())
                .nickname(signUpForm.getNickname())
                .password(passwordEncoder.encode(signUpForm.getPassword()))
                .studyCreateByWeb(true)
                .studyEnrollmentResultByWeb(true)
                .studyUpdateByWeb(true)
                .build();
        return accountRepository.save(account);
        // jpa entity lifecycle persist 상태
    }

 

saveNewAccount 메서드의 accountRepsository.save는 persist상태이지만

processNewAccount에서 리턴된 상태의 newAccount는 detached(분리)된 상태여서

DB에 싱크가 제대로 되지 않는다.

@Transactional 어노테이션을 붙이면 persist상태가 유지된다.

persist 상태의 객체는 transaction상태가 종료 될때 DB에 sync하게 된다.

 

트랜잭션으로 DB에 토큰값을 제대로 저장 후 나온 뷰 페이지

 

토큰값이나 이메일 값이 이상할 경우 나오는 뷰 페이지

 

JPA 엔티티 생명주기 및 영속성 컨텍스트 참고 자료

ict-nroo.tistory.com/130

 

[JPA] 영속성 컨텍스트와 플러시 이해하기

영속성 컨텍스트 JPA를 공부할 때 가장 중요한게 객체와 관계형 데이터베이스를 매핑하는 것(Object Relational Mapping) 과 영속성 컨텍스트를 이해하는 것 이다. 두가지 개념은 꼭 알고 JPA를 활용하자.

ict-nroo.tistory.com