HTTP basic authentication

기본 인증(Basic Authentication)은 웹 애플리케이션과 API에서 사용되는 가장 단순하고 오래된 HTTP 인증 방식 중 하나이다. 이 인증 방식은 1996년에 발표된 HTTP/1.0 명세의 일부로 처음 소개되었으며, 현재까지도 많은 시스템에서 활용되고 있다. 간단한 구조와 광범위한 지원으로 인해 여전히 중요한 인증 메커니즘으로 남아 있다.

기본 인증의 작동 원리

기본 인증은 매우 직관적인 프로세스를 따른다:

  1. 요청 시도: 클라이언트가 보호된 리소스에 접근을 시도한다.

  2. 인증 요구: 서버는 리소스가 보호되어 있음을 인식하고 상태 코드 401 (Unauthorized)와 함께 응답한다. 이 응답에는 다음과 같은 헤더가 포함된다.

    1
    
    WWW-Authenticate: Basic realm="접근 영역 설명"
    

    여기서 “realm"은 보호 영역을 설명하는 문자열로, 사용자에게 어떤 자격 증명을 사용해야 하는지 힌트를 제공한다.

  3. 자격 증명 제공: 클라이언트는 사용자 이름과 비밀번호를 콜론(:)으로 결합하고, Base64로 인코딩하여 다음 형식의 Authorization 헤더를 요청에 포함시킨다:

    1
    
    Authorization: Basic {Base64(username:password)}
    
  4. 인증 검증: 서버는 Authorization 헤더에서 Base64 인코딩된 문자열을 디코딩하여 사용자 이름과 비밀번호를 추출하고, 이를 저장된 자격 증명과 비교하여 인증을 검증한다.

  5. 접근 허용 또는 거부: 자격 증명이 유효하면 서버는 요청을 처리하고 응답을 반환한다. 유효하지 않으면 다시 401 상태 코드를 반환한다.

기본 인증의 구현 예시

클라이언트 측 구현

브라우저를 통한 기본 인증

브라우저는 기본 인증을 기본적으로 지원한다. 서버가 401 응답을 반환하면, 브라우저는 자동으로 사용자 이름과 비밀번호를 요청하는 대화 상자를 표시한다.

프로그래밍 방식의 기본 인증 (JavaScript)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 사용자 이름과 비밀번호를 Base64로 인코딩
function encodeCredentials(username, password) {
  return btoa(`${username}:${password}`);
}

// 기본 인증을 사용한 API 요청
async function fetchWithBasicAuth(url, username, password) {
  const encodedCredentials = encodeCredentials(username, password);
  
  try {
    const response = await fetch(url, {
      headers: {
        'Authorization': `Basic ${encodedCredentials}`
      }
    });
    
    if (!response.ok) {
      if (response.status === 401) {
        throw new Error('인증 실패: 사용자 이름 또는 비밀번호가 잘못되었습니다.');
      }
      throw new Error(`요청 실패: ${response.status}`);
    }
    
    return await response.json();
  } catch (error) {
    console.error('요청 오류:', error);
    throw error;
  }
}

// 사용 예시
fetchWithBasicAuth('https://api.example.com/resource', 'username', 'password')
  .then(data => console.log('데이터:', data))
  .catch(error => console.error('오류:', error));

서버 측 구현

Node.js/Express 서버에서의 기본 인증
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
const express = require('express');
const app = express();

// 기본 인증 미들웨어
function basicAuth(req, res, next) {
  // Authorization 헤더 확인
  const authHeader = req.headers.authorization;
  
  if (!authHeader) {
    // 인증 헤더가 없으면 인증 요구
    res.setHeader('WWW-Authenticate', 'Basic realm="접근하려면 로그인하세요"');
    return res.status(401).send('인증이 필요합니다');
  }
  
  // Basic 인증 스키마 확인
  if (!authHeader.startsWith('Basic ')) {
    return res.status(401).send('지원되지 않는 인증 방식입니다');
  }
  
  // Base64 인코딩된 자격 증명 디코딩
  const base64Credentials = authHeader.split(' ')[1];
  const credentials = Buffer.from(base64Credentials, 'base64').toString('utf-8');
  const [username, password] = credentials.split(':');
  
  // 자격 증명 검증 (실제로는 데이터베이스 등에서 검증)
  if (username === 'admin' && password === 'secret') {
    // 인증 성공 시 요청 객체에 사용자 정보 추가
    req.user = { username };
    next();
  } else {
    // 인증 실패
    res.setHeader('WWW-Authenticate', 'Basic realm="접근하려면 로그인하세요"');
    res.status(401).send('잘못된 사용자 이름 또는 비밀번호');
  }
}

// 모든 라우트에 기본 인증 적용
app.use(basicAuth);

// 보호된 라우트
app.get('/api/protected', (req, res) => {
  res.json({
    message: '인증된 접근 성공',
    user: req.user.username
  });
});

app.listen(3000, () => {
  console.log('서버가 3000번 포트에서 실행 중입니다');
});
Apache 웹 서버에서의 기본 인증 설정

Apache에서는 .htaccess 파일을 사용하여 기본 인증을 설정할 수 있다:

1
2
3
4
5
# 기본 인증 활성화
AuthType Basic
AuthName "제한된 영역"
AuthUserFile /path/to/.htpasswd
Require valid-user

그리고 htpasswd 도구를 사용하여 비밀번호 파일을 생성할 수 있다:

1
htpasswd -c /path/to/.htpasswd username

기본 인증의 장점

  1. 단순성: 구현이 매우 간단하고 직관적.
  2. 호환성: 모든 브라우저와 HTTP 클라이언트에서 지원.
  3. 표준화: HTTP 프로토콜의 공식 부분으로, 널리 채택되고 이해되고 있다.
  4. 무상태(Stateless): 각 요청에 인증 정보가 포함되므로 서버 측에서 세션 상태를 유지할 필요가 없다.
  5. 프록시 및 중개자 호환성: 대부분의 프록시와 HTTP 중개자가 이 방식을 이해하고 처리할 수 있다.

기본 인증의 단점 및 보안 위험

  1. 자격 증명 전송: 비밀번호가 요청마다 전송되며, Base64 인코딩은 암호화가 아닌 인코딩 방식이므로 중간자 공격에 취약하다.
  2. HTTPS 필수: 보안을 위해서는 반드시 HTTPS와 함께 사용해야 한다.
  3. 자격 증명 저장: 브라우저는 일반적으로 사용자 세션 동안 자격 증명을 저장하므로, 공유 컴퓨터에서는 보안 위험이 있다.
  4. 로그아웃 메커니즘 부재: 표준화된 로그아웃 메커니즘이 없어, 브라우저를 닫거나 다른 자격 증명으로 인증하는 방법밖에 없다.
  5. 제한된 보안 기능: 다단계 인증, 접근 제어 목록, 토큰 갱신 등의 고급 보안 기능을 제공하지 않는다.
  6. 브루트 포스 공격에 취약: 요청 제한이나 계정 잠금 메커니즘이 없으면 무차별 대입 공격에 취약하다.

보안 강화 방법

기본 인증을 사용할 때 취할 수 있는 보안 강화 조치:

  1. HTTPS 필수 사용: 모든 인증 통신은 TLS/SSL을 통해 암호화되어야 한다.

  2. 강력한 비밀번호 정책: 복잡하고 길며 예측하기 어려운 비밀번호를 요구한다.

  3. 요청 제한(Rate Limiting): 특정 IP 또는 사용자의 과도한 인증 시도를 제한한다.

  4. 보안 헤더 추가: 추가적인 HTTP 보안 헤더를 사용하여 중간자 공격, 클릭재킹 등을 방지한다.

    1
    2
    3
    
    Strict-Transport-Security: max-age=31536000; includeSubDomains
    X-Content-Type-Options: nosniff
    X-Frame-Options: DENY
    
  5. 접근 제한: 기본 인증을 특정 IP 범위나 내부 네트워크에서만 사용하도록 제한한다.

기본 인증의 실제 사용 사례

기본 인증은 다음과 같은 상황에서 적절하게 사용될 수 있다:

  1. 내부 도구 및 관리 인터페이스: 조직 내부에서만 사용되는 도구나 관리 페이지.
  2. 개발 환경 및 스테이징 서버: 공개되지 않은 개발 중인 웹사이트나 API.
  3. 간단한 API 액세스: 복잡한 인증이 필요하지 않은 간단한 API.
  4. 레거시 시스템 통합: 현대적인 인증 방식을 지원하지 않는 오래된 시스템과의 통합.
  5. 프록시 서버 인증: 리버스 프록시에서의 간단한 인증 계층.

코드 예시: 다양한 언어에서의 기본 인증 구현

Python (Flask)에서의 기본 인증 구현

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from flask import Flask, request, Response
import base64

app = Flask(__name__)

# 사용자 데이터베이스 (실제로는 DB 사용)
USERS = {'admin': 'password'}

def check_auth(username, password):
    """사용자 이름과 비밀번호 검증"""
    return username in USERS and USERS[username] == password

def authenticate():
    """인증 요구 응답 생성"""
    return Response(
        'Could not verify your access level for that URL.\n'
        'You have to login with proper credentials', 401,
        {'WWW-Authenticate': 'Basic realm="Login Required"'})

def requires_auth(f):
    """기본 인증을 요구하는 데코레이터"""
    def decorated(*args, **kwargs):
        auth = request.authorization
        if not auth or not check_auth(auth.username, auth.password):
            return authenticate()
        return f(*args, **kwargs)
    return decorated

@app.route('/api/protected')
@requires_auth
def protected_resource():
    """인증이 필요한 보호된 리소스"""
    return {'message': '인증 성공!', 'user': request.authorization.username}

if __name__ == '__main__':
    app.run(debug=True)

Java (Spring Boot)에서의 기본 인증 구현

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeRequests(authorizeRequests ->
                authorizeRequests
                    .antMatchers("/public/**").permitAll()
                    .anyRequest().authenticated()
            )
            .httpBasic();
        
        return http.build();
    }

    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails user = User.builder()
            .username("admin")
            .password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW") // "password"
            .roles("ADMIN")
            .build();
            
        return new InMemoryUserDetailsManager(user);
    }
}

실제 사용 시나리오: 기본 인증의 현대적 적용

CI/CD 파이프라인에서의 적용

젠킨스(Jenkins), 깃랩(GitLab) 등의 CI/CD 도구는 종종 기본 인증을 사용하여, 빌드 파이프라인의 특정 웹훅이나 API 엔드포인트를 보호한다. 이는 간단하면서도 효과적인 방법으로, 내부 시스템 간의 통신에 적합하다.

1
2
3
# Jenkins 웹훅 호출 예시
curl -X POST https://jenkins.example.com/job/build-project/build \
     -u username:api_token

개발 및 스테이징 환경 보호

개발 중인 웹사이트나 애플리케이션은 종종 기본 인증을 사용하여 무단 접근으로부터 보호된다. 이는 개발 중인 기능이 공개되거나 검색 엔진에 인덱싱되는 것을 방지한다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Nginx 설정 예시
server {
    listen 80;
    server_name dev.example.com;
    
    auth_basic "Development Environment";
    auth_basic_user_file /etc/nginx/.htpasswd;
    
    location / {
        proxy_pass http://localhost:3000;
    }
}

API 키 전송

일부 API는 API 키를 사용자 이름으로, 비밀 값을 비밀번호로 사용하는 기본 인증 방식을 통해 간단한 API 키 인증을 구현한다.

1
2
3
4
5
6
7
# Python에서 API 키를 사용한 기본 인증 요청
import requests

response = requests.get(
    'https://api.example.com/data',
    auth=('api_key_12345', '')  # 비밀번호 부분은 빈 문자열
)

기본 인증의 현대적 대안

현대 웹 애플리케이션에서 기본 인증은 점차 다음과 같은 방식으로 대체되고 있다:

  1. Bearer 토큰 인증: JWT(JSON Web Tokens)와 같은 서명된 토큰을 사용한 인증 방식.
  2. OAuth 2.0: 서비스 간 권한 위임을 위한 표준 프로토콜.
  3. API 키: 특히 서버 간 통신에서 널리 사용된다.
  4. HMAC 인증: 요청 메시지의 해시 기반 메시지 인증 코드를 사용하는 방식.
  5. SSO(Single Sign-On): 사용자가 한 번의 인증으로 여러 서비스에 접근할 수 있게 해주는 인증 시스템.

용어 정리

용어설명

참고 및 출처