Spring security

희운·2025년 4월 7일

SpringBoot

목록 보기
8/10

로그인을 하고 나서 토큰을 client 가 받게 되고
예를들어 로그인을 한 상태로 요청을 할때 헤더에 jwt 를 넣어서 요청을 해야한다.
위의 사진처럼 처음에 특정한 경로로 요청이 오면
첫번째로 SecurityAuthentication Filter 가 검증을 일단 진행하고
우리가 JWTFilter 를 강제로 만들어서 필터 검증을 한다.
JWTFilter 에서 토큰에 정보가 정확히 일치하면
JWTFilter 에서 일시적인 세션을 만든다 그것이
SecurityContextHolder Session 이다.
새로운 요청이 들어오면 동일한 아이디라도 또다시 세션을 만들고 요청이 끝나면 사라진다. -> Stateless

  • jwt를 가지고 jwtFilter 를 통과하는순간! SecuriyContextHolder session 을 만든다.
    그럼 그 SecurityHolder session 에서 사용자 이름을 확인할 수 있다.


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);
  }
}

  • jwtFilter 는 로그인한 사용자가 ( 토큰을 헤더에 포함시켜서 보내는 요청) 보내는 요청을 가로채는 필터이다. 이 필터는 토큰을 검증하고 Authentication 객체를 만들어서
    세션에 넣어준다.
    그럼 로그인한 사용자가 요청을 보내면 @AuthenticationPrincipal CustomUserDetails userDetails 을 통해서 세션에 있는 정보를 가져와서 사용가능하다.

정리하면 다음과 같다

클라이언트가 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와의 연결까지, 내부 메커니즘을 알아봐요.


🔍 1. @AuthenticationPrincipal이란?

Spring Security에서 제공하는 어노테이션으로, 현재 인증된 사용자 정보를 메서드 파라미터로 주입받을 수 있도록 도와줍니다.

✔️ 핵심 포인트

  • SecurityContextHolder에 저장된 인증 정보에서 Principal을 꺼내서 파라미터에 주입합니다.
  • 타입에 맞게 자동으로 캐스팅해줍니다 (CustomUserDetails 등).

🧭 2. 전체 흐름 요약

@AuthenticationPrincipal CustomUserDetails userDetails가 동작하는 전체 흐름을 단계별로 설명해볼게요.


✅ (1) JWT 인증 필터에서 인증 정보 저장

보통 우리가 커스터마이징한 JwtFilterJwtAuthenticationFilter에서 다음과 같은 코드가 실행됩니다:

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 객체의 PrincipalCustomUserDetails를 설정.
  • 이 객체를 SecurityContextHolder에 저장!

✅ (2) 클라이언트가 요청을 보내면…

예를 들어 /post로 요청이 오면, Spring MVC가 해당 컨트롤러 메서드를 실행합니다:

@GetMapping("/post")
public String createPost(@AuthenticationPrincipal CustomUserDetails userDetails) {
    return "Post by " + userDetails.getUsername();
}

✅ (3) @AuthenticationPrincipal이 작동하는 순간

Spring MVC는 @AuthenticationPrincipal이 붙은 파라미터를 발견하면, 이를 처리하기 위해 Argument Resolver를 사용합니다.

⚙️ 구체적 동작

  1. AuthenticationPrincipalArgumentResolver가 실행됩니다.
  2. 내부적으로 SecurityContextHolder.getContext().getAuthentication()을 호출해서 현재 요청의 인증 정보를 가져옵니다.
  3. authentication.getPrincipal()을 통해 Principal을 꺼냅니다.
  4. 이 값을 CustomUserDetails로 캐스팅해서 파라미터로 주입합니다.

📌 3. 핵심 구성요소 정리

구성요소설명
SecurityContextHolder현재 스레드의 인증 정보를 저장 (ThreadLocal)
Authentication인증 객체로, Principal, Credentials, Authorities 포함
Principal실제 사용자 정보 객체 (CustomUserDetails)
@AuthenticationPrincipalAuthentication.getPrincipal()을 꺼내서 자동 주입

💡 주의할 점

  • 인증이 안 된 요청이라면 SecurityContextHolder가 비어 있을 수 있어요.
    • 이럴 땐 @AuthenticationPrincipal(required = false)null을 허용할 수 있습니다.
  • 항상 Principal 타입을 우리가 원하는 타입(CustomUserDetails)으로 설정해야 타입 캐스팅 오류가 안 나요.

✅ 결론

정리하자면…

@AuthenticationPrincipalSecurityContextHolder에 저장된 Authentication 객체에서 Principal을 꺼내 컨트롤러의 파라미터로 주입해주는 어노테이션입니다.
Principal은 우리가 JWT 필터에서 설정한 CustomUserDetails이고, Spring Security의 ArgumentResolver가 자동으로 캐스팅해서 주입해줍니다.


🧠 한 문장으로 요약!

@AuthenticationPrincipal은 Spring Security의 SecurityContextHolder에 저장된 인증 객체에서 현재 로그인한 사용자의 정보를 꺼내서, 컨트롤러 파라미터에 주입해주는 도우미입니다.


profile
기록하는 공간

0개의 댓글