주의
이 책은 Spring Security 5 버전을 기준으로 작성되었으므로, Spring Boot 3.X 버전에서는 일부 클래스가 더 이상 사용되지(deprecated) 않을 수 있습니다.
개요
- 인증 논리를 담당하는 것은 AuthenticationProvider 계층이며 여기에서 요청을 허용할지 결정하는 조건과 명령을 발견할 수 있음
- AuthenticationManager는 HTTP 필터 계층에서 요청을 수신하고 해당 책임을 AuthenticationProvider에 위임하는 구성 요소
- 요청을 나타내는 방법을 명확히 이해하려면 Authentication 인터페이스부터 알아야 함
- 그런 다음 인증이 성공한 후 요청 세부 사항이 처리하는 방법을 배울 수 있으며 SecurityContext 인터페이스와 스프링 시큐리티가 이를 관리하는 방법을 논의할 수 있음

1. AuthenticationProvider의 이해
- 스프링 시큐리티에서는 AuthenticationProvider 계약으로 모든 맞춤형 인증 논리를 정의할 수 있음
인증 프로세스 중 요청 나타내기
- Authentication은 인증이라는 이름이 의미하듯이 인증 프로세스의 필수 인터페이스
- Authentication 인터페이스는 인증 요청 이벤트를 나타내며 애플리케이션에 접근을 요청한 엔티티의 세부 정보를 담으며 인증 요청 이벤트와 관련한 정보는 인증 프로세스 도중과 이후에 이용할 수 있음
- 애플리케이션에 접근을 요청하는 사용자를 Principal이라고 지칭하며 자바 시큐리티 API의 Principal 인터페이스와 같은 개념
- 스프링 시큐리티의 Authentication 계약은 주체만 나타내는 것이 아니라 인증 프로세스 완료 여부, 권한의 컬렉션 같은 정보를 추가로 가짐
- 해당 계약이 자바 시큐리티 API의 Principal 계약을 확장하도록 설계됐다는 사실은 다른 프레임워크나 애플리케이션 구현과의 호환성이라는 측면에서 이점이 됨
- 이 유연성 덕분에 다른 방식으로 인증을 구현한 애플리케이션을 스프링 시큐리티로 보다 쉽게 마이그레이션 할 수 있음
- Authentication에서 숙지해야 할 메서드는 다음과 같음
- isAuthenticated(): 인증 프로세스가 끝났으면 true를 반환하고 아직 진행 중이면 false를 반환
- getCredentials(): 인증 프로세스에 이용된 암호나 비밀을 반환
- getAuthorities(): 인증된 요청에 허가된 권한의 컬렉션을 반환
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public interface Authentication extends Principal, Serializable { | |
Collection<? extends GrantedAuthority> getAuthorities(); | |
Object getCredentials(); | |
Object getDetails(); | |
Object getPrincipal(); | |
boolean isAuthenticated(); | |
void setAuthenticated(boolean var1) throws IllegalArgumentException; | |
} |

맞춤형 인증 논리 구현
- 스프링 시큐리티의 AuthenticationProvider는 인증 논리를 처리함
- AuthenticationProvider 인터페이스의 기본 구현은 시스템의 사용자를 찾는 책임을 UserDetailsService에 위임하고 PasswordEncoder로 인증 프로세스에서 암호를 관리함
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public interface AuthenticationProvider { | |
Authentication authenticate(Authentication var1) throws AuthenticationException; | |
boolean supports(Class<?> var1); | |
} |
- AuthenticationProvider 책임은 Authentication 계약과 강하게 결합돼 있음
- authenticate() 메서드는 Authentication 객체를 매개 변수로 받고 Authentication 객체를 반환함
- 인증 논리를 정의하려면 authenticate() 메서드를 구현해야 하는데, authentication 메서드를 구현하는 방법을 다음 세 항목으로 간단하게 요약 가능
- 인증이 실패하면 메서드는 AuthenticationException 예외 발생
- 메서드가 현재 AuthenticationProvider 구현에서 지원되지 않는 인증 객체를 받으면 null을 반환해야 하는데 이렇게 하면 HTTP 필터 수준에서 분리된 여러 Authentication 형식을 사용할 가능성이 생김
- 메서드는 완전히 인증된 객체를 나타내는 Authentication 인스턴스를 반환해야 하고 해당 인스턴스에 대해 isAuthenticated() 메서드는 true를 반환하며, 여기에는 인증된 엔티티의 모든 필수 세부 정보가 포함됨
- AuthenticationProvider 인터페이스의 두 번째 메서드는 supports(Class<?> authentication)
- 해당 메서드는 현재 AuthenticationProvider가 Authentication 객체로 제공된 형식을 지원하면 true를 반환하도록 구현
- 주의: 해당 메서드가 객체에 대해 true를 반환하더라도 authenticate() 메서드가 null을 반환해 요청을 거부할 수 있움
- AuthenticationManager와 AuthenticationProvider가 함께 작동하는 방식은 보안성이 강화된 출입문에 복합 잠금장치를 설치한 것과 유사함
- 해당 잠금장치는 카드나 전통적인 열쇠를 이용해 열 수 있다고 가정
- 여기서 AuthenticationManager는 문을 열지 말지를 결정하는 잠금장치 자체에 해당하며, 이 결정 과정에서 각 인증 수단(카드, 열쇠)을 검증하는 역할을 AuthenticationProvider에게 위임
- i.g. 사용자가 카드를 태그 하면 카드 인증을 담당하는 AuthenticationProvider가 해당 정보를 검증하고, 열쇠 인증을 담당하는 다른 AuthenticationProvider는 이 요청을 처리하지 않음
- 이처럼 각 인증 공급자가 자신이 처리할 수 있는 인증 요청만을 수락하도록 판단하는 기준이 바로 supports() 메서드

맞춤형 인증 논리 적용
- 맞춤형 인증 논리를 구현하는 과정은 다음과 같음
- AuthenticationProvider 계약을 구현하는 클래스를 선언
- 새 AuthenticationProvider가 어떤 종류의 Authentication 객체를 지원할지 결정
- 정의하는 AuthenticationProvider가 지원하는 인증 유형을 나타내도록 supports(Class<?> c) 메서드를 재정의
- authenticate(Authentication a) 메서드를 재정의해 인증 논리를 구현
- 새 AuthenticationProvider 구현의 인스턴스를 스프링 시큐리티에 등록
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Component | |
public class CustomAuthenticationProvider implements AuthenticationProvider { | |
@Autowired | |
private UserDetailsService userDetailsService; | |
@Autowired | |
private PasswordEncoder passwordEncoder; | |
@Override | |
public Authentication authenticate(Authentication authentication) { | |
String username = authentication.getName(); | |
String password = authentication.getCredentials().toString(); | |
UserDetails u = userDetailsService.loadUserByUsername(username); | |
if (passwordEncoder.matches(password, u.getPassword())) { | |
// 암호가 일치하면 필요한 세부 정보가 포함된 Authentication 계약의 구현을 반환 | |
return new UsernamePasswordAuthenticationToken(username, password, u.getAuthorities()); | |
} else { | |
// 암호가 일치하지 않으면 AuthenticationException 예외 발생 | |
// BadCredentialsException은 AuthenticationException을 상속 | |
throw new BadCredentialsException("Something went wrong!"); | |
} | |
} | |
@Override | |
public boolean supports(Class<?> authenticationType) { | |
return authenticationType.equals(UsernamePasswordAuthenticationToken.class); | |
} | |
} | |
@Configuration | |
public class ProjectConfig extends WebSecurityConfigurerAdapter { | |
@Autowired | |
private AuthenticationProvider authenticationProvider; | |
@Bean | |
public UserDetailsService userDetailsService(DataSource dataSource) { | |
return new JdbcUserDetailsManager(dataSource); | |
} | |
@Bean | |
public PasswordEncoder passwordEncoder() { | |
return NoOpPasswordEncoder.getInstance(); | |
} | |
// 앞서 정의한 AuthenticationProvider | |
@Override | |
protected void configure(AuthenticationManagerBuilder auth) { | |
auth.authenticationProvider(authenticationProvider); | |
} | |
} |

2. SecurityContext
- AuthenticationManager는 인증 프로세스를 성공적으로 완료한 뒤 요청이 유지되는 동안 Authentication 인스턴스를 저장함
- Authentication 객체를 저장하는 인스턴스를 SecurityContext라고 함

- 스프링 시큐리티의 SecurityContext 인터페이스는 다음과 같음
- SecurityContext의 주 책임은 Authentication 객체를 저장하는 것
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public interface SecurityContext extends Serializable { | |
Authentication getAuthentication(); | |
void setAuthentication(Authentication var1); | |
} |
- SecurityContextHolder는 관리자 역할을 하는 객체로 SecurityContext를 관리하는 세 가지 전략을 제공함
- MODE_THREADLOCAL: 각 쓰레드가 SecurityContext에 각자의 세부 정보를 저장할 수 있게 해 줌, request-per-thread 방식의 웹 애플리케이션에서는 각 요청이 개별 쓰레드를 가지므로 이는 일반적인 접근법
- MODE_INHERITABLETHREADLOCAL: MODE_THREADLOCAL과 비슷하지만 비동기 메서드의 경우 SecurityContext를 다음 쓰레드로 복사하도록 스프링 시큐리티에 지시함, 해당 방식으로 @Asysnc 메서드를 실행하는 새 쓰레드가 SecurityContext를 상속할 수 있음
- MODE_GLOBAL: 애플리케이션의 모든 쓰레드가 같은 SecurityContext 인스턴스를 보게 함
- 개발자가 스프링에 알려지지 않은 쓰레드를 정의할 경우 SecurityContext의 세부 정보를 명시적으로 새 쓰레드로 복사해야 함
- 스프링 시큐리티는 스프링의 컨텍스트에 없는 객체를 자동으로 관리할 수 없지만, 이를 위한 몇 가지 유용한 유틸리티 클래스를 제공함
SecurityContext를 위한 보유 전략 이용
- SecurityContext를 관리하는 첫 번째 전략은 MODE_THREADLOCAL 전략이며 스프링 시큐리티가 SecurityContext를 관리하는 데 이용하는 기본 전략
- 해당 전략에서 스프링 시큐리티는 ThreadLocal을 이용해 컨텍스트를 관리함
- ThreadLocal은 JDK에 있는 구현으로 해당 구현은 데이터의 컬렉션처럼 작동하지만 애플리케이션의 각 쓰레드가 컬렉션에 저장된 데이터만 볼 수 있도록 보장함
- 해당 방식으로 각 요청은 자신의 SecurityContext에 접근하며 쓰레드는 다른 쓰레드의 ThreadLocal에 접근할 수 없음
- 웹 애플리케이션에서 이는 각 요청이 자신의 SecurityContexxt만 볼 수 있다는 의미이고 백엔드 웹 애플리케이션의 일반적인 작동 방식
- 아래 그림처럼 비동기 메서드가 호출되어 새로운 쓰레드가 생성되면 해당 쓰레드도 자체적인 SecurityContext를 가지며, 상위 쓰레드인 요청의 원래 쓰레드의 세부 정보가 새로운 쓰레드의 SecurityContext에 복사되지 않음
- 인증 프로세스가 끝난 후 필요할 때마다 정적 getContext() 메서드로 보유자에게 SecurityContext를 요청하기만 하면 됨
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@GetMapping("/hello") | |
public String hello() { | |
SecurityContext context = SecurityContextHolder.getContext(); | |
Authentication a = context.getAuthentication(); | |
return "Hello, " + a.getName() + "!"; | |
} |
- 스프링은 Authentication을 메서드 매개 변수에 곧바로 주입할 수 있으므로 엔드포인트 수준에서는 더 편하게 컨텍스트에서 인증을 얻을 수 있음
- SecurityContextHolder 클래스를 매번 명시적으로 참조할 필요가 없음
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 스프링 부트가 현재 Authentication을 메서드 매개 변수에 | |
@GetMapping("/hello") | |
public String hello(Authentication a) { | |
return "Hello, " + a.getName() + "!"; | |
} |
비동기 호출을 위한 보유 전략 이용
- MODE_THREADLOCAL은 각 쓰레드의 SecurityContext를 격리할 수 있게 해 주고 SecurityContext를 더 자연스럽고 이해하기 쉽게 만들어주지만 해당 전략이 적용되지 않은 상황도 있음
- 요청당 여러 쓰레드가 사용될 때는 상황이 더 복잡해짐
- 엔드포인트가 비동기가 되면 메서드를 실행하는 쓰레드와 요청을 수행하는 쓰레드가 다른 쓰레드가 됨
- MODE_ INHERITABLETHREADLOCAL 전략을 설정하면 스프링이 SecurityContext를 올바르게 다음 쓰레드로 전파하며 Authentication도 더는 null이 아니게 됨
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@GetMapping("/bye") | |
@Async // @Async가 지정됐으므로 메서드가 별도의 쓰레드에서 실행됨 | |
public void goodbye() { | |
SecurityContext context = SecurityContextHolder.getContext(); | |
String username = context.getAuthentication().getName(); | |
// 사용자 이름으로 작업 수행 | |
} | |
@Configuration | |
@EnableAsync // @Async 어노테이션의 기능을 활성화하기 위해 @EnableAsync 어노테이션 지정 | |
public class ProjectConfig { | |
// 메서드가 SecurityContext를 상속하지 않는 다른 쓰레드에서 실행되기 때문에 MODE_INHERITABLEHTREADLOCAL 전략 설정 | |
// 해당 전략을 설정하지 않을 경우 Authentication 객체는 null | |
@Bean | |
public InitializingBean initializingBean() { | |
return () -> SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL); | |
} | |
} |

독립형 애플리케이션을 위한 보유 전략 이용
- SecurityContext가 애플리케이션의 모든 쓰레드에 공유되는 전략을 원할 경우 MODE_GLOBAL을 이용하면 됨
- 해당 전략은 일반적인 애플리케이션의 그림에는 맞지 않기 때문에 웹 서버에는 이용되지 않음
- 백엔드 웹 애플리케이션은 수신하는 요청을 독립적으로 관리하므로 모든 요청에 대해 하나의 컨텍스트를 이용하기보다 요청 별로 보안 컨텍스트를 분리하는 것이 더 합리적이지만 독립형 애플리케이션에는 공유하는 것이 좋은 전략일 수 있음

- MODE_INHERITABLETHREADLOCAL을 활성화할 때와 마찬가지로 SecurityContextHodler.setStrategyName() 메서드를 이용하거나 시스템 속성 spring.security.straategy를 설정해 전략을 변경할 수 있음
- SecurityContext는 thread-safe하지 않음
- 해당 전략을 설정할 경우 애플리케이션의 모든 쓰레드가 SecurityContext 객체에 접근할 수 있으므로 race condition은 개발자가 해결해야 함
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Bean | |
public InitializingBean initializingBean() { | |
return () -> SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_GLOBAL); | |
} |
DelegatingSecurityContextRunnable로 SecurityContet 전달
- 기본적으로 프레임워크는 요청의 쓰레드에 SecurityContext를 제공하고 해당 쓰레드만 이 SecurityContext에 접근하도록 보장하지만 새로 생성된 쓰레드에 관해서는 별도의 작업을 하지 않으므로 SecurityContext를 관리하는 다른 모드를 명시적으로 설정해야 한다고 배웠음
- 이러한 쓰레드는 프레임워크가 관리해주지 않아 개발자가 관리해야 하므로 종종 자체 관리 쓰레드라고 부름
- 특정한 SecurityContextHolder 전략으로 자체 관리 쓰레드를 위한 해결책을 얻을 수 없으며 개발자가 SecurityContext 전파를 해결해야 함
- 위 문제의 한 가지 해결책은 별도의 쓰레드에서 실행하고 싶은 작업을 DelegatingSecurityContextRunnable로 장식하는 것
- DelegatingSecurityContextRunnable은 Runnable을 확장하여 반환 값이 없는 작업 실행 후 이용할 수 있음
- 반환 값이 있는 작업에는 DelgatingSecurityContextCallable<T>에 해당하는 Callable<T>을 이용할 수 있음
- 두 클래스 모두 다른 Runnable 또는 Callable과 마찬가지로 비동기적으로 실행되는 작업을 나타내며, 작업을 실행하는 쓰레드를 위해 현재 SecurityContext를 복사해줌
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@GetMapping("/ciao") | |
public String ciao() throws Exception { | |
// 호출 가능 작업을 실행하기 위해 새로 생성된 쓰레드에 Authentication이 더는 존재하지 않고 SecurityContext는 비어 있음 | |
Callable<String> task = () -> { | |
SecurityContext context = SecurityContextHolder.getContext(); | |
return context.getAuthentication().getName(); | |
}; | |
ExecutorService e = Executors.newCachedThreadPool(); | |
try { | |
// 해당 문제를 해결하기 위해 DelegatingSecurityContextCallablle로 장식해 현재 컨텍스트를 새 쓰레드에 제공 | |
var contextTask = new DelegatingSecurityContextCallable<>(task); | |
return "Ciao, " + e.submit(contextTask).get() + "!"; | |
} finally { | |
e.shutdown(); | |
} | |
} |

DelegatingSecurityContextExecutorService로 SecurityContext 전달
- 프레임워크에 알리지 않고 코드에서 시작한 쓰레드를 다룰 때는 SecurityContext에서 다음 쓰레드로의 전파를 관리해야 함
- 스프링 시큐리티에는 DelegatingSecurityContextRunnable 및 DelegatingSecurityContextCallable과 같은 몇 가지 훌륭한 유틸리티 클래스를 통해 SecurityContext에 세부 정보를 복사하는 기법을 이용할 수 있으며 이들 클래스는 비동기적으로 실행하는 작업을 장식하고 구현이 새로 생성된 쓰레드의 SecurityContext에 접근할 수 있도록 SecurityContext에 세부 정보를 복하는 일도 함
- 하지만 새 쓰레드로의 SeucirtyContext 전파를 해결하는 다른 옵션이 있는데, Task에서 처리하지 않고 쓰레드 풀에서 전파를 관리하는 방법도 있음
- 작업을 장식하는 대신 특정 유형의 Executor를 이용할 수 있음
- 아래 예제에서는 task가 단순한 Callable<T>로 남아 있지만, 여전히 쓰레드가 SecurityContext를 관리하는 것을 볼 수 있음
- SecurityContext가 전파되는 이유는 DelegatingSecurityContextExecutorService 구현이 ExecutorService를 장식하기 때문
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@GetMapping("/hola") | |
public String hola() throws Exception { | |
Callable<String> task = () -> { | |
SecurityContext context = SecurityContextHolder.getContext(); | |
return context.getAuthentication().getName(); | |
}; | |
ExecutorService e = Executors.newCachedThreadPool(); | |
e = new DelegatingSecurityContextExecutorService(e); | |
try { | |
return "Hola, " + e.submit(task).get() + "!"; | |
} finally { | |
e.shutdown(); | |
} | |
} |

- 예약된 작업을 위해 SecurityContext 전파를 구현해야 할 경우 스프링 시큐리티에 있는 DelegatingSecurityContextScheduledExecutorService라는 데코레이터를 이용하면 됨
- 해당 데코레이터의 작동 메커니즘은 DelegatingSecurityContextExecutorService와 유사하지만, ScheduledExecutorService를 표시하므로 예약된 작업을 대상으로 하는 점이 다름
- 또한 스프링 시큐리티에는 유연성을 높이기 위한 DelegatingSecurityContextExecutor라는 더 추상적인 데코레이터 버전이 있음
- 해당 클래스는 쓰레드 풀 계층 구조의 가장 추상적인 계약인 Executor를 직접 장식하며 애플리케이션을 디자인할 때 쓰레드 풀의 구현을 언어가 제공하는 선택 사항으로 대체할 수 있기를 원할 경우 이를 선택할 수 있음
클래스 | 설명 |
DelegatingSecurityContextExecutor | Executor 인터페이스를 구현하여 Executor 객체를 장식하면 SecurityContext를 해당 풀에 의해 생성된 쓰레드로 전달하는 기능을 제공하도록 디자인 됨 |
DelegatingSecurityContextExecutorService | ExecutorService 인터페이스를 구현하며 ExecutorService 객체를 장식하면 SecurityContext를 해당 풀에 의해 생성된 쓰레드로 전달하는 기능을 제공하도록 디자인 됨 |
DelegatingSecurityContextScheduledExecutorService | ScheduledExecutorService 인터페이스를 구현하며 ScheduledExecutorService 객체를 장시갛면 SecurityContext를 해당 풀에 의해 생성된 쓰레드로 전달하는 기능을 제공하도록 디자인 됨 |
DelegatingSecurityContextRunnable | Runnable 인터페이스를 구현하고 다른 쓰레드에서 실행되며 응답을 반환하지 않는 작업을 나타냄, 일반 Runnable의 기능에 더해 새 쓰레드에서 이용하기 위해 SecurityContext를 전파할 수 있음 |
DelegatingSecurityContextCallable | Callable 인터페이스를 구현하고 다른 쓰레드에서 실행되며 최종적으로 응답을 반환하는 작업을 나타냄, 일반 Callable의 기능에 더해 새 쓰레드에서 이용하기 위해 SecurityContext를 전파할 수 있음 |
참고
스프링 시큐리티 인 액션
반응형
'Spring > 스프링 시큐리티 인 액션' 카테고리의 다른 글
[8장] 권한 부여 구성: 제한 적용 (0) | 2025.05.30 |
---|---|
[7장] 권한 부여 구성: 액세스 제한 (0) | 2025.05.30 |
[4장] 암호 처리 (0) | 2025.05.19 |
[3장] 사용자 관리 (0) | 2025.05.18 |
[2장] 스프링 시큐리티 기본 구성 (0) | 2025.05.18 |