Spring Security는 Controller 내부 기능이 아니라 Servlet FilterChain 위에서 동작한다.
요청은 Servlet Container, FilterChain, Spring Security FilterChain을 통과한 뒤 DispatcherServlet과 Controller에 도착한다.
Controller breakpoint가 걸리지 않는 401, 403, 302, CORS, CSRF 문제는 먼저 Security FilterChain 통과 여부를 확인해야 한다.
핵심 정리
Servlet Container는 HTTP 요청을 받고 HttpServletRequest, HttpServletResponse를 만든 뒤 FilterChain을 실행한다.
Spring Security는 DelegatingFilterProxy, FilterChainProxy, SecurityFilterChain 구조로 Servlet Filter 영역에 연결된다.
FilterChainProxy는 현재 요청에 맞는 SecurityFilterChain을 선택하고, 그 안의 Security Filter들을 정해진 순서로 실행한다.
여러 SecurityFilterChain이 있어도 현재 요청에 대해 첫 번째로 매칭된 체인만 실행된다. 그래서 체인 matcher 순서는 좁은 범위에서 넓은 범위로 두는 것이 안전하다.
인증 필터는 요청에서 자격 증명을 읽어 Authentication을 만들고, 인가 필터는 현재 인증 정보와 요청 규칙을 비교한다.
chain.doFilter()가 호출되지 않거나 Security 필터에서 응답이 결정되면 Controller까지 도달하지 않는다.
헷갈리는 지점
Security를 Controller 앞의 인터셉터 정도로 생각하기 쉽다. MVC 앱에서 Controller와 함께 설정하기 때문이다.
핵심은 Spring Security의 Servlet 지원이 Filter 기반이라는 점이다.
Controller에 도착하기 전 인증, 인가, CSRF, CORS, 예외 변환이 끝날 수 있다.
401/403이 나오면 Controller 코드를 먼저 의심하기 쉽다. API 응답이 Controller에서 만들어질 것처럼 느껴지기 때문이다.
핵심은 Controller breakpoint가 안 걸리면 이미 앞단 필터에서 멈췄을 가능성이 크다는 점이다.
Security debug 로그와 필터 체인을 먼저 확인한다.
필터 순서를 몰라도 된다고만 생각하기 쉽다. 대부분 자동 설정으로 동작하기 때문이다.
핵심은 커스텀 필터를 추가하거나 문제를 추적할 때 순서가 매우 중요하다는 점이다.
인증은 인가보다 앞서야 하고, 예외 변환 필터의 위치도 응답 처리에 영향을 준다.
확인 질문
Spring Security는 Spring MVC Controller 내부에서 동작하는가?
아니다. Servlet FilterChain 위에서 Controller 이전에 동작한다.
Controller breakpoint가 걸리지 않는 401/403에서 먼저 볼 곳은 어디인가?
Spring Security FilterChain과 각 보안 필터의 처리 결과를 먼저 봐야 한다.
SecurityFilterChain은 무엇을 결정하는가?
현재 요청에 어떤 Spring Security Filter들이 적용될지 결정한다.
Authentication 구조
3줄 요약
Authentication은 인증 요청이자 인증 완료된 현재 사용자 정보를 표현하는 핵심 인터페이스다.
AuthenticationManager는 인증 처리를 정의하고, 대표 구현인 ProviderManager는 여러 AuthenticationProvider에게 검증을 위임한다.
인증 성공 결과는 SecurityContext에 담겨 SecurityContextHolder를 통해 현재 요청에서 참조된다.
핵심 정리
인증 전 Authentication은 사용자가 제출한 자격 증명을 담은 요청 객체다.
인증 후 Authentication은 principal, authorities, 인증 상태를 가진 현재 사용자 정보다.
AuthenticationManager는 인증 요청을 받아 성공한 Authentication을 반환하거나 실패 예외를 던진다.
ProviderManager는 여러 AuthenticationProvider 중 처리 가능한 Provider에게 검증을 맡긴다.
SecurityContextHolder는 기본적으로 ThreadLocal 전략으로 현재 요청 스레드의 SecurityContext를 보관한다.
최신 Servlet Security 흐름에서는 SecurityContext를 읽고 저장하는 책임이 SecurityContextRepository, SecurityContextHolderFilter와 연결된다. 커스텀 인증을 직접 만들면 SecurityContext를 저장해야 하는 흐름까지 확인해야 한다.
헷갈리는 지점
Authentication을 로그인 성공 결과로만 보기 쉽다. SecurityContext에서 현재 사용자로 자주 꺼내기 때문이다.
핵심은 인증 전에는 요청 객체이고 인증 후에는 현재 사용자 표현이라는 점이다.
같은 인터페이스가 두 역할을 하므로 isAuthenticated()와 생성 시점을 함께 봐야 한다.
Controller가 로그인 성공/실패를 직접 판단해야 한다고 생각하기 쉽다. MVC 방식 로그인에 익숙하기 때문이다.
핵심은 Controller가 자격 증명을 포장할 수는 있지만 검증 책임은 Manager와 Provider로 분리하는 것이다.
Controller는 인증 결과를 응답이나 토큰 발급으로 연결하는 위치에 가깝다.
SecurityContext가 전역 변수처럼 안전하게 어디서나 유지된다고 오해하기 쉽다. static 접근 형태가 보이기 때문이다.
핵심은 기본 저장 전략이 요청 스레드 중심이라는 점이다.
비동기, 새 스레드, 배치 작업에서는 context 전파를 별도로 고려해야 한다.
확인 질문
인증 전 Authentication과 인증 후 Authentication의 차이는 무엇인가?
인증 전에는 자격 증명을 담은 요청이고, 인증 후에는 principal과 authorities를 가진 현재 사용자 정보다.
ProviderManager는 무엇을 하는가?
여러 AuthenticationProvider에게 인증 가능 여부와 검증을 위임한다.
SecurityContextHolder에는 무엇이 담기는가?
현재 요청에서 인증된 사용자의 Authentication을 가진 SecurityContext가 담긴다.
Authorization과 권한 규칙
3줄 요약
인증은 “누구인가”를 확인하고, 인가는 “이 요청을 수행할 권한이 있는가”를 판단한다.
AuthorizationFilter는 SecurityContextHolder의 Authentication과 authorizeHttpRequests 규칙을 비교해 접근을 허용하거나 거부한다.
401은 인증 실패 또는 미인증, 403은 인증은 되었지만 권한이 부족한 상태로 나누어 해석한다.
핵심 정리
인증이 성공해야 인가 판단에 사용할 principal과 authority가 생긴다.
GrantedAuthority는 role, scope 같은 높은 수준의 권한 표현이다.
authorizeHttpRequests는 요청 matcher와 접근 규칙을 순서대로 선언한다. 먼저 매칭되는 규칙이 중요하다.
AuthorizationFilter는 기본적으로 Security FilterChain 뒤쪽에서 동작하므로, 직접 추가한 필터가 AuthorizationFilter 앞인지 뒤인지에 따라 인가 대상 여부가 달라질 수 있다.
hasRole("USER")는 일반적으로 ROLE_USER authority와 연결된다. prefix 혼동은 403의 흔한 원인이다.
Method Security는 URL이 아니라 메서드 실행 권한을 제어한다. 웹 인가와 함께 쓰되 책임을 구분해야 한다.
헷갈리는 지점
로그인만 성공하면 모든 API 접근이 가능하다고 생각하기 쉽다. 인증 완료가 보안 완료처럼 느껴지기 때문이다.
핵심은 인증과 인가는 별개의 단계라는 점이다.
인증된 사용자라도 필요한 authority가 없으면 403이 날 수 있다.
hasRole과 hasAuthority를 같은 것으로 보기 쉽다. 둘 다 권한을 검사하기 때문이다.
핵심은 role 표현에는 ROLE_ prefix 규칙이 개입할 수 있다는 점이다.
저장된 authority 문자열과 matcher에서 기대하는 문자열을 맞춰야 한다.
보안 규칙 순서를 대충 둬도 된다고 생각하기 쉽다. matcher가 여러 개 있으면 모두 고려될 것 같기 때문이다.
핵심은 요청 규칙은 순서와 첫 매칭이 중요하다는 점이다.
넓은 규칙을 먼저 두면 좁은 규칙이 도달하지 못할 수 있다.
확인 질문
인증과 인가의 차이는 무엇인가?
인증은 사용자가 누구인지 확인하는 것이고, 인가는 그 사용자가 특정 요청을 수행할 수 있는지 판단하는 것이다.
401과 403은 어떻게 구분하는가?
401은 인증이 없거나 실패한 경우이고, 403은 인증은 되었지만 권한이 부족한 경우다.
authorizeHttpRequests에서 규칙 순서가 중요한 이유는 무엇인가?
요청 matcher가 순서대로 평가되고 먼저 매칭되는 규칙이 적용되기 때문이다.
Session JWT Cookie CSRF
3줄 요약
세션은 서버가 인증 상태를 저장하는 방식이고, JWT는 검증 가능한 토큰 값이며, 쿠키는 값을 브라우저가 자동 운반하는 수단이다.
세션 방식은 Session ID로 서버의 SecurityContext를 복원하고, JWT 방식은 매 요청 토큰 검증으로 Authentication을 다시 만든다.
CSRF는 unsafe HTTP method와 브라우저가 인증 정보를 자동 전송하는 성질과 관련되므로 인증 저장 방식과 클라이언트 운반 방식에 따라 대응이 달라진다.
핵심 정리
쿠키는 브라우저가 값을 저장하고 요청에 자동으로 붙여 보내는 운반 수단이다.
토큰은 사용자를 증명하거나 권한 정보를 담은 문자열 값이다. JWT는 토큰 형식 중 하나다.
세션 로그인은 서버 HttpSession에 SecurityContext를 저장하고, 클라이언트는 보통 JSESSIONID 쿠키를 보낸다.
JWT Resource Server 방식은 Authorization: Bearer 헤더의 토큰을 검증하고 Authentication을 구성한다.
Stateless JWT는 서버 세션 저장 부담을 줄이지만 로그아웃, 강제 만료, 탈취 대응을 위해 refresh token 저장소, denylist, tokenVersion 같은 별도 전략이 필요하다.
헷갈리는 지점
JWT와 쿠키를 같은 선택지로 비교하기 쉽다. 둘 다 클라이언트가 들고 다니는 값처럼 보이기 때문이다.
핵심은 JWT는 값의 형식이고 쿠키는 운반 수단이라는 점이다.
JWT를 Authorization 헤더로 보낼 수도 있고 쿠키에 담아 보낼 수도 있다.
JWT를 쓰면 서버가 아무 상태도 관리하지 않아도 된다고 생각하기 쉽다. stateless라는 설명 때문이다.
핵심은 access token 검증은 stateless할 수 있지만 refresh token, 로그아웃, 탈취 대응은 상태 관리가 필요할 수 있다는 점이다.
만료 전 강제 무효화가 필요하면 서버 측 전략이 필요하다.
CSRF는 JWT를 쓰면 항상 필요 없다고 단정하기 쉽다. 세션 쿠키와 함께 설명되는 경우가 많기 때문이다.
핵심은 브라우저가 인증 정보를 자동 전송하는 방식인지 보는 것이다.
Authorization 헤더를 JS가 직접 붙이는 구조와 쿠키 자동 전송 구조는 위험 모델이 다르다.
CSRF token을 쿠키에 저장하면 쿠키만으로 검증이 끝난다고 생각하기 쉽다. 토큰이 쿠키에 있으니 서버가 알아서 비교할 것처럼 보이기 때문이다.
핵심은 CSRF 방어는 브라우저가 자동 포함하지 않는 요청 위치에 실제 token을 함께 보내 비교하게 하는 것이다.
SPA에서는 cookie에 저장된 token을 JavaScript가 읽어 header로 보내는 구성이 자주 쓰인다.
확인 질문
세션, JWT, 쿠키는 각각 무엇에 가까운가?
세션은 서버 저장 방식, JWT는 토큰 값의 형식, 쿠키는 브라우저의 운반 수단에 가깝다.