SecurityFilterChain 기동 순서 · JwtAuthenticationFilter 코드 실행 · SecurityContext Thread-Local · @AuthenticationPrincipal 주입 흐름
3차는 2차 기동 순서에서 SecurityConfig의 filterChain() 빈이 추가됩니다. 이 빈이 만들어지는 순서와 무엇이 Heap에 올라가는지 봅니다.
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // [1] Stack: filterChain() 프레임 push (기동 시 1회만) // [2] CSRF 비활성화 — REST API이므로 http.csrf(csrf -> csrf.disable()); // [3] 세션 정책 STATELESS — Session 객체 생성 안 함 http.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); // [4] CORS 설정 객체 생성 → Heap http.cors(cors -> cors.configurationSource(corsConfigurationSource())); // [5] URL 권한 규칙 등록 http.authorizeHttpRequests(auth -> auth .requestMatchers("/api/user/**").permitAll() .requestMatchers("/api/admin/**").hasRole("ADMIN") .anyRequest().authenticated() ); // [6] JwtAuthenticationFilter Bean을 Heap에서 꺼내서 등록 http.addFilterBefore( jwtAuthenticationFilter, // 이미 Heap에 있는 Bean UsernamePasswordAuthenticationFilter.class ); // [7] SecurityFilterChain 객체 생성 → Heap return http.build(); // Heap: SecurityFilterChain 객체 (서버 살아있는 한 유지) // 안에 Filter 순서 목록이 들어있음 }
@Component가 붙어있으므로 Bean 스캔 시 이미 Heap에 생성됨.filterChain()에서 addFilterBefore(jwtAuthenticationFilter, ...)로 등록할 때는CustomUserDetailsService, JwtUtil도 마찬가지 — 기동 시 Heap에 생성, DI로 연결.
SecurityContextHolderFilter (1순위)
CorsFilter (2순위)
JwtAuthenticationFilter (3순위 — addFilterBefore로 삽입)
UsernamePasswordAuthenticationFilter (4순위 — 우리는 미사용)
AuthorizationFilter (마지막 — URL 권한 체크)
모든 요청은 이 순서대로 Filter를 거칩니다.
3차에서 가장 중요한 메모리 개념. request.setAttribute()와 어떻게 다른지 이해하면 Security 전체가 보입니다.
일반 변수는 여러 스레드가 공유할 수 있습니다.
ThreadLocal은 각 스레드마다 독립된 저장공간을 가집니다.
스레드 A가 ThreadLocal.set("value-A") 하면 → 스레드 A만 그 값 접근 가능.
스레드 B는 ThreadLocal.get() 해도 null — 스레드 A 값 못 봄.
SecurityContextHolder가 바로 이 ThreadLocal을 사용합니다.
요청마다 스레드가 다르므로 → 요청마다 완전히 독립된 인증 저장소가 생깁니다.
// SecurityContextHolder 내부 (Spring Security 소스) public class SecurityContextHolder { // 각 스레드마다 독립된 저장소 private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>(); } // 요청 1 스레드 (홍길동 로그인) SecurityContextHolder.getContext().setAuthentication(auth_홍길동); // → 스레드1의 ThreadLocal에 저장 // 요청 2 스레드 (동시에 들어온 김영희 요청) SecurityContextHolder.getContext().setAuthentication(auth_김영희); // → 스레드2의 ThreadLocal에 저장 — 스레드1과 완전히 독립 // 요청 2가 스레드1의 인증에 접근하는 것은 불가능
@Override protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ... { // [1] Stack: doFilterInternal() 프레임 push // [2] 헤더에서 토큰 추출 String authHeader = request.getHeader("Authorization"); // authHeader → Stack 지역변수 if (authHeader == null || !authHeader.startsWith("Bearer ")) { filterChain.doFilter(request, response); // 건너뜀 return; } // [3] "Bearer " 제거 String token = authHeader.substring(7); // [4] 토큰 유효성 검증 — jwtUtil(Heap Bean) 사용 if (jwtUtil.validateToken(token)) { // [5] 토큰에서 username 추출 String username = jwtUtil.getUsername(token); // [6] DB에서 CustomUserDetails 로드 // customUserDetailsService(Heap Bean).loadUserByUsername() UserDetails userDetails = customUserDetailsService.loadUserByUsername(username); // Heap: new CustomUserDetails(user) 생성 // userDetails → Stack 지역변수 (Heap 참조) // [7] Authentication 객체 생성 → Heap UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken( userDetails, // principal (CustomUserDetails) null, // credentials (null — 이미 검증됨) userDetails.getAuthorities() // [ROLE_USER] 또는 [ROLE_ADMIN] ); // auth → Stack 지역변수 (Heap 참조) // [8] SecurityContext에 저장 — Thread-Local에 저장! SecurityContextHolder.getContext().setAuthentication(auth); // 이 스레드의 ThreadLocal에만 저장 // 이후 같은 요청 처리 내 어디서든 꺼낼 수 있음 } // [9] 다음 Filter로 넘김 filterChain.doFilter(request, response); // [10] doFilterInternal() 프레임 pop // token, username, userDetails, auth 지역변수 소멸 }
SecurityContextHolder.getContext().getAuthentication() →
Thread-Local에서 auth 꺼냄 →/api/board/write → .anyRequest().authenticated() 체크 →
auth 있으면 통과 → Controller로.
@PostMapping("/write") public ResponseEntity<?> write( @RequestBody BoardDto boardDto, @AuthenticationPrincipal CustomUserDetails userDetails) { // @AuthenticationPrincipal 처리 순서: // [1] Spring이 메서드 호출 전에 ArgumentResolver 실행 // [2] SecurityContextHolder.getContext().getAuthentication() 호출 // [3] authentication.getPrincipal() → CustomUserDetails 꺼냄 // [4] 파라미터에 자동 주입 // → 개발자가 null 체크, getAttribute() 코드 작성 불필요 User loginUser = userDetails.getUser(); // loginUser → Stack 지역변수 (Heap의 User 참조) boardService.write(boardDto, loginUser); return ResponseEntity.ok(...); }
SecurityContextHolderFilter가SecurityContextHolder.clearContext() 실행 →
Thread-Local에서 auth 삭제.@Override public Collection<? extends GrantedAuthority> getAuthorities() { // user.getRole() → "ROLE_ADMIN" 또는 "ROLE_USER" return List.of(new SimpleGrantedAuthority(user.getRole())); // Heap: new SimpleGrantedAuthority("ROLE_ADMIN") 생성 } // UsernamePasswordAuthenticationToken 생성 시 이 Authorities 포함 // → Thread-Local에 저장된 auth 안에 권한 정보가 들어있음
[ROLE_ADMIN] 확인 →hasRole("ADMIN") = hasAuthority("ROLE_ADMIN") → 통과.ROLE_USER이면 → 403 Forbidden 즉시 반환 — AdminController 못 들어감.
@Transactional public void deleteUser(Long userId) { // [1] AOP Proxy가 이 메서드 호출을 가로챔 // [2] Proxy: 트랜잭션 시작 (DB 커넥션 획득, BEGIN 실행) // [3] Stack: deleteUser() 프레임 push User user = userRepository.findById(userId) .orElseThrow(() -> new RuntimeException("유저 없음")); // SELECT 쿼리 실행 → Heap에 User 객체 // [4] 게시글 먼저 삭제 (FK 제약) boardRepository.deleteByUser(user); // DELETE FROM board WHERE user_id = ? 쿼리 실행 // 아직 커밋 안 됨 — 트랜잭션 내에서 실행만 됨 // [5] 유저 삭제 userRepository.delete(user); // DELETE FROM users WHERE id = ? 쿼리 실행 // 아직 커밋 안 됨 // [6] Stack: deleteUser() 프레임 정상 종료 // [7] Proxy: 예외 없이 종료 → COMMIT 실행 // → 두 DELETE가 한 번에 DB에 반영 // 만약 deleteByUser()나 delete() 중 예외 발생 시: // [7'] Proxy: ROLLBACK → 두 DELETE 모두 취소 }
| 영역 | 2차 | 3차 변화 |
|---|---|---|
| Heap (추가) | JwtFilter, JwtUtil, Bean들 | SecurityFilterChain, JwtAuthenticationFilter, CustomUserDetailsService, CustomUserDetails 객체들 추가 |
| Thread-Local (신규) | 없음 | SecurityContext — 요청마다 인증 저장, 완료 후 clearContext() |
| Heap (제거) | request.setAttribute로 저장 | request에 저장 불필요 — Thread-Local이 담당 |
| Session | 없음 (STATELESS) | 없음 (STATELESS 명시 — SessionCreationPolicy.STATELESS) |