본문 바로가기
Tech Stack/Backend

🛡️Spring Security & HTTP Basic 인증 방식의 이해

by 박수무당벌레 2026. 6. 29.

1. Spring Security의 핵심 인증 절차

사용자가 로그인을 시도할 때, 스프링 시큐리티 내부에서는 디스패처 서블릿(DispatcherServlet)에 요청이 도달하기 전 여러 보안 필터들을 거치며 인증 및 권한 부여를 처리한다.

1) 인증 vs 권한 부여

  • 인증 (Authentication): 현재 사용자가 가입된 회원이 맞는지(ID 존재 여부 및 패스워드 일치 여부) 확인하는 절차.
  • 권한 부여 (Authorization): 인증된 사용자가 특정 리소스나 엔드포인트에 접근할 수 있는 자격(예: 구독 여부, 역할)이 있는지 확인하는 절차

2) 핵심 보안 컴포넌트의 흐름 (인증 과정)

또한, Spring Security는 인증 정보를 SecurityContextHolder에 저장하여 애플리케이션 전반에서 인증된 사용자 정보를 참조할 수 있게 함. 전체적인 메커니즘 흐름은 다음과 같다.

  1. AuthenticationFilter (요청 가로채기)
    • HTTP Request를 가장 먼저 가로챈다. starter-security 의존성을 추가하면 기본적으로 BasicAuthenticationFilter, LogoutFilter 등이 활성화됨.
    • 인증되지 않은 사용자는 이후 필터나 디스패처 서블릿으로 가지 못하고 이 단계에서 차단(Cut).
    • AuthenticationToken 생성: 사용자가 보낸 자격 증명(아이디/비밀번호)을 기반으로 미인증 상태의 Authentication 객체/구현체(예: UsernamePasswordAuthenticationToken)를 생성한다.
    • AuthenticationManager 전달: 위 생성된 토큰은 인증 처리를 총괄하는 AuthenticationManager(구현체: ProviderManager)로 전달됨.
  2. AuthenticationManager (인증 위임 및 총괄)
    • 필터로부터 인증 처리를 위임받아 authenticate() 메서드를 호출.
    • 내부적으로 등록된 Authentication Provider들을 순회(for문)하며 적절한 인증 로직을 찾는다(Provider로 인증을 위임하기 위함). 별도의 Provider를 지정하지 않으면 기본적으로 DaoAuthenticationProvider 등이 사용됨.
  3. AuthenticationProvider (실제 인증 로직 수행 ─ 사용자 정보 검증)
    • UserDetailsService: 데이터베이스(실습에서는 메모리 기반의 InMemoryUserDetailsManager)에서 loadUserByUsername()을 호출하여 해당 ID의 사용자 정보(UserDetails)가 존재하는지 조회.
    • PasswordEncoder: 사용자(유저)가 존재한다면 입력된 비밀번호와 DB에 암호화되어 저장된 비밀번호가 일치하는지 검증한다. (테스트용으로는 암호화를 하지 않는 NoOpPasswordEncoder가 쓰이기도 함)
  4. SecurityContext (인증 결과 저장)
    • 인증이 성공하면, 사용자의 권한(Authorities) 정보가 포함된 최종 인증 객체(Authentication)를 반환한다.
    • SecurityContext 저장: 인증된 객체는 SecurityContextHolder 내부의 SecurityContext에 저장(context.setAuthentication(authResult))되어 전역적으로 참조 가능.
    • 이 정보는 요청을 처리하는 동안 스레드(*Thread Local)와 연계되어 안전하게 보관된다. (톰캣의 스레드 풀에서 할당된 스레드가 해당 요청을 처리함)
  5. DispatcherServlet 인계
    • 모든 보안 필터 검증이 정상적으로 끝나면 디스패처 서블릿의 service()가 호출되어 마침내 컨트롤러(엔드포인트) 메서드가 실행된다.
✨ ThreadLocal: 해당 스레드만 접근할 수 있는 전용 보관함과 같아서, 하나의 HTTP 요청을 처리하는 동안 (컨트롤러나 서비스 등) 애플리케이션 전역에서 파라미터로 인증 객체를 일일이 넘겨주지 않아도 안전하게 유저 정보를 꺼내 쓸 수 있게 해줌.

2. HTTP Basic 인증 (RFC 표준)

1) 동작 방식 및 특징

  • 사용자의 ID와 Password를 ID:Password 형태로 결합한 뒤, Base64로 인코딩하여 HTTP 요청 헤더의 Authorization 필드에 담아 보내는 방식. (HTTP 프로토콜 자체에서 지원하는 가장 단순한 인증 방식)
  • 동작 원리 (Base64 인코딩):
    1. 인증 요청: 클라이언트가 보호된 리소스에 접근하려고 할 때, 서버는 401 Unauthorized 상태 코드와 함께 WWW-Authenticate: Basic realm="영역" 헤더를 반환한다.
    2. 자격 증명 전송: 사용자가 아이디(username)와 비밀번호(password)를 입력하면, 클라이언트는 이 둘을 콜론(:)으로 결합한다. (username:password)
    3. Base64 인코딩: 결합된 문자열을 Base64 방식으로 인코딩한다.
      • 예시: admin:1234 YWRtaW46MTIzNA==
    4. 헤더 부착: 인코딩된 값을 Authorization 헤더에 담어 서버로 보냅니다.
    5. Authorization: Basic YWRtaW46MTIzNA==
  • 양식: Authorization: Basic [Base64 인코딩된 문자열]
  • 단점: 브라우저가 자체적으로 로그인 팝업창(WWW-Authenticate 헤더에 의해 트리거됨)을 제공하므로 편리하지만, 매 요청마다 비밀번호가 노출되고 Base64는 쉽게 디코딩이 가능하므로 보안이 매우 취약하다.
⚠️ 보안 취약성 추가 설명
Base64는 암호화(Encryption)가 아닌 단순 인코딩(Encoding) 기법. 누구나 쉽게 디코딩하여 원본 아이디와 비밀번호를 추출할 수 있으므로, HTTP Basic 인증을 사용할 때는 반드시 HTTPS(SSL/TLS)를 함께 적용해 네트워크 구간을 암호화해야 한다.

2) 왜 Base64 인코딩을 사용하는가? (부가 목적: 엔디안 보정)

  • 핵심 목적: 엔디안 차이를 포함한 시스템 간 데이터 깨짐 방지
    • HTTP 헤더는 텍스트(문자열)만 전송할 수 있는 규격.
    • 시스템마다 줄바꿈 문자(\n, \r\n)나 특수문자를 해석하는 방식이 달라 데이터가 깨질 수 있기 때문에, 전 세계 모든 시스템이 공통으로 안전하게 인식할 수 있는 64개의 안전한 알파벳/숫자 ASCII 문자로만 바꾸기 위해(바이너리-텍스트 변환) Base64를 사용하는 것.
  • 컴퓨터 CPU마다 메모리에 데이터를 저장하는 방식(엔디안)이 다름.
    • 리틀 엔디안 (Little-Endian): 하위 바이트부터 메모리에 저장 (Intel, 대부분의 ARM)
    • 빅 엔디안 (Big-Endian): 상위 바이트부터 메모리에 저장 (네트워크 데이터 전송 표준)
  • 이처럼 CPU 환경과 네트워크 전송 방식 간의 데이터 표현 차이(엔디안)를 보정하고, 시스템 간 시스템 독립적인 문자열 송수신을 보장하기 위해 3바이트 단위를 6비트씩 그룹핑하는 Base64 인코딩을 사용한다.

3. HTTP Basic 인증의 필터 구조 (Filter Chain)

Spring Security는 서블릿 필터 체인(Filter Chain)을 기반으로 동작합니다. HTTP Basic 인증이 활성화되면 Filter Chain 내부에 BasicAuthenticationFilter가 활성화되어 요청을 가로챔.

필터 내부 흐름

  1. 헤더 검사: 요청이 들어오면 BasicAuthenticationFilter가 HTTP 요청 헤더에 Authorization 헤더가 있는지, 그리고 그 값이 Basic 으로 시작하는지 확인한다.
  2. 조건 미충족 시: 헤더가 없거나 Basic 인증 요청이 아니라면, 인증 처리를 하지 않고 다음 필터(chain.doFilter())로 요청을 넘김.
  3. 인증 처리: 헤더가 존재하면 Base64로 인코딩된 값을 디코딩하여 아이디와 비밀번호를 추출.
  4. 토큰 생성 및 위임: 추출한 정보로 UsernamePasswordAuthenticationToken을 생성하여 앞서 설명한 AuthenticationManager에게 인증을 요청.
  5. 성공/실패 처리:
    • 성공: 인증 객체를 SecurityContextHolder에 세팅하고 다음 필터로 진행한다.
    • 실패: AuthenticationEntryPoint(기본적으로 BasicAuthenticationEntryPoint)가 호출되어 클라이언트에게 다시 401 Unauthorized 응답을 보냄.

4. Spring Security 직접 설정 및 테스트

1) SecurityFilterChain 설정 클래스

특정 URL에 대한 접근 권한을 제어하고 Basic 인증을 활성화하는 자바 설정 코드.

  • Whitelabel Error Page 참고: 인증에는 성공했으나 실제 서버 내에 해당 URL(예: "/")과 매핑된 컨트롤러가 존재하지 않을 때 브라우저에 나타나는 기본 에러 페이지.
  • .anyRequest().authenticated(): 이 설정만 넣게 되면 모든 URL들 인증 Basic 인증 거쳐야함.
  • return new InMemoryUserDetailsManager(): UserDetailsService를 빈에 등록하면 더 이상 콘솔에서 자동 생성된 비밀번호가 생성되지 않음. 애플리케이션은 디폴트 자동 구성 대신, 컨텍스트에 추가한 UserDetailsService 타입의 인스턴스를 사용한다. 하지만 기본 설정을 안하면 엔드포인트에 접근할 수 없다(에러는 안남). 아래의 경우 참고해서 코드 추가 필요:
    • 사용자가 없다. (유저 정보를 넘겨주지 않음)
    • PasswordEncoder가 없다.
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain configure(HttpSecurity http) throws Exception {
        // HTTP Basic 인증을 기본값으로 활성화
        http.httpBasic(Customizer.withDefaults());
        
        // URL별 접근 권한 설정
        http.authorizeHttpRequests(auth -> auth
            // "/", "/home", "/signup"은 인증 없이 누구나 접근 가능
            .requestMatchers("/", "/home", "/signup").permitAll()
            // 그 외의 모든 요청은 무조건 인증(로그인)이 필요함
            .anyRequest().authenticated()
        );
        
        return http.build();
    }
    
    @Bean
    UserDetailsService userDetailsService() {
        var user = User.withUsername("user")
                .password("12345")
                .authorities("read") // read 권한 부여
                .build();
		
        return new InMemoryUserDetailsManager(user);
	}
    
    @Bean 
    PasswordEncoder pwEncoder() {
        // 테스트 용도
        return NoOpPasswordEncoder.getInstance();
    }
    
}

2) application.yaml (선택 사항)

  • 스프링 시큐리티가 켜졌을 때 사용할 테스트용 관리자 계정을 지정할 수 있다. (기본 계정 설정)
spring:
  application:
    name: sessions
  security:
    user:
      name: admin
      password: "1234"