이메일 인증 구현하기 : 인증 정보 저장

김재현·2024년 1월 19일
0

TIL

목록 보기
75/88

1. 문제 상황

회원가입시 이메일 인증을 할 때 사용자는 loginId, password, email 등을 입력한다.
이메일에 받은 인증코드를 입력 할 때 이러한 정보를 다시 입력하는 일 없이 회원가입이 완료된다.

이 때 입력한 데이터를 어디에, 어떻게 저장하고 넘겨주어야 할까?
보안은 괜찮을지도 고민해보았다.

2. 고민 및 해결 방안

생각해야하는 것은 데이터 저장 및 읽기, 유효시간, 보안!

  1. 단순히 쿠키에 정보를 넣어 보내는 것은 사용자의 개인정보를 노출 시킬 수 있는 위험한 행동이다. 따라서 데이터의 안전을 위해 서버측의 DB를 활용하는 것이 좋겠다.

  2. 인증 정보는 임시 데이터이므로 5분간 유효하고, 그 이후에는 삭제하자.

  3. 쿠키를 활용하여 회원가입을 하려는 당사자인지 한번 더 확인하는 것은 어떻까?
    인증코드를 발송 할 때 loginId를 쿠키에 넣어보낸다면 아래 두가지 장점이 있을 것이다.
    - 해당 쿠키의 존재유무로 인증코드 발송을 요청한 클라이언트가 맞는지 확인 가능
    - 서버에서는 loginId를 활용하여 DB에 손쉽게 접근 가능

  4. 인증 과정이 복잡하므로 @Transactional 을 사용하자.

회원정보 임시 저장에 활용할 DB는 Redis로 결정하였다.

2-1. @Transactional 사용 이유

@Transactional 어노테이션은 DB의 일관성과 안정성을 위해 계획했다.
@Transacional을 사용하면 모든 작업을 하나의 트랜잭션으로 처리한다. 따라서 만약 중간에 문제가 발생한다면 전체 메서드가 롤백되어 잘못된 회원 가입 정보가 입력되는 것을 방지 할 수 있다.

2-2. Redis 사용 이유

  1. DB를 나누어 사용하여 메인 DB인 MySQL의 부하를 줄일 수 있다.
  2. Redis는 메모리 기반이므로 MySQL보다 훨씬 빠른 응답을 제공할 수 있다.
  3. 데이터 유효시간 설정 기능이 내장되어 있다.

3. 구현

3-1. 발송

EmailAuthService

인증 번호와 함께 입력한 정보를 Redis에 임시저장하며, 5분의 유효시간을 설정한다.
이 때 EmailAuth 객체를 생성하여 저장했다.

  public void setSentCodeByLoginIdAtRedis(EmailAuth emailAuth) {
    String loginId = emailAuth.getLoginId();
    emailAuthRepository.saveEmailAuth(loginId, emailAuth);
  }

EmailAuthRepository

  public void saveEmailAuth(String key, EmailAuth emailAuth) {
    redisTemplate.opsForValue().set(key, emailAuth);
    redisTemplate.expire(key, 5 * 60, TimeUnit.SECONDS);
  }

UserController

앞에서 설계했던 대로 loginId를 갖고 있는 쿠키를 클라이언트에 보내어 2차 확인을 할 수 있게 만들었다. (쿠키 또한 5분의 유효시간을 갖는다.)

  @PostMapping("/signup")
  public ResponseEntity<ApiResponse> signup(...) {

    userService.signup(signupRequestDto);

    String loginId = signupRequestDto.getLoginId();
    Cookie cookie = emailAuthService.getCookieByLoginId(loginId);
    response.addCookie(cookie);

    return ResponseEntity.ok(new ApiResponse<>("인증 번호를 입력해주세요.", HttpStatus.OK.value()));
  }

3-2. 수신 및 회원정보 저장

UserController

FE에서 인증코드를 헤더에 넣어보내기로 했기 때문에 request header에서 verificationCode를 받아줬다.
쿠키에서 loginId를 받아오며, 클라이언트에게 해당 쿠키가 없다면 인증 과정은 더 이상 진행되지 않는다.

이후 인증 정보를 확인하는 verificateCode 메서드를 실행한다.

  @GetMapping("/signup")
  public ResponseEntity<ApiResponse> verificateCode(
      HttpServletRequest request,
      HttpServletResponse response,
      @CookieValue(EmailAuthService.LOGIN_ID_AUTHORIZATION_HEADER) String loginId) {

    String verificationCode = request.getHeader(EmailAuthService.VERIFICATION_CODE_HEADER);

    UserResponseDto userResponseDto = userService.verificateCode(verificationCode, loginId);
    
    emailAuthService.removeloginIdCookie(response);

    return ResponseEntity.ok()
        .body(new ApiResponse("회원가입 성공", HttpStatus.OK.value(), userResponseDto));
  }

UserService

verificateCode 메서드에서 코드가 동일한지 확인 후 회원 정보를 main DB에 저장한다.
인증이 완료되면 임시 데이터는 삭제한다.

앞서 계획한대로 verificateCode 메서드에 @Transactional 어노테이션은 DB의 일관성과 안정성 유지를 도모했다.

  @Transactional
  public UserResponseDto verificateCode(String verificationCode, String loginId) {

    EmailAuthDto emailAuthDto = emailAuthService.checkVerifyVerificationCode(loginId,
        verificationCode);

    			... // 입력 정보 확인

    User user = new User(loginId, nickname, email, password, firstPreferredCategory,
        secondPreferredCategory);

    userRepository.save(user);

    // 인증 완료되면 Redis의 임시 데이터 삭제
    emailAuthService.concludeEmailAuthentication(loginId);

    return new UserResponseDto(user);
  }

4. 결과

4-1. 발송

Redis의 사용자 정보

key는 loginId이며, value가 아래와 같이 나타난다.
객체로 저장했기에 JSON 형태로 출력되는 것을 볼 수 있다.

{"@class":"com.example.jujuassembly.domain.user.emailAuth.entity.EmailAuth","loginId":"asdzxc123123","nickname":"qsdqwe123123","email":"robbie@naer.com","password":"$2a$10$BYykFjajtC/5osd4Oq4ace1gSh3k5y/Xzlpaf32dMgLY64I1uJShi","firstPreferredCategoryId":3,"secondPreferredCategoryId":4,"sentCode":"664099"}

쿠키가 잘 발행되었다.

4-2. 회원가입 완료

Redis의 데이터와 쿠키는 삭제되었다.

MySQL (메인 DB)

데이터가 잘 들어가서 회원가입이 완료되었다.


이메일 인증에 5분의 유효시간, 재입력 방지, 보안 문제라는 요구사항들을 충족시키며 기능을 구현 할 수 있었다.


관련 포스팅

Previouse Post

profile
I live in Seoul, Korea, Handsome

0개의 댓글