Super Kawaii Cute Cat Kaoani
본문 바로가기
{Lecture}/Capstone design

[캡스톤디자인] 회원가입 및 레디스 설정하기

by wonee1 2026. 5. 15.
728x90

Spring Boot + Redis + JWT 기반 인증 시스템 구현기록 

 
 
 
Spring Boot 기반 캡스톤 프로젝트에서 회원가입 / 로그인 / 로그아웃 / 토큰 재발급 기능을 구현했다.
 
단순한 세션 기반 인증 대신 JWT + Redis 조합을 선택했고, 대학생 서비스 특성상 학교 이메일(@ac.kr) 인증을 필수로 적용했다.

 

 

0. 기술 스택

프레임워크Spring Boot, Spring Security
DBJPA / MySQL
캐시Redis
인증JWT (JJWT 라이브러리)

 

1. 회원가입

 

이메일 인증코드 발송 → 인증코드 확인 → 회원가입

 
회원가입 전 반드시 학교 이메일(@ac.kr) 인증을 완료해야 한다. 
 

POST/api/users/email/send인증코드 이메일 발송
POST/api/users/email/verify인증코드 검증
POST/api/users/signup회원가입

 

이메일 인증 구조

// 1. 인증코드 발송
public void sendVerificationCode(EmailSendRequestDTO request) {
    // @ac.kr 도메인 검증
    // SecureRandom으로 6자리 코드 생성
    // Redis에 저장 (email:code:{email}, TTL: 5분)
    // @Async로 비동기 메일 전송
}

// 2. 인증코드 검증
public void verifyCode(EmailVerifyRequestDTO request) {
    // Redis에서 코드 조회 및 비교
    // 일치 시 email:verified:{email} 저장 (TTL: 30분)
    // 인증코드 삭제
}

 

회원가입 로직

public UserResponseDTO signUp(UserRequestDTO request) {
    // Redis에 email:verified:{email} 존재 확인 (인증 완료 여부)
    // 중복 이메일 확인
    // BCrypt로 비밀번호 암호화
    // DB 저장
    // Redis 인증 키 삭제
}

 

Redis 키 설계

email:code:{email}6자리 인증코드5분
email:verified:{email}"true"30분

 

2. 로그인

 

POST /api/users/login

 
 

로직

public LoginResponseDTO login(LoginRequestDTO request) {
    // 이메일로 사용자 조회
    // passwordEncoder.matches()로 비밀번호 검증
    // 실패 시 AUTHENTICATION_FAILED(401) 예외
    // Access Token + Refresh Token 생성
    // Redis에 refresh:token:{userId} 저장 (TTL: 24시간)
    // 토큰 + 사용자 정보 반환
}

 
 
 

3. 로그아웃

로그아웃 시 두 가지 처리를 수행한다.

  1. Redis에서 Refresh Token 즉시 삭제
  2. Access Token을 블랙리스트에 등록 (남은 유효시간만큼 TTL 설정)

 

POST /api/users/logout
Authorization: Bearer {accessToken}

 

로직

public void logout(String accessToken) {
    // Bearer 제거 및 토큰 정규화
    // Access Token 유효성 검증
    // userId 추출
    // Redis에서 refresh:token:{userId} 삭제
    // 남은 유효시간 계산 (만료시간 - 현재시간)
    // Redis에 blacklist:access:{token} 저장 (TTL: 남은 유효시간)
}

 
 
⭐TTL을 토큰 남은 유효시간으로 설정하면, 만료된 토큰은 블랙리스트에서도 자동 삭제되어 Redis 메모리를 불필요하게 점유하지 않는다.
 
 
 

Redis 키 설계

blacklist:access:{token}"logout"토큰 남은 유효시간

 

4. JWT 토큰

 

jwt:
  secret: ${JWT_SECRET}             # Base64 인코딩 비밀키
  access-token-expire-ms: 3600000   # 1시간
  refresh-token-expire-ms: 86400000 # 24시간

 

클레임 구조

- memberId : Long
- email    : String
- type     : "access" | "refresh"
- iat      : 발급 시간
- exp      : 만료 시간

 
type 클레임을 포함한 이유는 Access Token으로 Refresh 엔드포인트를 호출하거나, Refresh Token으로 일반 API를 직접 호출하는 것을 방지하기 위함이다.

 

 

JwtProvider 주요 메서드

createAccessToken(Long memberId, String email)   // Access Token 생성
createRefreshToken(Long memberId, String email)  // Refresh Token 생성
validateAccessToken(String token)                // 유효성 + type 확인
validateRefreshToken(String token)               // 유효성 + type 확인
getMemberId(String token)                        // userId 추출
getExpiration(String token)                      // 만료 시간 반환

 

JwtAuthenticationFilter

 
모든 요청에서 JWT를 검증하는 필터로, UsernamePasswordAuthenticationFilter 이전에 실행된다.

protected void doFilterInternal(...) {
    // Authorization 헤더에서 Bearer 토큰 추출
    // Redis 블랙리스트 확인 (blacklist:access:{token})
    // 토큰 유효성 검증
    // 유효하면 SecurityContext에 AuthPrincipal(memberId, email) 등록
}

 
 
 
 

5. 토큰 재발급 (Token Rotation)

POST /api/users/refresh
Request:  { "refreshToken": "..." }
Response: { "accessToken": "...", "refreshToken": "...", ... }

 

원자적 Token Rotation (Lua 스크립트)

 
토큰 재발급 시 Race Condition 문제를 방지하기 위해 Redis Lua 스크립트로 원자적으로 처리했다.

if redis.call('get', KEYS[1]) == ARGV[1] then
    redis.call('set', KEYS[1], ARGV[2], 'PX', ARGV[3])
    return 1
else
    return 0
end
  1. Refresh Token 유효성 검증
  2. Redis에 저장된 토큰과 일치 확인 + 새 토큰으로 교체를 원자적으로 처리
  3. 일치하지 않으면 재발급 거부 (TOKEN_INVALID)
  4. 새 Access Token + Refresh Token 반환

Token Rotation을 적용한 이유는 Refresh Token이 탈취되더라도, 정상 사용자가 먼저 재발급받으면 기존 토큰이 무효화되므로 공격자가 사용할 수 없기 때문이다.
 

// 세션 사용 안 함 (JWT 기반 Stateless)
.sessionManagement(session -> session
    .sessionCreationPolicy(SessionCreationPolicy.STATELESS))

// 공개 엔드포인트
.authorizeHttpRequests(auth -> auth
    .requestMatchers(
        "/api/users/signup",
        "/api/users/email/**",
        "/api/users/login",
        "/api/users/refresh"
    ).permitAll()
    .anyRequest().authenticated())

// JWT 필터 등록
.addFilterBefore(jwtAuthenticationFilter,
    UsernamePasswordAuthenticationFilter.class)



7. 예외 처리

USER_NOT_FOUND404사용자 없음
USER_ALREADY_EXISTS409이미 존재하는 이메일
AUTHENTICATION_FAILED401이메일/비밀번호 불일치
USER_EMAIL_NOT_VERIFIED400이메일 인증 미완료
EMAIL_CODE_INVALID400인증코드 오류/만료
EMAIL_DOMAIN_INVALID400@ac.kr 아님
TOKEN_INVALID401유효하지 않거나 만료된 토큰
UNAUTHORIZED_USER403접근 권한 없음

 

 

 

전체 Redis 키 

email:code:{email}이메일 인증코드5분
email:verified:{email}인증 완료 상태30분
refresh:token:{userId}Refresh Token24시간
blacklist:access:{token}로그아웃된 Access Token토큰 남은 유효시간

 


 

⭐마무리

 
구현하면서 신경 쓴 포인트 세 가지를 정리하면 다음과 같다.
1. 블랙리스트 TTL 최적화 만료된 토큰은 어차피 검증 단계에서 걸러지므로, Redis에는 남은 유효시간만큼만 보관하도록 TTL을 설정했다. 불필요한 메모리 점유를 방지할 수 있다.
2. Token Rotation 원자성 보장 Lua 스크립트를 활용해 조회 + 교체를 하나의 트랜잭션처럼 처리했다. 동시에 여러 재발급 요청이 들어오는 Race Condition 상황을 방어할 수 있다.
3. 학교 이메일 검증 @ac.kr 도메인 필수 적용으로 서비스 대상을 재학생으로 한정했다. 별도 학교 API 연동 없이 도메인 검증만으로 간단하게 구현했다.
 

728x90