
로그인을 하고 나서 토큰을 client 가 받게 되고
예를들어 로그인을 한 상태로 요청을 할때 헤더에 jwt 를 넣어서 요청을 해야한다.
위의 사진처럼 처음에 특정한 경로로 요청이 오면
첫번째로 SecurityAuthentication Filter 가 검증을 일단 진행하고
우리가 JWTFilter 를 강제로 만들어서 필터 검증을 한다.
JWTFilter 에서 토큰에 정보가 정확히 일치하면
JWTFilter 에서 일시적인 세션을 만든다 그것이
SecurityContextHolder Session 이다.
새로운 요청이 들어오면 동일한 아이디라도 또다시 세션을 만들고 요청이 끝나면 사라진다. -> Stateless
package com.example.securitytestback.global.filter;
import com.example.securitytestback.domain.User;
import com.example.securitytestback.global.util.JwtUtil;
import com.example.securitytestback.login.dto.CustomUserDetails;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
@RequiredArgsConstructor
@Slf4j
public class JwtFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 인증 생략 경로
if (request.getRequestURI().equals("/api/auth/login")) {
filterChain.doFilter(request, response);
return;
}
// request에서 Authorization 헤더를 찾음
String authorization = request.getHeader("Authorization");
// Authorization 헤더 검증
if (authorization == null || !authorization.startsWith("Bearer ")) {
log.error("토큰이 존재하지 않습니다.");
filterChain.doFilter(request, response);
return;
}
log.info("토큰이 존재합니다");
// Bearer 부분 제거 후 순수 토큰만 획득
String token = authorization.split(" ")[1];
//토큰 소멸 시간 검증
if (jwtUtil.isExpired(token)) {
System.out.println("token expired");
filterChain.doFilter(request, response);
//조건이 해당되면 메소드 종료 (필수)
return;
}
//토큰에서 username과 role 획득
String username = jwtUtil.getUsername(token);
//user를 생성하여 값 set
User user = User.builder()
.username(username)
.build();
//UserDetails에 회원 정보 객체 담기
CustomUserDetails customUserDetails = new CustomUserDetails(user);
//스프링 시큐리티 인증 토큰 생성
Authentication authToken = new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities());
//세션에 사용자 등록
SecurityContextHolder.getContext().setAuthentication(authToken);
filterChain.doFilter(request, response);
}
}
정리하면 다음과 같다
클라이언트가 Authorization: Bearer 헤더를 보냄.
Spring Security의 필터(예: JwtFilter)가 요청을 가로채고, JWT를 검증.
검증 성공 시 Authentication 객체(예: UsernamePasswordAuthenticationToken)를 생성하고, Principal로 CustomUserDetails를 주입
이 Authentication 객체를 SecurityContextHolder.getContext().setAuthentication()로 저장.
컨트롤러 호출 시 @AuthenticationPrincipal이 SecurityContextHolder에서 Principal을 꺼냄.
요청이 끝나면 SecurityContextHolder가 자동으로 클리어됨(스레드 로컬이므로 다음 요청과 무관).
좋아요! 블로그용으로 깔끔하게 정리해드릴게요. 이해를 바탕으로 개발자들이 보기 좋도록 구성해봤어요. 문장도 조금 더 다듬어서 자연스럽고 읽기 좋게 구성했어요.
@AuthenticationPrincipal 완벽 정리: 어떻게 인증된 사용자 정보가 주입될까?Spring Security를 사용하다 보면 컨트롤러 메서드에서 이런 코드 자주 보셨을 거예요:
@GetMapping("/post")
public String createPost(@AuthenticationPrincipal CustomUserDetails userDetails) {
String username = userDetails.getUsername();
return "Post by " + username;
}
근데 문득 이런 의문이 들 수 있죠.
❓ 이
userDetails는 어디서, 어떻게 주입되는 걸까?
오늘은 이 과정을 단계별로 완전히 이해할 수 있도록 풀어보겠습니다. @AuthenticationPrincipal의 동작 원리부터 SecurityContextHolder와의 연결까지, 내부 메커니즘을 알아봐요.
@AuthenticationPrincipal이란?Spring Security에서 제공하는 어노테이션으로, 현재 인증된 사용자 정보를 메서드 파라미터로 주입받을 수 있도록 도와줍니다.
SecurityContextHolder에 저장된 인증 정보에서 Principal을 꺼내서 파라미터에 주입합니다.CustomUserDetails 등).@AuthenticationPrincipal CustomUserDetails userDetails가 동작하는 전체 흐름을 단계별로 설명해볼게요.
보통 우리가 커스터마이징한 JwtFilter나 JwtAuthenticationFilter에서 다음과 같은 코드가 실행됩니다:
String username = jwtUtil.getUsername(token);
User user = User.builder().username(username).build();
CustomUserDetails customUserDetails = new CustomUserDetails(user);
Authentication authToken = new UsernamePasswordAuthenticationToken(
customUserDetails, null, customUserDetails.getAuthorities()
);
SecurityContextHolder.getContext().setAuthentication(authToken);
여기서 중요한 건:
Authentication 객체의 Principal로 CustomUserDetails를 설정.SecurityContextHolder에 저장!예를 들어 /post로 요청이 오면, Spring MVC가 해당 컨트롤러 메서드를 실행합니다:
@GetMapping("/post")
public String createPost(@AuthenticationPrincipal CustomUserDetails userDetails) {
return "Post by " + userDetails.getUsername();
}
@AuthenticationPrincipal이 작동하는 순간Spring MVC는 @AuthenticationPrincipal이 붙은 파라미터를 발견하면, 이를 처리하기 위해 Argument Resolver를 사용합니다.
AuthenticationPrincipalArgumentResolver가 실행됩니다.SecurityContextHolder.getContext().getAuthentication()을 호출해서 현재 요청의 인증 정보를 가져옵니다.authentication.getPrincipal()을 통해 Principal을 꺼냅니다.CustomUserDetails로 캐스팅해서 파라미터로 주입합니다.| 구성요소 | 설명 |
|---|---|
SecurityContextHolder | 현재 스레드의 인증 정보를 저장 (ThreadLocal) |
Authentication | 인증 객체로, Principal, Credentials, Authorities 포함 |
Principal | 실제 사용자 정보 객체 (CustomUserDetails) |
@AuthenticationPrincipal | Authentication.getPrincipal()을 꺼내서 자동 주입 |
SecurityContextHolder가 비어 있을 수 있어요.@AuthenticationPrincipal(required = false)로 null을 허용할 수 있습니다.Principal 타입을 우리가 원하는 타입(CustomUserDetails)으로 설정해야 타입 캐스팅 오류가 안 나요.정리하자면…
✅
@AuthenticationPrincipal은SecurityContextHolder에 저장된Authentication객체에서Principal을 꺼내 컨트롤러의 파라미터로 주입해주는 어노테이션입니다.
이Principal은 우리가 JWT 필터에서 설정한CustomUserDetails이고, Spring Security의ArgumentResolver가 자동으로 캐스팅해서 주입해줍니다.
@AuthenticationPrincipal은 Spring Security의SecurityContextHolder에 저장된 인증 객체에서 현재 로그인한 사용자의 정보를 꺼내서, 컨트롤러 파라미터에 주입해주는 도우미입니다.