[SpringBoot] Spring Security

나른한 개발자·2026년 1월 12일

f-lab

목록 보기
22/44

Spring Security

Spring Security는 애플리케이션에서 인증/인가 등 다양한 보안 기능을 제공하는 프레임워크이다.

사용 이유

  • 검증된 보안 매커니즘: 보안 기능을 직접 구현하면 보안 취약점이 생길 수 있지만, Spring Security를 사용하면 검증된 방식으로 보안을 구현할 수 있다.
  • 비즈니스 로직에 집중: 인증/인가와 같은 보안 검사를 컨트롤러마다 매번 작성해주지 않아도 되어 개발자는 실제 서비스 구현에 집중할 수 있다.
  • 높은 확장성: 폼 로그인, OAuth2, JWT, LDAP 등 다양한 인증 방식을 유연하게 통합할 수 있다.

특징

  • 선언적 보안: 코드 내부에 복잡한 if-else 문을 쓰지 않고, 설정 클래스나 어노테이션(@PreAuthorize 등)을 통해 간단히 보안규칙을 적용할 수 있다.
  • 암호화 지원: 비밀번호를 안전하게 저장하기 위한 PasswordEncoder를 제공한다.
  • Spring 생태계에 적합: Spring이라는 프레임워크 안에서 활용하기 적합한 구조로 설계되어 있어, 보안 기능을 추가할 때 활용하기 좋다.

관련 용어

  • Principal: 시스템에 접근을 시도하여 신원이 확인된 사용자. (대부분의 경우 UserDetails를 구현한 객체 혹은 사용자 ID/객체 자체)
  • Credentials: 비밀번호나 토큰. 보통 인증 후 보안을 위해 삭제됨
  • Authorities: 사용자에 할당된 권한 정보. ROLE_USER, ROLE_ADMIN 등

동작 원리


Java Spring MVC 프레임워크에서 클라이언트로부터의 모든 HTTP 요청은 DispatcherServlet이 가장 먼저 수신한다. DispatcherServlet은 Java EE의 HttpServlet 클래스를 상속받아 구현된 프론트 컨트롤러로, 수신한 HTTP 요청 정보를 기반으로 적절한 컨트롤러에 요청 처리를 위임하는 역할을 한다. 또한, 운영 환경에 따라 서버로 들어오는 모든 요청에 대해 공통적으로 적용해야 할 부가 작업이 있을 때는 위 그림과 같이 DispatcherServlet 이전 단계에 위치한 Filter가 이를 처리하게 된다.

Filter는 javax.servlet.Filter 인터페이스를 구현한 클래스로, HTTP 요청이 DispatcherServlet에 도달하기 전에 요청과 응답을 가로채고 부가 작업을 처리하는 역할을 수행한다. 위 그림에서 보면 DispatcherServlet은 Java Spring 컨테이너의 가장 앞 단에 위치해 있는 반면, Filter는 Java Spring 컨테이너 외부의 웹 컨테이너(Servlet 컨테이너)에 위치해 있음을 알 수 있다.

그렇기 때문에 Filter 단계에서 인증, 인가, 로깅, 인코딩 등의 보안 기능을 수행하면 Java Spring 컨테이너에 도달하기 전에 잠재적인 보안 위협을 차단하여 시스템의 안정성을 높일 수 있다. 그리고 Spring Security도 Filter 단계에서 보안 기능을 수행하게 된다.

클라이언트 요청
  ↓
[FilterChain - 서블릿 컨테이너가 생성]
  ├── Filter1 (예: CharacterEncodingFilter)
  ├── Filter2 (예: CorsFilter)
  ├── Filter3 (예: 커스텀 로깅 필터)
  └── DispatcherServlet (Spring MVC)
       ↓
    Controller

클라이언트가 요청을 서버로 전송하면 컨테이너가 1개 이상의 Filter 인스턴스와 Servlet을 포함하는 FilterChain을 생성한다. 이때, 사용되는 Servlet은 DispatcherServlet의 인스턴스이며, Filter는 Filter 구현체의 인스턴스이다. HTTP 요청은 이렇게 생성된 FilterChain안의 여러 Filter를 순차적으로 거치며 각 Filter는 자신에게 할당된 작업을 수행한다.

다만, Filter는 웹 컨테이너에서만 생성되고 실행되며, Java Spring의 IoC 컨테이너와는 별개의 컨테이너이기 때문에 Java Spring에서 정의된 Bean을 주입받아 사용할 수 없다. 즉, Spring 컨테이너에서 사용되는 기술(객체 의존성)을 필터에서 직접 활용할 수 없는 것이다. 이를 해결하기 위해 Java Spring에서는 DelegatingFilterProxy라는 Filter 구현체를 사용한다.

DelegatingFilterProxy 클래스는 Java Spring 프레임워크에서 제공하는 클래스 중 하나로, 웹 컨테이너의 Filter와 Spring IoC 컨테이너 사이의 통합(연결)을 지원한다. 웹 컨테이너는 DelegatingFilterProxy 클래스를 사용해서 Filter 정의를 Spring IoC 컨테이너에 정의된 특정 Bean(springSecurityFilterChain)에게 위임함으로써, Java Spring의 DI와 관리 기능을 사용할 수 있게 한다. 이를 통해 웹 컨테이너에서 생성된 Filter가 직접 요청을 처리하는 대신 해당 Bean이 요청을 처리하게 된다.

[서블릿 컨테이너의 FilterChain]

클라이언트 요청
  ↓
Filter 1 실행 (: CharacterEncodingFilter)Filter 2 실행 (: CorsFilter)Filter 3 실행 → DelegatingFilterProxy ← 여기서!|
  |              | Spring 컨테이너에서
  |              | "springSecurityFilterChain" Bean 찾기
  ||        [Spring Security 영역]
  |        FilterChainProxy 실행
  ||        보안 필터 15+ 순차 실행
  ||        (처리 완료, 다시 돌아옴)|
Filter 4 실행    |DispatcherServletController

즉, 서블릿 컨테이너에 의해 DelegatingFilterProxy가 필터 체인 중 일부로 등록이 된다. 순서에 맞춰 다른 필터들이 실행되다가 이 순서가 오면 스프링 컨테이너에서 springSecurityFilterChain 빈을 찾아서 보안 필터 실행을 위임한 뒤 처리가 끝나면 다시 Filter로 돌아오는 것이다.

Spring Security Filter


필터가 하는 역할은 클라이언트와 자원 사이에서 요청과 응답 정보를 이용해 다양한 처리를 하는데 목적이 있다. Spring Security는 다양한 기능을 가진 필터들을 10개 이상 제공하고 있다. 이렇게 제공되는 필터들을 Security Filter Chain이라고 한다.

Spring Security 주요 모듈

Authentication

현재 접근하는 주체의 정보와 권한을 담는 인터페이스이다.

public interface Authentication extends Principal {
    Collection<? extends GrantedAuthority> getAuthorities(); // 권한 목록
    Object getCredentials(); // 비밀번호 등 인증 정보
    Object getDetails(); // 추가 정보
    Object getPrincipal(); // 사용자 정보
    boolean isAuthenticated(); // 인증 여부
}

SecurityContext

Authentication을 보관하는 역할을 하며 SecurityContext를 통해 Authentication을 꺼내올 수 있다.

SecurityContextHolder

보안 주체를 세부 정보를 포함하여 응용 프로그램의 현재 보안 컨텍스트에 대한 세부정보가 저장된다.

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

이 세 모듈의 연관관계를 알아보자. 예를들어 유저가 인증을 마친다. 인증에 성공하면 principal, credential 정보를 Authentication에 담는다. 그리고 Spring Security에서 Authentication을 SpringContext에 저장한다. 그리고 이 SpringContext를 SecurityContextHolder에 담아 보관한다.

UserDetails

public interface UserDetails extends Serializable{
	// 권한 목록
    Collection<? extends GrantedAuthority> getAuthorities();
    String getPassword();
    String getUserame();
    // 계정 만료 여부
    boolean isAccountNonExpried();
    // 계정 잠김 여부
    boolean isAccountNonLocked();
    //비밀번호 만료 여부
    boolean isCredentialNonExpired();
    //사용자 활성화 여부
    boolean isEnabled();
}

public class MyUserDetails implements UserDetails{
	private User user;
    public User getUser(){
    	return user;
    }
    public MyUserDetails(User account){
    	this.user = account;
    }
    @Override ...
}

인증에 성공하여 생성된 UserDetails는 Authentication객체를 구현한 UsernamePasswordAuthenticationToken를 생성하기 위해 사용된다. UserDetails를 implements하여 처리할 수 있다.

UserDetailsService

UserDetailsService는 UserDetails를 반환하는 하나의 메서드 만을 가지고 있는데, 일반적으로 이를 implements한 클래스에 UserRepository를 주입받아 DB와 연결하여 처리한다. 즉 이곳에서 DB의 사용자 정보를 조회한다.

public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) 
        throws UsernameNotFoundException;
}

@Service
@RequiredArgsConstructor
public class MyUserDetailsService implements UserDetailsService{
	private final UserRespository userRepository;
    
    @Override
    public UserDetails loadUserByUsername(String id) throws UsernameNotFoundException{
    	User user = userRepository.findUser(id);
    	return new UserDetail(user);
    }
}

UsernamePasswordAuthenticationToken

Authentication을 구현한 AbstractAuthenticationToken의 하위 클래스로, User의 Id가 principal의 역할을 하고, password가 credential 역할을 한다. UsernamePasswordAuthenticationToken의 인증 전의 객체를 생성하는 생성자와, 인증이 완료된 객체를 생성하는 생성자가 있다.

AuthenticationManager

public interface AuthenticationManager {
    Authentication authenticate(Authentication authentication) 
        throws AuthenticationException;
}

인증에 대한 부분은 AuthenticationManager에서 처리하게 되는데, 실질적으로는 AuthenticationManager에 등록된 AuthenticationProvider에 의해 처리된다. 실제 구현체는 ProviderManager이다. 인증에 성공하면 객체를 생성하여 SecurityContext에 저장한다.

AuthenticationProvider

실제 인증에 대한 처리를 한다. 인증 전의 Authentication 을 받아서 인증이 완료된 객체를 반환하는 역할을 한다.



Spring Security의 인증과정

일반적인 프로세스

1. 클라이언트가 인증을 시도한다.
2. Authentication Filter에서 인증을 처리한다.
Servelt Filter에 의해 Security Filter 로 작업이 위임되고 여러 시큐리티 필터 중에서 UsernamePasswordAuthenticationFilter에서 인증을 처리한다. UsernamePasswordAuthenticationFilter에 의해 로그인 폼으로 보내진다. 로그인 폼을 사용하지 않으면 사용자 정의 필터를 추가하면 된다.
3. UsernameAuthenticationToken 발급
AuthenticationFilter는 HttpServletRequest에서 아이디, 비밀번호를 추출하여 UsernameAuthenticationToken을 발급한다.
4. AuthenticationManager에 인증 객체 전달
AuthenticationFilter는 AuthenticationManager에 인증객체를 전달한다.
5.인증을 위해 AuthenticationProvider에게 인증객체를 전달
6. 전달받은 인증객체의 정보를 UserDetailsService에 전달
7. UserDetails 구현객체 생성
UserDetailsService는 DB에서 사용자를 조회하여 이를 기반으로 UserDetails 객체를 생성하여 반환한다.
8.ProviderManager에게 권한을 담은 검증된 인증객체 전달
AuthenticationProvider는 전달받은 UserDetails를 인증해 성공하면 ProviderManager에게 권한을 담은 검증된 인증 객체를 전달한다.
9.검증된 인증객체를 AuthenticationFilter에 전달
ProviderManager가 AuthenticationFilter에 전달한다.
10. 검증된 인증객체를 SecurityContextHolder의 SecurityContext에 저장한다.
AuthenticationFilter가 UserDetails정보를 SecurityContextHolder에 저장한다.

로그인한 사용자 정보 가져오기

위 과정을 통해 인증을 완료했다. 이후 다른 유저 서비스를 이용하면 UserDetails가 필요하다. 이때는 다음과 같은 과정으로 처리된다.

1. 빈에서 사용자 정보 가져오기
전역에 선언된 SecurityContextHolder에서 가져오는 방법이다.

public static String getCurrenUserId(){
	Object principal = SecurityContextHolder.getContext().getAuthentication().getPricipal();
    User currUser = (User) principal;
    return currUser.getId();
}    

2. 컨트롤러에서 가져오기

@PostMapping()
public void create(
	Principal principal, 
    Authenticaion authentication){...}

3. @AuthenticationPrincipal

@PostMapping()
public void create(
	@AuthenticationPrincipal MyUserDetials myUserDetails){...}

Spring Security 3.2 부터는 어노테이션으로 현재 로그인한 사용자 객체를 인자에 주입할 수 있다.

참고 자료

스프링 시큐리티는 인증/인가를 포함한 다양한 보안 기능을 구현할 수 있는 프레임워크이다. 직접 보안 기능을 구현하면 취약점이 발생할 수 있지만 스프링 시큐리티를 사용하면 검증된 보안 기능을 구현할 수 있고, 설정 클래스나 어노테이션으로 간단하게 보안 규칙을 적용할 수 있다. 폼 로그인, OAuth2, JWT 등 다양한 인증 방식을 지원하고, CSRF공격 같은 보안 위협에 대한 방어 기능도 제공한다. 스프링 시큐리티는 필터 기반 아키텍처로 동작하는데, 요청이 컨트롤러에 도달하기 전에 여러 보안 필터를 거치면서 인증과 인가를 처리한다. 인증된 사용자 정보는 SecurityContext에 저장되며, 애플리케이션 어디서든 접근할 수 있다.
필터 기반 아키텍처? => 서블릿 컨테이너는 요청이 들어오면 FilterChain을 생성하는데, Spring Security는 이 FilterChain에 DelegatingFilterProxy로 등록됩니다.
DelegatingFilterProxy는 실제 보안 처리를 하지 않고, Spring 컨테이너에서 FilterChainProxy Bean을 찾아서 위임한다.
FilterChainProxy는 내부적으로 15개 이상의 보안 필터들을 순차적으로 실행하는데, 예를 들어 UsernamePasswordAuthenticationFilter는 로그인을 처리하고, AuthorizationFilter는 권한을 검증하는 식으로 각 필터가 특정 역할을 담당한다.

profile
Start fast to fail fast

0개의 댓글