Spring Security 로그인은 Controller가 직접 성공/실패를 판단하는 구조가 아니라, Authentication -> AuthenticationManager -> AuthenticationProvider -> SecurityContext -> AuthorizationFilter로 책임을 분리하는 구조다.
3줄 요약
MVC 직접 로그인은 Controller가 사용자 조회, 비밀번호 비교, 인증 상태 저장까지 떠안기 쉽다.
Security 방식은 인증 요청을 Authentication으로 표현하고, 검증 책임을 Manager와 Provider로 분리한다.
JWT 방식에서는 서버 세션에 인증 상태를 저장하지 않으므로 매 요청마다 토큰을 검증해 Authentication을 다시 구성해야 한다.
체크포인트
Controller가 로그인 API를 가질 수는 있지만 인증 검증 책임을 독점하면 안 된다는 점을 설명할 수 있는가?
인증 전 Authentication과 인증 후 Authentication의 의미 차이를 구분할 수 있는가?
AuthenticationManager와 AuthenticationProvider의 역할을 분리해서 말할 수 있는가?
인증과 인가의 차이를 SecurityContext와 AuthorizationFilter 흐름으로 설명할 수 있는가?
JWT 로그아웃과 강제 만료가 세션 로그아웃보다 어려운 이유를 말할 수 있는가?
전체 흐름
MVC 직접 로그인의 문제를 확인한다.
username/password를 Authentication 인증 요청 객체로 바꾸는 이유를 이해한다.
AuthenticationManager가 검증을 직접 하지 않고 Provider에게 위임하는 구조를 본다.
인증 성공 후 SecurityContextHolder에 저장되는 사용자 정보를 확인한다.
이후 요청에서 인가가 어떻게 권한과 URL 규칙을 비교하는지 본다.
JWT 방식에서 매 요청 재인증과 별도 무효화 전략이 필요한 이유를 정리한다.
분리 후보
현재 문서는 최종 목표 문장을 한 번에 해체하는 성격이므로 유지한다. 다만 복습성이 떨어지면 MVC 직접 로그인에서 Security 인증 구조로 전환, SecurityContext와 인가 흐름, JWT 재인증과 무효화 전략의 3개 문서로 나눌 수 있다.
결론
결론은 이렇다. 이해되면 1강으로 넘어가면 된다.
Spring Security를 적용한 로그인은Controller가 직접 로그인 성공/실패를 판단하는 기능이 아니다.사용자의 username/password 같은 자격 증명을Authentication이라는 인증 요청 객체로 만들고,AuthenticationManager가 인증 요청을 받아처리 가능한 AuthenticationProvider에게 검증을 위임한다.검증이 성공하면 인증 완료된 Authentication이 만들어지고,이 정보는 SecurityContext에 담겨SecurityContextHolder를 통해 현재 요청에서 접근할 수 있게 된다.그 뒤 AuthorizationFilter가현재 사용자의 권한과 요청 URL의 보안 규칙을 비교해서요청을 허용할지 거부할지 판단한다.JWT 방식에서는 서버 세션에 인증 상태를 저장하지 않으므로,매 요청마다 access token을 검증해서 Authentication을 다시 만들어야 한다.또한 JWT access token은 만료 전까지 계속 유효할 수 있으므로,로그아웃/탈취/강제 만료 대응을 위해refresh token 저장소,access token jti denylist,tokenVersion 같은 별도 무효화 전략이 필요하다.
교재의 최종 이해 목표도 이 흐름이다. 즉, Controller 직접 판단이 아니라 Authentication 중심의 인증 구조, 그리고 JWT에서는 매 요청 재인증과 별도 무효화 전략이 필요하다는 것을 이해하는 게 최종 목표다.
결론에 도달해보자!
전형적인 MVC 기반 로그인의 문제점
처음 MVC 구조를 배우고 로그인 흐름을 설명하면 보통 이렇게 말하게 된다.
1. 사용자가 아이디/비밀번호 입력2. Controller가 요청을 받음3. DB에서 사용자 조회4. 비밀번호가 맞는지 if문으로 비교5. 맞으면 로그인 성공6. 틀리면 로그인 실패
전형적인 직접 로그인 코드
@PostMapping("/login")public ResponseEntity<?> login(@RequestBody LoginRequest request) { User user = userMapper.findByUsername(request.username()); if (user == null) { return ResponseEntity.status(401).build(); } if (!request.password().equals(user.getPassword())) { return ResponseEntity.status(401).build(); } return ResponseEntity.ok("login success");}
이 코드는 이해하기 쉽다.
하지만 Spring Security 관점에서는 Controller가 너무 많은 일을 하고 있다.
Controller가 하는 일:- 로그인 요청 받기- 사용자 DB 조회하기- 비밀번호 직접 비교하기- 로그인 성공/실패 판단하기- 권한 만들기- 인증 상태 저장하기
문제는 단순히 코드가 길다는 것이 아니다.
핵심 문제는 인증이라는 보안 책임이 Controller 안에 흩어진다는 것이다.
Security를 이용하면 책임을 나눌 수 있다
Spring Security를 사용하면 Controller가 인증 검증을 직접 하지 않는다.
Controller는 로그인 요청을 받은 뒤, 아이디/비밀번호를 Spring Security가 이해할 수 있는 객체로 포장한다.
그 객체가 Authentication이다.
아이디/비밀번호를 보냈군.→ Authentication이라는 인증 요청 객체로 만들자.검증은 Controller가 직접 하지 말자.→ AuthenticationManager에게 맡기자.
코드 친화적으로 말하면 Controller는 아래 일만 한다.
- 로그인 요청 받기- username/password를 Authentication으로 포장하기- AuthenticationManager 호출하기- 성공 결과를 바탕으로 응답 만들기
Spring Security 방식 로그인 코드
@PostMapping("/api/auth/login")public TokenResponse login(@RequestBody LoginRequest request) { Authentication input = new UsernamePasswordAuthenticationToken( request.username(), request.password() ); Authentication result = authenticationManager.authenticate(input); return tokenService.issue(result);}
이제 Controller는 아래 일을 하지 않는다.
Controller가 하지 않는 일:- DB password_hash 직접 조회- raw password 직접 비교- 로그인 성공/실패를 if문으로 최종 판단
여기서 중요한 문장은 이것이다.
Controller가 로그인 API를 가질 수는 있다.하지만 인증 검증 책임을 Controller가 독점하면 안 된다.
Authentication은 무엇인가?
Authentication을 단순히 “자격 증명”이라고만 이해하면 조금 부족하다.
더 정확히는 이렇다.
Authentication은인증 전에는 “검증해 달라는 요청 객체”이고,인증 후에는 “검증 완료된 사용자 정보 객체”다.
사용자가 자신을 증명하기 위해 제출하는 정보는 여러 가지가 있다.
상황
제출하는 자격 증명
일반 로그인
username + password
JWT API 요청
Bearer access token
OAuth 로그인
authorization code
API Key 방식
API key
Spring Security는 이런 인증 재료를 Authentication 객체로 표현한다.
일반 로그인에서는 보통 이렇게 만든다.
Authentication input = new UsernamePasswordAuthenticationToken(username, rawPassword);
이 시점의 의미는 이렇다.
이 사용자가 보낸 username/password가 맞는지 검증해 주세요.
즉, 인증 전 Authentication은 검증 요청이다.
그런데 인증이 성공하면 의미가 바뀐다.
Authentication result = authenticationManager.authenticate(input);
이 결과 객체의 의미는 이렇다.
이 사용자는 검증 완료된 사용자입니다.이 사용자는 ROLE_USER 권한을 가집니다.
정리하면 아래와 같다.
시점
Authentication의 의미
예시
인증 전
검증 요청 객체
username/password가 맞는지 확인해 주세요
인증 후
인증 완료 사용자 객체
이 사용자는 joseph이고 ROLE_USER 권한을 가집니다
Authentication 내부에는 대략 이런 정보가 들어간다.
Authentication- principal: 사용자 식별 정보- credentials: 비밀번호, 토큰 같은 자격 증명- authorities: 권한 목록- authenticated: 인증 완료 여부
따라서 Authentication = 자격 증명이라고만 외우면 부족하다.
더 정확히는:
Authentication은 자격 증명을 포함해서현재 인증 요청 또는 인증 완료 사용자를 표현하는 Spring Security의 중심 객체다.
인증 요청 접수 담당자와 실제 검증 담당자는 다르다
앞에서 Controller가 이 코드를 호출했다.
Authentication result = authenticationManager.authenticate(input);
여기서 AuthenticationManager는 인증 요청을 받는 진입점이다.
하지만 실제 검증 로직을 전부 혼자 처리한다기보다는, 처리 가능한 AuthenticationProvider에게 위임한다.
AuthenticationManager= 인증 요청을 받는 진입점AuthenticationProvider= 특정 인증 방식을 실제로 검증하는 담당자
비유하면 병원 접수와 비슷하다.
AuthenticationManager:"인증 요청이 왔네.이건 username/password 방식이군.이 방식은 DaoAuthenticationProvider가 처리할 수 있겠다.그쪽에 맡기자."AuthenticationProvider:"DB에서 사용자를 찾고,PasswordEncoder로 비밀번호를 비교하고,권한을 구성해서 인증 결과를 만들자."
username/password 로그인이라면 대략 이런 흐름이다.
AuthenticationManager→ ProviderManager→ DaoAuthenticationProvider→ UserDetailsService→ MyBatis Mapper→ users 테이블 조회→ PasswordEncoder.matches()→ 인증 성공 Authentication 반환
핵심은 이것이다.
Controller가 직접 검증하지 않는다.AuthenticationManager가 요청을 받고,AuthenticationProvider가 실제 검증을 담당한다.
인증과 인가는 다르다
로그인에 성공했다고 모든 요청이 허용되는 것은 아니다.
예를 들어 어떤 사용자가 로그인했다고 해서 관리자 페이지에 접근할 수 있는 것은 아니다.
인증 Authentication:너 누구야?인가 Authorization:너 이 요청 해도 돼?
JWT 방식= 서버 세션에서 인증 상태를 복원하지 않는다.= 매 요청마다 토큰을 검증해서 Authentication을 다시 만든다.
여기서 중요한 오해가 있다.
JWT stateless= SecurityContextHolder를 안 쓴다는 뜻이 아니다.JWT stateless= 요청과 요청 사이에 서버 세션으로 인증 상태를 저장하지 않는다는 뜻이다.
즉, JWT 방식에서도 현재 요청을 처리하는 동안에는 SecurityContextHolder를 사용한다.
다만 다음 요청에서는 다시 JWT를 검증해서 Authentication을 새로 만든다.
JWT 로그아웃은 세션 로그아웃보다 어렵다
세션 방식의 로그아웃은 비교적 단순하다.
로그아웃→ 서버 세션 삭제→ session id가 다시 와도 인증 정보 없음
하지만 JWT access token은 이미 클라이언트에게 발급된 문자열이다.
이 토큰은 보통 그 자체 안에 이런 정보를 가진다.
- 누가 발급했는지- 누구에게 발급됐는지- 언제 발급됐는지- 언제 만료되는지- 토큰 고유 ID가 무엇인지- 서명이 유효한지
그래서 access token이 아래 조건을 만족하면 계속 사용될 수 있다.
JWT access token의 특징:- 서명이 맞고- 만료 시간이 지나지 않았고- 서버가 별도 차단 검사를 하지 않으면- 토큰을 가진 사람은 계속 사용할 수 있다.
즉, 사용자가 로그아웃하면서 브라우저에서 토큰을 삭제해도, 누군가 그 토큰을 이미 복사해두었다면 만료 전까지 사용할 수 있다.
그래서 JWT에서는 별도 전략이 필요하다.
전략
의도
짧은 access token 만료
탈취된 access token의 사용 가능 시간을 줄임
refresh token DB 저장
refresh token을 서버에서 폐기 가능하게 만듦
refresh token rotation
재발급 때마다 refresh token을 교체해 탈취 재사용을 탐지
access token jti denylist
특정 access token ID를 차단 목록에 등록
tokenVersion
사용자 단위로 기존 토큰 전체를 무효화
여기서 JWT의 장단점이 같이 드러난다.
JWT의 장점:- 서버 세션 저장소에 덜 의존할 수 있다.- 매 요청을 토큰 검증 중심으로 처리할 수 있다.- 서버 확장에 유리한 구조를 만들 수 있다.JWT의 단점:- 이미 발급된 access token을 즉시 무효화하기 어렵다.- 로그아웃/탈취/강제 만료를 제대로 하려면 서버 쪽 상태가 다시 필요하다.
결국 핵심은 이것이다.
JWT는 stateless하다는 장점이 있지만,보안상 필요한 순간에는 일부 state를 다시 서버에 도입해야 한다.
0강 전체 흐름 요약
로그인 요청 흐름은 이렇게 정리할 수 있다.
[로그인 요청]username/password 제출 ↓Controller ↓UsernamePasswordAuthenticationToken 생성 ↓AuthenticationManager.authenticate() ↓AuthenticationProvider가 실제 검증 ↓UserDetailsService + MyBatis + PasswordEncoder ↓인증 성공 Authentication 반환 ↓JWT 발급 또는 세션 저장
JWT 보호 API 요청 흐름은 이렇게 정리할 수 있다.
[보호 API 요청]Authorization: Bearer access-token ↓Spring Security FilterChain ↓JWT 검증 ↓Authentication 재구성 ↓SecurityContextHolder에 저장 ↓AuthorizationFilter 권한 판단 ↓Controller 도착
JWT 로그아웃/무효화 흐름은 이렇게 생각해야 한다.
[JWT 로그아웃]클라이언트 토큰 삭제 +refresh token 폐기 +필요하면 access token jti denylist 등록 +필요하면 tokenVersion 증가
자주 하는 오해와 교정
오해
교정
Controller에 login API가 있으면 Spring Security 방식이 아니다
login API는 Controller에 있어도 된다. 다만 검증을 직접 하지 않고 AuthenticationManager에 위임해야 한다
Authentication은 그냥 자격 증명이다
Authentication은 인증 전에는 요청 객체, 인증 후에는 인증 완료 사용자 객체다
AuthenticationManager가 모든 검증을 직접 한다
대표 구현체인 ProviderManager가 처리 가능한 AuthenticationProvider에게 검증을 위임한다
이미 발급된 access token은 만료 전까지 살아 있을 수 있어서 별도 무효화 전략이 필요하다
클라이언트에서 토큰 삭제하면 서버 로그아웃이 끝난다
탈취된 복사본이 있으면 계속 사용될 수 있으므로 서버 측 폐기 전략이 필요하다
핵심 정리
Spring Security 로그인은Controller가 비밀번호를 직접 비교해서 성공/실패를 판단하는 코드가 아니다.Controller는 username/password를 Authentication으로 만들고,AuthenticationManager에게 검증을 위임한다.AuthenticationManager는 처리 가능한 AuthenticationProvider에게 검증을 맡긴다.검증이 성공하면 인증 완료된 Authentication이 만들어지고,이 정보는 SecurityContext에 담겨SecurityContextHolder를 통해 현재 요청에서 접근된다.그 뒤 AuthorizationFilter가현재 Authentication의 authorities와 요청 보안 규칙을 비교해접근 허용 여부를 판단한다.JWT 방식에서는 서버 세션에서 인증 상태를 복원하지 않으므로,매 요청마다 access token을 검증해 Authentication을 다시 만든다.그리고 JWT access token은 만료 전까지 계속 유효할 수 있으므로,로그아웃/탈취/강제 만료를 위해refresh token 저장소,jti denylist,tokenVersion 같은 별도 무효화 전략이 필요하다.
확인 질문
아래 질문에 답할 수 있으면 0강은 넘어가면 된다.
1. “Controller가 로그인 성공/실패를 직접 판단하지 않는다”는 말은 Controller에 login API를 만들면 안 된다는 뜻인가?2. Authentication은 인증 전과 인증 후에 각각 어떤 의미인가?3. AuthenticationManager와 AuthenticationProvider는 각각 어떤 역할인가?4. 인증과 인가는 어떻게 다른가?5. 인증 성공 결과는 SpringContext에 저장되는가, SecurityContext에 저장되는가?6. JWT 방식에서 stateless라는 말은 SecurityContextHolder를 아예 안 쓴다는 뜻인가?7. JWT 로그아웃에서 클라이언트 토큰 삭제만으로 부족한 이유는 무엇인가?