본문 바로가기

Spring Boot/유저 관리 기능

기본 인증, 인코딩 : 기본 인증

Basic Authentication

 

- 기본 인증의 정의, 동작 원리

- RFC 7617 표준에 따라 기본 인증이 어떻게 정의되는가?

- Authorization 헤더의 구조, 예

- 기본 인증이 동작하는 전체 과정을 단계별로 이해하기

- 기본 인증의 장점, 한계


기본 인증(Basic Authentication)

1. RFC 7617

기본 인증이 HTTP 표준 인증 방식중 하나로, RFC 7617에 정의되어 있음

사용자 이름과 비밀번호를 콜론(:, 세미콜론 x)으로 연결한 뒤, Base64로 인코딩하여 Authorization 헤더에 담아서 서버에 전달함

매우 단순한 구조 덕에 구현이 쉽지만, 보안적으로 취약한점이 있다고 함

 

(1) RFC 7617에 대한 기초

RFC 7617은 HTTP Basic Authentication Scheme에 대한 기술 표준 문서이다.

https://datatracker.ietf.org/doc/html/rfc7617

  • 정의 : 사용자의 ID와 비밀번호를 결합해서 서버에 전송하는 가장 기본적 인증 방식인 Basic 인증을 정의함
  • 동작방식
    • 아이디:비밀번호 형태의 문자열을 만듬
    • 이 문자열을 Base64로 인코딩함
    • HTTP 요청 헤더에 Authorization: Basic[...=] 이런 형태로 실어서 보냄
  • 특징
    • UTF-8을 지원함. 아이디/비밀번호에 비-ASCII 문자(한글 등)가 포함될 때 처리방식이 애매했는데, UTF-8을 사용하도록 명시하고 인코딩 처리 방식을 개선했음
  • 주의사항
    • Base64는 암호화가 아닌 인코딩이기 때문에 디코딩 될 수 있으므로 반드시 HTTPS(SSL/TLS) 환경에서 사용해야 안전하다

 

2. 개념

클라이언트 : 사용자 이름:비밀번호 - > username:password

인코딩 : Base64로 인코딩함

서버 요청 : 인코딩한 문자열을 Authorization 헤더에 담아서 전송함

GET /mypage HTTP/1.1
Host: example.com
Authorization: Basic ...=

 

Authorization 헤더 확인 -> Basic 접두사 제거 후 Base64 디코딩 -> username:password 추출

-> 서버 저장소의 자격 증명과 비교한 뒤 인증 성공 여부를 결정함

 


Authorization 헤더

1. 구조 설명

구성 요소 예시 설명
인증방식 Basic RFC 7617에서 정의한 키워드
인코딩된 자격 증명 ...= username:password를 Base64로 인코딩한 값

 

최종 형태

Authorization: Basic <Base64로 인코딩된 문자열=>

 

 

2. 동작 흐름 다이어그램

기본 인증에서 Authorization 헤더는 클라이언트가 서버에 자신의 자격 증명을 전달하는 핵심 역할을 함

  1. 사용자가 보호된 리소스에 접근을 시도함
  2. 서버는 401 Unauthorized와 함께 WWW-Authenticate: Basic realm="..." 헤더를 응답하여 인증을 요구
  3. 브라우저(클라이언트)는 사용자에게 아이디와 비밀번호 입력을 요청
  4. 입력받은 아이디와 비밀번호를 username:password 형태로 합친 후, Base64로 인코딩함
  5. 인코딩된 문자열을 Authorization: Basic <값> 형태로 요청 헤더에 담아서 서버에 다시 보낸다(브라우저->서버)
  6. 서버는 Base64 디코딩 후 사용자의 저장소와 비교하여 인증 성공 여부를 판단한다

Authorization 인가까지의 워크플로우

 


기본 인증 구현(러프하게)

User.java

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity
@Table(name = "users")
@Getter
@Setter
@NoArgsConstructor
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
}

 

UserRepository.java

public class UserRepository {
    public boolean validate(String username, String password) {
        return "user".equals(username) && "password".equals(password);
    }
}

 

 

BasicAuthHandler.java

import java.util.Base64;

public class BasicAuthHandler {
    private final UserRepository userRepository;

    public BasicAuthHandler(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    // 요청 헤더에서 Authorization을 받아서 검증함
    public boolean authenticate(String authHeader) {
        if(authHeader == null || !authHeader.startsWith("Basic ")) {
            return false; // 인증 헤더가 없음
        }

        // Basic 접두사 제거하기
        String base64Credentials = authHeader.substring("Basic ".length());

        // Base64 디코딩하기
        byte[] decodeByte = Base64.getDecoder().decode(base64Credentials);
        String credentials = new String(decodeByte);

        // 아이디와 비밀번호 분리하기
        String[] parts = credentials.split(":", 2);
        if(parts.length != 2) { // 검증로직, username:password 형태가 아니라 a:b:c 등이라면
            return false; // 분리가 잘못됐거나 뭔가 이상함
        }

        String username = parts[0];
        String password = parts[1];

        // 저장소에서 사용자를 검증함
        return userRepository.validate(username, password);
    }
}

 

 

BasicAuthHandlerTest.java

import java.util.Base64;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

class BasicAuthHandlerTest {
    private BasicAuthHandler authHandler;
    private UserRepository userRepository;

    @BeforeEach
    void setUp() {
        userRepository = mock(UserRepository.class);
        authHandler = new BasicAuthHandler(userRepository);
    }

    @Test
    void authenticate_success() {
        // Given
        String username = "B1uffer";
        String password = "password123";
        String credentials = username + ":" + password;
        String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes());
        String authHeader = "Basic " + encodedCredentials;

        System.out.println("credentials : " + credentials);
        System.out.println("authHeader : " + authHeader);

        when(userRepository.validate(username, password)).thenReturn(true);

        // when
        boolean result = authHandler.authenticate(authHeader);
        System.out.println("result : " + result);
        // then
        assertTrue(result);
        verify(userRepository).validate(username, password);
    }

    @Test
    void authenticate_fail_password() {
        //Given
        String username = "B1uffer";
        String password = "password123";
        String credentials = username + ":" + password;
        String authHeader = "Basic " + Base64.getEncoder().encodeToString(credentials.getBytes());
        when(userRepository.validate("B1uffer", "wrongpassword")).thenReturn(false);
        System.out.println("credentials : " + credentials);

        // when
        boolean result = authHandler.authenticate(authHeader);
        System.out.println("result : " + result);

        // then
        assertFalse(result);
    }

    @Test
    void authenticate_fail_header() {
        // 필터링에 대해서 테스트함
        assertFalse(authHandler.authenticate(null));
        assertFalse(authHandler.authenticate("Bearer anyToken"));
    }

    @Test
    void authenticate_fail_credentials() {
       // Base64의 형식이 맞지 않는 경우, : 빠져있음
        String authHeader = "Basic " + Base64.getEncoder().encodeToString("B1ufferPassword123".getBytes());
        when(userRepository.validate("B1uffer", "password123")).thenReturn(false);

        // when
        boolean result = authHandler.authenticate(authHeader);

        // then
        assertFalse(result);
    }
}

 

 


기본 인증의 한계와 보안 위험성

  • 인코딩은 암호화가 아님 : Base64는 단순 인코딩일 뿐, 보안 기능이 전혀 없다
  • 재전송 위험 : 요청마다 자격 증명이 반복적으로 전송되므로 탈취의 위험이 있다
  • 저장소 문제 : 서버는 사용자의 비밀번호를 안전하게 저장해야하며(해시, 솔트 등), 그렇지 않으면 유출의 위험이 있다
  • 현대적 대체 수단이 필요함 : 실제 서비스에서는 세션 기반 인증이나 토큰 기반 인증(JWT 등)이 일반적으로 사용됨

 


정리

항목 설명
RFC 7617 기본 인증을 정의한 공식 표준
Authorization 헤더 Basic username:password를 Base64로 인코딩한 형태
장점 단순하고, 표준 지원이 넓음
단점 보안에 취약하고(노출 위험), 매 요청시 전송됨