이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- AuthenticationProvider와 Authentication 객체는 무엇을 검증하고 무엇을 보장하지 않는가?
- SecurityContext는 요청, 비동기, batch, message 경계에서 어떻게 오염되거나 끊길 수 있는가?
- principal에 담아도 되는 정보와 매번 서버에서 다시 조회해야 하는 정보는 어떻게 구분하는가?
개요
AuthenticationProvider는 credential을 검증하고 Authentication 객체를 만든다. 예를 들어 username/password, OAuth2/OIDC, JWT, API key 같은 입력을 확인해 “이 요청의 주체가 누구인지”를 Spring Security에 알려 준다.
SecurityContext는 현재 실행 흐름에서 Authentication을 보관한다. Servlet 요청에서는 필터가 context를 만들고 정리하지만, 비동기 실행, batch, message listener, 직접 Thread 생성에서는 context 전파와 초기화를 따로 봐야 한다.
Authentication이 있다고 해서 최신 권한, 계정 상태, resource ownership이 모두 보장되는 것은 아니다. principal은 신원 확인 결과이며, 민감 action은 서버 DB나 policy service에서 최신 상태를 다시 확인해야 한다.
이 문서의 핵심은 “로그인된 사용자”와 “이 action을 지금 해도 되는 사용자”를 구분하는 것이다.
공격 시나리오
- 공격자는 오래된 session이나 token에 남은 role을 이용해 권한 변경 뒤에도 접근을 시도한다.
- 비동기 작업에서 SecurityContext가 누락되어 anonymous로 처리되거나, 반대로 이전 요청 context가 섞인다.
- principal에 들어 있는
tenantId,role,emailVerified를 최신 상태로 다시 확인하지 않는 endpoint를 찾는다. - 직접 SecurityContext를 세팅하는 테스트/관리자 코드가 운영 경로에 남아 인증 우회를 만든다.
- 계정 잠금, 탈퇴, 비밀번호 변경 후 기존 session/token이 계속 허용되는지 확인한다.
- 인증 실패 로그가 계정 존재 여부를 드러내는지 관찰한다.
취약한 요청/응답 예시
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
CustomPrincipal principal = (CustomPrincipal) auth.getPrincipal();
if (principal.role().equals("ADMIN")) {
return adminReportService.exportAll();
}principal.role=ADMIN
member.currentRole=USER
roleChangedAt=2026-07-01T09:00:00Zprincipal의 role이 stale한데 최신 DB 권한을 다시 확인하지 않으면 권한 회수 후에도 관리자 작업이 가능할 수 있다.
주제별 핵심 판단
- Authentication은 인증 결과이지 모든 업무 권한의 원천이 아니다.
- principal에는 최소 식별자와 안정 속성만 넣는다.
- 최신 권한, 계정 잠금, MFA 상태, 약관 동의, 결제 제한은 필요 시 서버에서 다시 조회한다.
- SecurityContext는 thread boundary에서 자동 전파되지 않을 수 있다.
- 직접 SecurityContext를 조작하는 코드는 테스트와 감사가 필요하다.
- 인증 실패 응답은 계정 존재 여부를 노출하지 않는다.
- login success/failure, session fixation, logout, token revocation event를 추적한다.
개선된 요청/응답 예시
public record CustomPrincipal(
Long memberId,
String username
) {}public void exportAdminReport(Authentication authentication) {
Long memberId = ((CustomPrincipal) authentication.getPrincipal()).memberId();
Member member = memberRepository.findActiveById(memberId)
.orElseThrow(DisabledAccountException::new);
authorizationService.requireAdmin(member);
reportService.exportAll(member.id());
}principal은 member id를 전달하고, 최신 계정 상태와 권한은 서버 저장소에서 확인한다.
방어 설계
- AuthenticationProvider별로 credential 검증 항목과 실패 로그를 정의한다.
- principal에는 비밀번호 hash, token, secret, 과도한 개인정보를 넣지 않는다.
- role과 권한이 자주 바뀌는 시스템은 권한 캐시 만료와 강제 refresh 정책을 둔다.
- 계정 잠금, 탈퇴, 비밀번호 변경, MFA 변경 후 기존 session/token 처리 정책을 정한다.
- 비동기 작업은 DelegatingSecurityContext 계열이나 명시적 actor id 전달을 검토한다.
- batch와 message consumer는 요청 SecurityContext가 없으므로 service account와 actor를 명확히 기록한다.
- 테스트에서 SecurityContext를 직접 세팅할 때 실제 인증 provider를 우회한 테스트임을 구분한다.
- logout과 session invalidation이 실제 저장소에서 동작하는지 검증한다.
Spring/HTTP 예시
@EventListener
void onAuthenticationSuccess(AuthenticationSuccessEvent event) {
audit.record("login_succeeded", event.getAuthentication().getName());
}@Async
public void sendSecurityNotice(Long memberId, String eventName) {
Member member = memberRepository.findById(memberId)
.orElseThrow();
notificationService.send(member, eventName);
}비동기 작업에는 SecurityContext 자체를 기대하기보다 필요한 actor/member id를 명시적으로 넘기고 서버에서 다시 조회하는 방식이 더 단순할 수 있다.
운영 로그와 감사 지점
level=INFO event=authentication_succeeded result=allowed principal=member:1004 provider=password sessionIdHash=991e traceId=f3a09210level=WARN event=stale_principal_detected result=blocked principal=member:1004 principalRole=ADMIN currentRole=USER reason=role_revoked traceId=f3a09210- 인증 성공/실패 event에는 provider와 reason을 구분한다.
- raw credential, token, session id 원문은 남기지 않는다.
- role revocation 후 stale session 사용을 탐지한다.
- SecurityContext 직접 세팅 경로는 운영 코드에서 금지하거나 강하게 감사한다.
- 비동기 작업 event에는 actor 전달 방식과 source job을 남긴다.
실전 판단 기준
- principal에 너무 많은 상태를 넣으면 stale data 문제가 커진다.
- JWT claim의 role은 서버 권한 변경을 즉시 반영하지 못할 수 있다.
- SecurityContextHolder는 편리하지만 thread boundary에서 주의가 필요하다.
- 인증 실패 메시지가 세부적이면 계정 enumeration에 쓰일 수 있다.
- 테스트 helper가 실제 인증 provider를 우회하면 통합 테스트가 별도로 필요하다.
- logout이 UI에서만 처리되고 서버 session/token 저장소에 반영되지 않으면 위험하다.
테스트 포인트
- 계정 잠금 후 기존 session/token으로 접근이 막히는지 확인한다.
- 권한 회수 후 principal의 old role로 민감 action이 거부되는지 확인한다.
- 비동기 작업에서 actor가 누락되거나 잘못 전파되지 않는지 확인한다.
- 인증 실패 응답이 계정 존재 여부를 구분하지 않는지 확인한다.
- SecurityContext 직접 조작 코드가 운영 경로에 없는지 검색한다.
- logout 후 protected API 접근이 실패하는지 확인한다.
위험 신호!
- principal에 role, tenant, approval 상태를 모두 넣고 장시간 신뢰한다.
SecurityContextHolder.getContext()결과만으로 민감 action을 허용한다.- 비동기 작업이 누가 실행했는지 기록하지 않는다.
- 인증 실패 메시지가 “없는 계정”과 “비밀번호 틀림”을 구분한다.
- 테스트가 모두 mock Authentication으로만 구성되어 있다.
- 계정 상태 변경 후 기존 session/token 정책이 없다.
확인 질문
확인 질문
- Authentication 객체가 보장하는 것은 무엇인가?
- credential 검증을 통과한 현재 주체의 인증 결과다.
- Authentication이 자동으로 보장하지 않는 것은 무엇인가?
- 최신 권한, 객체 소유권, 계정 상태, 민감 action 승인 여부다.
- SecurityContext를 다룰 때 주의할 경계는 무엇인가?
- 비동기, batch, message, 직접 Thread, 테스트 helper 같은 실행 흐름 경계다.