feat: 무차별 대입 방지(로그인·비밀번호 게이트) + 미처리 예외 추적 강화
- 로그인/비밀번호 재인증에 실패 기반 레이트리밋(Redis 슬라이딩 윈도우) · 로그인 IP당 10회/10분, 비밀번호 게이트 회원당 5회/10분, 초과 시 429 · 성공 시 실패 카운터 초기화, Redis 장애 시 통과(가용성 우선) · 회원가입 레이트리밋도 공통 enforceRateLimit 으로 일원화 - GlobalExceptionHandler: 미처리 500 로그에 메서드+경로 기록(추적성), 잘못된 JSON은 400 - 테스트: 레이트리밋 429 케이스 2개 추가 (백엔드 총 34) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
4a85902183
commit
a9ff2387c8
@@ -78,6 +78,8 @@ class AuthServiceTest {
|
||||
void verifyPassword_wrong() {
|
||||
when(memberMapper.findById(1L)).thenReturn(localMember());
|
||||
when(passwordEncoder.matches("bad", "hash")).thenReturn(false);
|
||||
when(redisTemplate.opsForValue()).thenReturn(valueOps); // 실패 카운트(레이트리밋)
|
||||
when(valueOps.increment(anyString())).thenReturn(1L);
|
||||
|
||||
assertThatThrownBy(() -> service.verifyPassword(1L, "bad"))
|
||||
.isInstanceOf(ApiException.class)
|
||||
@@ -204,12 +206,14 @@ class AuthServiceTest {
|
||||
void login_wrongPassword() {
|
||||
when(memberMapper.findByLoginId("u1")).thenReturn(localMember());
|
||||
when(passwordEncoder.matches("bad", "hash")).thenReturn(false);
|
||||
when(redisTemplate.opsForValue()).thenReturn(valueOps); // 실패 카운트(레이트리밋)
|
||||
when(valueOps.increment(anyString())).thenReturn(1L);
|
||||
|
||||
LoginRequest req = new LoginRequest();
|
||||
req.setLoginId("u1");
|
||||
req.setPassword("bad");
|
||||
|
||||
assertThatThrownBy(() -> service.login(req))
|
||||
assertThatThrownBy(() -> service.login(req, "1.2.3.4"))
|
||||
.isInstanceOf(ApiException.class)
|
||||
.extracting("status").isEqualTo(HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
@@ -226,13 +230,43 @@ class AuthServiceTest {
|
||||
req.setPassword("pw");
|
||||
req.setRememberMe(true);
|
||||
|
||||
LoginResponse res = service.login(req);
|
||||
LoginResponse res = service.login(req, "1.2.3.4");
|
||||
|
||||
assertThat(res.getToken()).isNotBlank();
|
||||
verify(valueOps).set(anyString(), any(), any()); // Redis 저장
|
||||
verify(authSessionMapper).insert(any(AuthSession.class)); // DB 백업
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("login: 실패 누적이 한도 초과면 429")
|
||||
void login_rateLimited() {
|
||||
when(memberMapper.findByLoginId("u1")).thenReturn(localMember());
|
||||
when(passwordEncoder.matches("bad", "hash")).thenReturn(false);
|
||||
when(redisTemplate.opsForValue()).thenReturn(valueOps);
|
||||
when(valueOps.increment(anyString())).thenReturn(11L); // LOGIN_LIMIT(10) 초과
|
||||
|
||||
LoginRequest req = new LoginRequest();
|
||||
req.setLoginId("u1");
|
||||
req.setPassword("bad");
|
||||
|
||||
assertThatThrownBy(() -> service.login(req, "1.2.3.4"))
|
||||
.isInstanceOf(ApiException.class)
|
||||
.extracting("status").isEqualTo(HttpStatus.TOO_MANY_REQUESTS);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("verifyPassword: 실패 누적이 한도 초과면 429")
|
||||
void verifyPassword_rateLimited() {
|
||||
when(memberMapper.findById(1L)).thenReturn(localMember());
|
||||
when(passwordEncoder.matches("bad", "hash")).thenReturn(false);
|
||||
when(redisTemplate.opsForValue()).thenReturn(valueOps);
|
||||
when(valueOps.increment(anyString())).thenReturn(6L); // VERIFY_PW_LIMIT(5) 초과
|
||||
|
||||
assertThatThrownBy(() -> service.verifyPassword(1L, "bad"))
|
||||
.isInstanceOf(ApiException.class)
|
||||
.extracting("status").isEqualTo(HttpStatus.TOO_MANY_REQUESTS);
|
||||
}
|
||||
|
||||
// ===== getSession (DB 백업 복원) =====
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user