f-lab-edu / f-lab-edu/Personal-Interview
로그인 기능 설계
- Dominant language
- Java
- Stars
- 0
- Forks
- 0
- PR merge metrics
- No merged PRs in 30d
Description
# 로그인 기능 구현
## 개요
사용자가 이메일과 비밀번호를 사용하여 시스템에 로그인하고, 인증 토큰(JWT)을 발급받을 수 있는 기능을 구현합니다.
## 기능 요구사항
1. **로그인 API**
- **Endpoint**: `POST /api/auth/login`
- **Request**: `email`, `password`
- **Response**: `accessToken`, `refreshToken` (성공 시), 에러 메시지 (실패 시)
- **검증**:
- 이메일 존재 여부 확인
- 비밀번호 일치 여부 확인 (BCrypt)
- **보안**:
- 로그인 성공 시 JWT(Access/Refresh) 토큰 발급
- 토큰은 Header 또는 Body를 통해 클라이언트에 전달
2. **인증 처리 (Authentication)**
- API 요청 시 Authorization Header의 Bearer Token을 검증하여 사용자 인증 처리
- **Access Token 만료 시**:
- 401 Unauthorized 등 명확한 에러 응답 반환 (프론트엔드에서 구분 가능하도록)
- `ExpiredJwtException` 예외 처리
3. **토큰 재발급 (Refresh)**
- **Endpoint**: `POST /api/auth/refresh`
- **Request**: `refreshToken` (Body)
- **Response**: `accessToken`, `refreshToken` (성공 시, RTR 적용)
- **로직**:
- 요청받은 **Refresh Token(JWT)**의 서명을 검증하고 Claims에서 `userId`를 추출합니다.
- `userId`로 DB에 저장된 Refresh Token을 조회하여 요청받은 토큰과 일치하는지 확인합니다.
- 일치하고 유효기간이 남아있다면 새로운 Access/Refresh Token을 발급하고 DB를 업데이트합니다. (RTR)
- **에러**:
- Refresh Token 만료/위변조 시: 401 Unauthorized (재로그인 필요)
## 기술적 의사결정 및 근거
### 1. 인증 방식: Stateless + Stateful (Hybrid)
- **전략**:
- **Access Token (Stateless)**: JWT 자체에 만료 시간과 권한 정보를 포함하여 서버의 상태 조회 없이 검증합니다. 이를 통해 확장성(Scale-out)과 성능을 보장합니다.
- **Refresh Token (Stateful)**: DB에 저장하여 관리합니다. 이는 "완전한 Stateless"라는 개념과는 거리가 있지만, **보안과 제어권의 균형**을 위함입니다.
- **이유**: Access Token이 탈취되었을 때 유일한 제어 수단은 "만료"뿐입니다. 따라서 Access Token의 수명을 짧게(30분) 가져가고, 대신 Refresh Token을 통해 제어권(강제 로그아웃, 탈취 감지 등)을 서버가 가집니다.
### 2. Refresh Token 저장 및 보안 전략
- **선정: RDB (MySQL)** (초기 단계)
- **이유**:
- 현재 인프라를 고려해 MySQL을 사용합니다.
- 추후 트래픽 증가 시 Redis로 마이그레이션하여 성능을 최적화할 계획입니다.
- **구현**: `UserRefreshToken` 엔티티를 생성하여 관리.
- **Hashing 저장**:
- DB에 Refresh Token 원본을 저장하지 않고, **SHA-256** 등 단방향 해시 함수로 해싱하여 저장합니다.
- **이유**: DB가 탈취되더라도 공격자가 유효한 Refresh Token을 얻을 수 없도록 하여, 오프라인 공격을 방지합니다.
- **RTR (Refresh Token Rotation)**:
- Refresh Token 사용 시마다 새로운 Refresh Token으로 교체합니다.
### 3. 에러 핸들링 및 보안 정책
- **로그인 실패**:
- "이메일이 존재하지 않습니다" 또는 "비밀번호가 틀렸습니다"와 같이 구체적인 에러를 반환하지 않고, **"아이디 또는 비밀번호가 올바르지 않습니다"**라는 일반적인 메시지로 통일합니다. (계정 존재 여부 열거 공격 방지)
- **토큰 검증 실패**:
- Access Token 만료: `401 Unauthorized` + `EXPIRED_TOKEN` 코드
- 위변조/형식 오류: `401 Unauthorized` + `INVALID_TOKEN` 코드
- **권한 부족**: `403 Forbidden` + `ACCESS_DENIED` 코드
- **refresh token 재사용 감지 및 대응**:
- 이미 사용된(Reused) 또는 **만료된(Expired)** Refresh Token으로 재발급 요청이 들어오면, **토큰 탈취 시도**로 간주합니다.
- 해당 `userId`로 저장된 **모든 Refresh Token을 즉시 삭제(무효화)**하여 모든 기기에서 강제 로그아웃 처리합니다.
### 4. 토큰 전달 방식 (확정)
- **Access Token**: Authorization Header (`Bearer `)
- 표준적인 규격(RFC 6750)을 따르며, 프론트엔드/모바일 클라이언트에서 처리가 용이합니다.
- **Refresh Token**: Response Body (JSON)
- 클라이언트가 토큰을 받아 저장소(Secure Storage 등)에 관리하기 위함입니다.
- *참고: 웹 환경 전용일 경우 HttpOnly Cookie가 보안상 유리하지만, 모바일 앱 등 다양한 클라이언트 지원을 위해 Body 전달 방식을 채택합니다.*
## 기술적 요구사항
- **Spring Security** 설정 변경: 로그인 경로(`api/auth/login`) 허용, CSRF 비활성화, Session Creation Policy Stateless 설정
- **JWT**: `io.jsonwebtoken` 라이브러리 활용 (build.gradle에 추가 필요)
- **비밀번호 암호화**: 기존 `BCryptPasswordEncoder` 사용
## 엔티티 설계
### UserRefreshToken
- **개요**: 사용자의 Refresh Token을 저장하여 RTR(Refresh Token Rotation)을 구현하고, 토큰 탈취 시 대응할 수 있도록 합니다.
- **테이블**: `user_refresh_tokens`
- **필드**:
- `id` (PK): `BIGINT`, Auto Increment
- `userId` (FK): `BIGINT` (User 엔티티 참조가 아닌 논리적 참조 권장)
- `refreshToken`: `VARCHAR(255)`
- `createdAt`: `DATETIME` (발급 일시)
- `expiryAt`: `DATETIME` (만료 일시)
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.