[Spring] Spring Security 인증 구성

2024. 1. 2. 23:42·Spring/SpringBoot
스프링 시큐리티는 스프링 컨테이너 외부인 서블릿에 대해 필터를 추가함으로써 그 기능을 수행합니다. 개발자가 직접 이 필터들을 서블릿 컨테이너에 개발하는 대신 FilterChainProxy를 이용해 스프링 컨테이너 내에 존재하는 빈을 필터로 등록할 수 있도록 했습니다.

 

이번 포스팅에서는 더 나아가 이 필터 중 인증 관련 필터를 스프링 시큐리티가 어떻게 구성했는지, 개발자는 이를 어떻게 확장할 수 있을지에 대해 알아보겠습니다.

 


 스프링 시큐리티 인증 처리 과정

첫번째로, 요청에 대한 인증입니다. HTTP의 Stateless특성으로 HTTP를 사용하는 서버는 매 순간의 접속이 어떤 클라이언트의 요청인지에 대한 상태를 저장하지 않습니다. 서버는 이 요청이 리소스에 대한 권한을 가지고 있는지만을 판단함으로써 접근을 허용할지, 말지를 결정합니다. 따라서, 클라이언트는 HTTP Request 메시지에 자신을 증명할 수 있는 데이터( Credential Data )를 포함하여 요청합니다. 이를통해 서버는 메시지에 포함된 헤더를 이용해 현재 요청 메시지가 리소스에 접근할 권한이 있는지를 파악할 수 있게 됩니다. 

 

정리하자면, 클라이언트의 HTTP 요청을 처리하기 위해선 다음과 같은 과정을 필요로 합니다.

 

  1. HTTP 요청 메시지에서 Credential 데이터를 추출합니다.
  2. 추출한 Credential 데이터를 이용해 해당 사용자에게 어떤 권한이 있는지를 확인하고, 확인된 권한을 내부적으로 저장합니다.
  3. 현재 요청을 처리하는데 필요한 권한과 2번 단계에서 저장한 권한을 비교하여 일치할때, 요청을 성공적으로 처리합니다. 

위 단계에 대해 알아보기 전에, 사전지식이 필요합니다. 

기본개념 - SecurityContextHolder

요청에 대해 할당되는 스레드는 이를 통해 현재 요청에 대한 정보를 기록합니다

 

요청에 대해 일련의 인증 및 인가 과정을 거치기 위해서는 하나의 요청 즉, 쓰레드가 요청에 대한 정보를 가지고 있을 필요가 있습니다.

시큐리티는 요청의 Credential 데이터 및 Authorized 정보를 저장하기 위한 SecurityContextHolder를 가지고 있습니다.  모든 Spring Security의 기능들은 이를 바탕으로 이루어지며 보안 기능을 구현하기 위한 핵심적인 자료구조입니다.

SecurityContextHolder가 가지는 SecurityContext는 Thread-Local로 관리되어짐으로, 실행되는 스레드 안에서 언제 어디서든 접근이 가능하며, SecurityContext는 사용자의 Principal 정보, Credential 정보, Authorities 정보를 가지는 Authentication 객체을 가집니다. 이 Authentication 객체를 이용하여 개발자는 자기만의 인증 및 인가를 구현할 수 있습니다. 

Thread-local이란, 각각의 스레드만이 접근가능한 데이터를 말합니다. 하나의 스레드는 하나의 요청을 처리함으로, 각각의 요청에 대해 본인만이 접근할 수 있는 자료구조를 가짐으로써 더 안전하게 사용자 데이터를 관리할 수 있게 됩니다. 

SecurityContextHolder는 스레드 별로 SecurityContext를 가집니다. 요청에 대해 할당되는 스레드는 이를 통해 현재 요청에 대한 정보(를 기록합니다.

 

Spring Security는 SecurityContext내의 Authentication이 어떤 Authentication이 사용되는지 상관하지 않습니다. 개발자는 그저 Authentication 인터페이스를 구현한 커스텀 객체를 사용할 수 있습니다.

 

Authentication 객체를 필터 내에 구현하고 이를 AuthenticationManger에게 넘겨 이 요청이 권한을 가질지, 가지지 못할지를 결정하게 됩니다. 

아래 코드는 SecurityContextHolder를 이용해 Authentication을 새로 등록하는 코드입니다.

// SecurityContextHolder 로부터 새로운 SecurityContext 생성
SecurityContext context = SecurityContextHolder.createEmptyContext(); 

// 사용자 커스텀 Authentication 객체 사용 사용된 파라미터는 임의로 사용한 principal, credential, authorities 입니다.
Authentication authentication =
    new TestingAuthenticationToken("username", "password", "ROLE_USER"); 
    
// SecurityContext에 Authentication 설정
context.setAuthentication(authentication);

// SecurityContextHolder에 SecurityContext 설정
SecurityContextHolder.setContext(context);

 

SecurityContextHolder가 가지는 Thread-local 데이터 SecurityContext가 다중 스레드로 인해 RaceCondition에 빠질 수 있기 떄문에 아래와 같은 방법은 사용하기 권장되지 않습니다.

SecurityContextHolder.getContext().setAuthentication(authentication)

 

 

 

기본개념 - Authentication Manager

AuthenticationManager는 인증 작업을 수행하는 Security Filter에 API를 제공하여, SecurityFilter의 인증 작업을 돕습니다. 이는 하나의 인터페이스 이며, 이 인터페이스를 구현한 ProviderManager를 대부분 사용합니다. ProviderManager는 내부적으로 Authentication 객체를 처리하기 위한 여러개의 AuthenticationProvider 객체를 가지고 있습니다. 각각의 AuthenticationProvider는 파라미터로 넘겨지는 Authentication객체의 클래스 타입을통해 자신이 처리할 수 있는 Authentication객체인지 확인하고, 이를 처리합니다. 처리를 수행할 AuthenticationProvider객체를 찾지 못하면, ProviderNotFoundException이 발생합니다. 

 

 

 

 

ProviderManager가 가질 AuthenticationProvider객체는 사용자가 직접 등록해야 합니다.

 


 

1. 요청 메시지에서 데이터 추출하기

이 과정은 HttpServletRequest 메시지를 다루는 필터를 통해 구현합니다. 필터는 HttpServletRequest메시지에서 Credential 정보를 추출합니다. 이를 통해 만든 Authentication을 ProviderManager를 통해 검증 할 수 있도록 합니다. ProviderManager 로 부터 받은 Authentication을 SecurityContext에 등록하고 이를 SecurityContextHolder에 저장하여 스레드 내에서 사용할 수 있도록 합니다. 

private static final String HEADER_ID = "id";
private static final String HEADER_PWD = "pwd";

@Bean
public OncePerRequestFilter customFilter() {
    return new OncePerRequestFilter() {
        @Autowired
        ProviderManager providerManager;

        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            String id = request.getHeader(HEADER_ID);
            String pwd = request.getHeader(HEADER_PWD);

            SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
            Authentication authentication = new TestingAuthenticationToken(id, pwd);
            // TestingAuthenticationToken 은 스프링 시큐리티에서 제공하는 Authentication을 구현한 클래스입니다. 
			//	해당 예시에서는 편의를 위해 사용했으며,Authentication객체를 언제든 새로 구성하여 사용하실 수 있습니다.

            securityContext.setAuthentication(providerManager.authenticate(authentication));
            SecurityContextHolder.setContext(securityContext);

            filterChain.doFilter(request, response);
        }
    };
}

 

2. ProviderManger 구성

AuthenticationProvider과 이를 내부에서 관리하는 ProviderManger 이용해 authentication의 유효성을 검사합니다. 

파라미터로 받은 authentication 객체가 유효할 시, provider는 새로운 Authentication객체를 리턴합니다. 

@Bean
ProviderManager providerManager(AuthenticationProvider authenticationProvider) {
	// ProviderManager는 여러 AuthenticationProvider를 가질 수 있습니다. 이 예시에서 더 확장하여 여러 AuthenticationProvider를 가지도록 할 수 있습니다.
    return new ProviderManager(authenticationProvider);
}

@Bean
AuthenticationProvider authenticationProvider() {
    return new AuthenticationProvider() {
        @Override
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            String id = (String) authentication.getPrincipal();
            String pwd = (String) authentication.getCredentials();
            if (id.equals("heejong") && pwd.equals("1234")) {
                return new TestingAuthenticationToken(id, pwd, "ROLE_USER");
            }
            return authentication;
        }

        @Override
        public boolean supports(Class<?> authentication) {
        	// 파라미터로 받은 authentication객체를 현재 provider객체에서 처리할 수 있는지 여부를 확인하는 용도입니다. 
            // 이 예시는 구성을 이해하기 위함으로 항상 true를 리턴하도록 구성했습니다.
            return true;
        }
    };
}

3. SecurityFilterChain 구성 

필터를 구성합니다. 모든 경로로의 요청의 Authentication 객체가 USER 권한이 있는지를 확인하고 있다면 이를 허용하도록 구성했습니다.( 이에 대한 자세한 설명은 Authorization에 대해 다루며 자세히 설명하겠습니다. )

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
    return httpSecurity
            .addFilterBefore(customFilter(), AuthorizationFilter.class)
            .authorizeHttpRequests(http ->
                    http.anyRequest().hasRole("USER"))
            .build();
}

 

 

 

 

전체 코드는 다음과 같습니다.

package com.example.security.config;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.intercept.AuthorizationFilter;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@EnableWebSecurity
@Configuration
public class SecurityConfig {

    private static final String HEADER_ID = "id";
    private static final String HEADER_PWD = "pwd";

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity
                .addFilterBefore(customFilter(), AuthorizationFilter.class)
                .authorizeHttpRequests(http ->
                        http.anyRequest().hasRole("USER"))
                .build();
    }

    @Bean
    public OncePerRequestFilter customFilter() {
        return new OncePerRequestFilter() {
            @Autowired
            ProviderManager providerManager;

            @Override
            protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
                String id = request.getHeader(HEADER_ID);
                String pwd = request.getHeader(HEADER_PWD);

                SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
                Authentication authentication = new TestingAuthenticationToken(id, pwd);

                securityContext.setAuthentication(providerManager.authenticate(authentication));
                SecurityContextHolder.setContext(securityContext);

                filterChain.doFilter(request, response);
            }
        };
    }


    @Bean
    ProviderManager providerManager(AuthenticationProvider authenticationProvider) {
        return new ProviderManager(authenticationProvider);
    }

    @Bean
    AuthenticationProvider authenticationProvider() {
        return new AuthenticationProvider() {
            @Override
            public Authentication authenticate(Authentication authentication) throws AuthenticationException {
                String id = (String) authentication.getPrincipal();
                String pwd = (String) authentication.getCredentials();
                if (id.equals("heejong") && pwd.equals("1234")) {
                    return new TestingAuthenticationToken(id, pwd, "ROLE_USER");
                }
                return authentication;
            }

            @Override
            public boolean supports(Class<?> authentication) {
                return true;
            }
        };
    }
}

 

 

긴 글 읽어주셔서 감사합니다. 

 

참고 : https://docs.spring.io/spring-security/reference/servlet/authentication/architecture.html

'Spring > SpringBoot' 카테고리의 다른 글

Spring boot - Cache  (0) 2025.01.07
[SpringBoot] 스프링의 에러처리 탐구  (0) 2024.01.17
[Spring] 다수의 SecurityFilterChain 구성 방법  (0) 2024.01.01
[Spring] Spring Security Architecture  (1) 2023.12.29
[Spring] RestTemplate  (1) 2023.12.29
'Spring/SpringBoot' 카테고리의 다른 글
  • Spring boot - Cache
  • [SpringBoot] 스프링의 에러처리 탐구
  • [Spring] 다수의 SecurityFilterChain 구성 방법
  • [Spring] Spring Security Architecture
윤희종
윤희종
호기심을 잃지 말자 지적, 질문은 언제나 환영합니다 ;)
  • 윤희종
    서버견문록
    윤희종
  • 전체
    오늘
    어제
    • 분류 전체보기 (36)
      • 데일리 플랜 (1)
      • 이것저것 (4)
      • Java (6)
      • Spring (12)
        • SpringBoot (10)
        • Spring MVC (0)
      • Computer Science (4)
        • Network (1)
        • Operating System (0)
        • Data Structure (0)
        • Algorithm (2)
        • Database (0)
      • IOS (0)
      • 프로그래머스 문제풀이 (2)
      • 프로젝트 일기 (7)
        • 한편의 수학 학원 (7)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    read through
    인증 우회 테스트
    cache write back
    비동기 처리 유의점
    스프링
    springboot
    servlet
    스프링 시큐리티 사용법
    성능 개선
    Spring
    스프링 부트
    인증 테스트
    캐시
    SecurityFilterChain 구성
    알고리즘
    제네릭
    스프링 시큐리티 구성
    SecurityFilterChain
    mysql 쿼리 최적화
    스프링 부트 인증 우회
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
윤희종
[Spring] Spring Security 인증 구성
상단으로

티스토리툴바