[Spring Boot] 비밀번호 재설정 링크를 이메일로 전송하기 (Redis 토큰)

손은실·2024년 6월 21일
0

Spring Boot

목록 보기
7/13
post-thumbnail

들어가며

지금까지의 프로젝트에서 사용자의 비밀번호 변경을 서버에서 처리해, 보안성이 매우 낮았습니다.

개발자도 사용자의 개인 정보를 열람할 수 없도록 하기 위해 BCryptPasswordEncoder로 암호화하여 저장했습니다. 하지만 이를 복호화할 수 없어, 새 비밀번호로 변경해야 했습니다.

안전하게 변경하기 위해 비밀번호 변경 링크를 사용자의 이메일로 전송하는 방법을 도입하고, 링크에 접속하는 자의 신원 확인을 위해 Token을 발급했습니다.

해당 포스팅에는 코드 부분만 나와있으며, import와 의존성 주입 등 자세한 전체 코드는 북마키에서 보실 수 있습니다.

개발 환경
Spring Boot 3 / Java 17 / Spring Security 6 / MySQL / Redis / JavaMailSender




설계 (구조와 실행 흐름)

일단 필요한 기능을 생각해봤습니다.

  1. 메일 생성+전송 → JavaMailSender
  2. 요청 들어온 이메일 주소가 존재하는 사용자인지 확인 → 토큰 발급 전 확인
  3. 링크에 접근 가능한지 확인 → 일회용 토큰
  4. 토큰 관리(저장 · 발급 · 유효성 검증 · 무효화) → Redis
  5. 보안 질문 검증

Token

  • 이메일로 전송된 링크에 무분별하게 비밀번호 변경 API 요청이 들어오는 것을 방지하기 위해 토큰을 사용합니다.
  • 24시간 동안 1회 사용 가능한 일회용 토큰을 발급합니다.

Redis

  • 사용자마다 개인의 토큰을 저장하기 위해 Key-Value 구조의 NoSQL DB를 사용합니다.
  • 토큰 유효성 검증과 무효화를 간단히 수행할 수 있습니다.

전체적인 흐름은 아래 그림과 같습니다.


토큰 발급 + 메일 전송

🟢 build.gradle

필요한 의존성을 추가합니다.


🟢 application.yml

  • RedisSMTP 정보들을 작성합니다.
    • 저는 실제 프로젝트에서는 민감한 정보들을 숨기기 위해 application-private.yml 파일을 따로 만들어 application.yml에 연결시키고 private 파일은 gitignore에 등록했습니다.
    • SMTP의 비밀번호는 Gmail 앱 비밀번호를 사용하시길 추천합니다!
      (Google 계정에서 2차 인증 활성화 후 생성 가능)
  • props메일로 전송될 주소를 환경 변수로 설정합니다.
    • 코드 내에 주소를 기재에 하드코딩하는 것은 추천하지 않습니다.
    • 보안, 유지보수, 확장성, 테스트 등의 문제

🟢 UserController

메일 요청 API

사용자의 이메일 주소를 전달하며 비밀번호 변경 메일을 요청합니다.


🟢 UserService

이메일 전송을 위해 필요한 절차는 크게 3가지입니다.

  • 존재하는 사용자인지 확인
  • 토큰 생성 (resetTokenService 호출)
  • 메일 작성+ 생성 (mailService 호출)

sendResetEmailWithToken 메서드는 컨트롤러에서 호출되며, 내부 로직은 단일 메서드로 분리하고 private으로 정의해 외부로부터 보호합니다.


🟢 MailService

1) 메일 작성 및 전송

  • generateEmail 메서드를 호출하면 메일 작성과 전송이 수행되며, 내부 로직은 단일 메서드로 분리했습니다.
  • 메일 전송 성공 → log로 성공했다는 기록을 남깁니다.
  • 메일 전송 실패 → RuntimeException을 발생시킵니다.

2) 변수 설정 (하드코딩 지양)

  • yml 파일에서 환경 변수가 변경되면, 코드 내에 사용된 모든 곳에서 자동으로 변경됩니다.
  • 메일 내부 내용들도 상수로 선언했습니다.

🟢 ResetTokenService

1) 토큰 생성 + Redis 저장

  1. UUID 생성
  2. 식별자를 앞에 붙여 비밀번호 변경 Token임을 표시 TOKEN_PREFIX
  3. 24시간 제한과 함께 Token 저장
  • ValueOperations<Key, Value> : Redis와 같은 외부 캐시 저장소와 활용 가능한 Spring Data Redis의 인터페이스, 분산 환경에서 주로 사용

토큰을 Key로 설정한 이유?
토큰 유효성 확인, 무효화할 때 간단하게 Redis에서 해당 key의 존재 여부를 확인하면 되므로 O(1) 시간 복잡도로 조회 가능합니다.

2) 토큰 유효성 검증

3) 토큰 무효화

토큰 생성 & 무효화 메서드에 @Transactional을 붙인 이유?

  • 비밀번호 변경과 토큰 무효화를 하나의 트랜잭션 내에서 수행하여, 원자성을 보장하기 위함입니다.
  • 두 작업이 모두 성공하거나 하나라도 실패하면 모두 롤백되도록 하여, 일관성 있는 처리가 이루어지도록 하고자 했습니다.


테스트

SMTP LOG

application.yml에서 debug: true 를 하면 로그를 볼 수 있습니다.

  • Gmail에 정상적으로 로그인

  • 메일이 작성되는 과정


정상적으로 메일이 전송됐을 때


존재하지 않는 이메일로 전달됐을 때

  • 코드 상에서 존재하지 않는 이메일은 API 요청을 거부하기에 이메일이 반송되는 경우는 없습니다.
  • 구현 과정에서 반송된다는 것을 알게되어, 해당 내용을 추가했습니다.


토큰 검증 + 비밀번호 변경

🟢 UserController

비밀번호 변경 API

비밀번호 변경 요청을 전달했을 때, 실패할 경우는 토큰 유효성 실패이므로, 해당 내용을 반환합니다.

PasswordVO

비밀번호 변경을 위한 VO (username, password, token)


🟢 UserService

컨트롤러에서 resetPwWithToken을 호출하면

  • 토큰 유효성 검증
  • 비밀번호 변경
  • 토큰 무효화

를 수행하는 각 단일 메서드를 호출하고, 성공 시 로그를 출력합니다.

  • 내부 비즈니스 로직을 가진 단일 메서드는 private으로 정의해 외부로부터 보호합니다.
  • 사용자가 존재하지 않는 경우, 예외를 발생시켜 NPE 발생 가능성을 줄였습니다.


테스트

변경 성공 (1회만 가능)

변경 실패

  • 1회 변경 후에 동일한 토큰으로 변경 요청 시 변경에 실패하게 됩니다.

변경 Log

0개의 댓글