포스트

Basic Auth와 세션 인증은 무엇이 다른가

Basic Auth와 세션 인증은 무엇이 다른가

관리자 기능과 내부 API를 보호해야 하는 상황에서 가장 빠르게 손이 가는 선택은 Basic Auth였다. 설정 한 줄로 인증을 걸 수 있고, 로그인 화면도 세션 저장소도 필요 없다. 실제로 잘 동작했다.

문제는 그다음이었다. 이 API를 관리자 페이지 형태로 노출하면서 로그인 상태가 유지되고 로그아웃 버튼을 누르면 인증이 끊기고 일정 시간이 지나면 저절로 만료되는 동작이 필요해졌다. Basic Auth로는 이 요구사항을 자연스럽게 만들 수 없었다.

그리고 한동안 엉뚱한 곳을 뒤졌다. 배포 파이프라인을 손보던 시기와 겹쳐서, 이 인증 방식 변경이 GitHub Actions 워크플로우나 서버 환경변수 설정에서 처리할 수 있는 문제인지 헷갈렸다. 결론부터 적으면 아니었다. 이건 Spring Security 설정과 애플리케이션 인증 구조를 바꿔야 하는 문제였고, 배포 도구는 그 변경된 코드를 서버로 옮겨주는 역할만 한다.

이번 글은 그 과정에서 정리한 내용이다.


1. Basic Auth를 사용하게 된 배경

Basic Auth를 선택한 이유는 단순하다.

  • 별도의 로그인 화면이 필요 없다. 브라우저가 기본 인증 팝업을 띄워준다.
  • 세션 저장소, 쿠키 설정, 로그인 API가 없어도 된다.
  • HTTP 표준 인증 방식이라 curl, Postman, 모니터링 스크립트에서 바로 쓸 수 있다.

내부 도구나 헬스 체크용 엔드포인트, 배치 트리거 API처럼 사람이 UI로 오래 머무르지 않는 대상에는 이 단순함이 그대로 장점이 된다.

하지만 일반 사용자나 운영자가 브라우저로 접속해서 여러 화면을 오가는 상황에서는 이야기가 달라진다. Basic Auth에는 로그인이라는 시점이 없고, 따라서 로그아웃이라는 시점도 없다. 브라우저가 자격 증명을 캐시하고 있는 동안에는 계속 인증된 상태처럼 보이고 그 캐시를 애플리케이션이 통제할 방법이 마땅치 않다.


2. Basic Auth의 동작 방식

Basic Auth는 요청마다 자격 증명을 헤더에 실어 보낸다.

1
Authorization: Basic base64(username:password)

클라이언트는 username:password 문자열을 Base64로 인코딩해 이 헤더에 넣는다. 서버는 요청이 들어올 때마다 헤더를 디코딩해 아이디와 비밀번호를 확인하고 맞으면 통과시킨다. 서버는 이 사용자가 아까 로그인했다는 사실을 기억하지 않는다. 매 요청이 독립적으로 검증된다.

Base64는 암호화가 아니다

가장 자주 오해가 생기는 대목이다. dXNlcjpwYXNzd29yZA== 같은 문자열은 사람 눈에 읽히지 않으니 암호화된 것처럼 보인다. 하지만 Base64는 데이터를 표현하는 인코딩 방식이다. 바이너리나 특수문자를 포함한 데이터를 HTTP 헤더에 안전하게 담으려는 변환일 뿐, 비밀을 지켜주지는 않는다.

1
2
echo -n 'admin:1234' | base64          # YWRtaW46MTIzNA==
echo -n 'YWRtaW46MTIzNA==' | base64 -d # admin:1234

키도 비밀도 필요 없다. 값을 손에 넣은 사람은 누구나 원래 문자열로 되돌릴 수 있다. Basic Auth 헤더는 사실상 평문 비밀번호를 매 요청에 실어 보내는 것과 같다.

그래서 Basic Auth를 쓸 때 HTTPS는 선택이 아니라 전제 조건이다. HTTP로 통신하면 중간에서 트래픽을 관찰할 수 있는 누군가가 비밀번호 자체를 그대로 얻는다. 세션 ID가 탈취되면 그 세션만 무효화하면 되지만 비밀번호가 유출되면 계정 자체를 잃는다. 노출되는 값의 무게가 다르다.


3. 세션 인증의 동작 방식

세션 인증은 인증 시점과 이후 요청을 분리한다.

1
2
3
4
5
6
1. 사용자가 아이디와 비밀번호로 로그인 요청
2. 서버가 사용자 정보를 검증
3. 서버가 세션을 생성
4. 세션 ID를 쿠키로 클라이언트에 전달
5. 이후 요청마다 클라이언트가 세션 ID 쿠키를 전송
6. 서버가 세션 ID를 통해 사용자를 식별

핵심은 4번과 5번 사이에서 일어나는 교체다. 아이디와 비밀번호는 최초 로그인 요청에서 한 번만 전달되고 그 이후의 모든 요청은 세션 ID로 처리된다. 서버는 세션 저장소에 “이 세션 ID는 어떤 사용자이고, 언제 만들어졌고, 언제 만료되는가”를 기록해두고 요청이 올 때마다 조회한다.

이 구조라면 Basic Auth로는 어려웠던 일을 그냥 할 수 있다.

  • 로그아웃: 서버가 해당 세션을 삭제하면 그 즉시 인증이 끊긴다.
  • 만료: 세션에 유효 시간을 두면 일정 시간 뒤 자동으로 인증이 풀린다.
  • 강제 로그아웃: 관리자가 특정 사용자의 세션을 지울 수 있다.

인증 상태의 주도권이 클라이언트가 아니라 서버에 있다는 점이 Basic Auth와의 가장 큰 차이다.

한 가지는 분명히 해두는 게 좋다. 세션 ID는 비밀번호가 아니라서 안전한 값이 아니다. 세션 ID를 탈취한 사람은 비밀번호를 몰라도 인증된 요청을 보낼 수 있다. 세션 ID도 비밀번호와 같은 급으로 다뤄야 하는 인증 정보다. 그래서 뒤에서 다룰 쿠키 보안 설정이 따라붙는다.

Basic Auth는 매 요청에 아이디와 비밀번호를 실어 보내고 서버는 상태를 저장하지 않는 흐름, 세션 인증은 최초 1회 로그인 후 세션 ID 쿠키만 주고받고 서버가 세션 저장소를 두는 흐름 비교


4. Stateless와 Stateful의 차이

두 방식은 서버가 무엇을 기억하느냐에서 갈린다.

Basic Auth는 요청마다 필요한 정보가 요청 안에 다 들어 있다. 서버는 로그인 상태를 저장하지 않고, 헤더만 보고 판단한다. 서버를 재시작해도, 요청이 다른 서버로 흘러가도 결과가 같다. Stateless에 가까운 방식이다.

세션 인증은 서버가 세션 정보를 보관한다. 요청에 담긴 세션 ID는 저장소를 조회하는 열쇠일 뿐이고, 실제 인증 상태는 서버 쪽에 있다. 인증 상태에 수명과 생애주기가 생기므로 Stateful 방식이다.

Stateless가 언제나 나은 선택은 아니다. Stateless는 서버 확장이 쉽고 저장소 의존이 없는 대신, 서버가 인증 상태를 통제하지 못한다. 로그아웃이나 강제 만료처럼 지금 이 인증을 끊으라는 요구는 어딘가에 상태가 있어야 처리할 수 있다. 반대로 Stateful은 통제권을 얻는 대신 저장소와 확장 전략이라는 비용을 진다.

Spring Security가 요청을 어떤 순서로 처리하고 인증 정보를 어디에 담아두는지는 Spring Security 요청 처리 흐름과 DispatcherServlet에서 따로 정리한 적이 있는데, 상태를 어디에 두는 문제는 결국 그 흐름 어디에 개입할지를 정하는 일이기도 하다.


5. Basic Auth와 세션 인증 비교

항목Basic Auth세션 인증
인증 정보 전달 방식요청마다 Authorization 헤더에 아이디·비밀번호(Base64)최초 1회 아이디·비밀번호, 이후 세션 ID 쿠키
서버의 인증 상태 저장저장하지 않음세션 저장소에 저장
로그인 상태 유지브라우저의 자격 증명 캐시에 의존서버 세션과 쿠키 만료 정책으로 관리
로그아웃 처리표준적인 방법이 없음(브라우저 캐시 통제 어려움)세션 무효화로 즉시 처리
인증 만료 처리없음(비밀번호를 바꾸는 것 외에는)세션 타임아웃, 유휴 시간 만료 설정
서버 확장 시 고려사항사실상 없음세션 공유 전략 필요(Sticky Session, 공용 저장소)
주요 사용 사례내부 도구, 간단한 API, 테스트·모니터링 스크립트일반 웹 로그인, 관리자 페이지
구현 복잡도낮음중간(로그인 API, 쿠키, CSRF, 저장소)
보안상 주의점HTTPS 필수, 비밀번호가 매 요청에 노출세션 ID 탈취, CSRF, 쿠키 속성 설정

표를 채워보고 나서 든 생각은 세션 인증이 더 좋다는 게 아니었다. 두 방식은 목적이 다르다.

Basic Auth는 인증을 거는 비용이 거의 없다는 게 본질적인 장점이다. 스크립트가 호출하는 내부 API, 배포 직후 확인용 엔드포인트, 로컬·테스트 환경 보호에는 여전히 합리적인 선택이다. 세션 인증은 사람이 브라우저로 들어와 머무르는 화면, 즉 로그인·로그아웃·만료라는 개념이 필요한 서비스에 맞는 방식이다.

내 경우 문제는 Basic Auth 자체가 아니라, 사람이 쓰는 화면에 스크립트용 인증을 붙여둔 데 있었다.


6. 세션 인증으로 전환할 때 고려할 사항

세션 인증으로 옮기면 로그인 API 하나 만들고 끝나지 않는다. 인증 정보가 헤더에서 쿠키로 옮겨가면서 함께 딸려오는 것들이 있다.

쿠키 보안 설정

세션 ID는 쿠키에 담기므로, 쿠키 속성이 곧 세션 ID의 방어선이 된다.

속성역할줄여주는 위험
HttpOnlyJavaScript에서 쿠키에 접근하지 못하게 한다XSS로 스크립트가 삽입되더라도 세션 ID를 읽어 외부로 빼내는 경로를 막는다
SecureHTTPS 연결에서만 쿠키를 전송한다평문 HTTP 요청에 쿠키가 실려 네트워크에서 관찰되는 상황을 막는다
SameSite다른 사이트에서 시작된 요청에 쿠키를 붙일지 제어한다외부 사이트가 사용자의 쿠키를 이용해 요청을 보내는 CSRF 위험을 줄인다

SameSite는 세 값의 의미를 구분해서 이해해야 한다. Strict는 외부 사이트에서 시작된 요청에 쿠키를 아예 붙이지 않고, Lax는 최상위 내비게이션 GET 정도만 허용하며, None은 제한을 두지 않는 대신 Secure가 필수다. 프론트엔드와 백엔드의 도메인이 다르면 None이 필요해지는 경우가 있는데, 그만큼 CSRF 대비를 별도로 챙겨야 한다.

CSRF

세션 쿠키는 편리한 이유와 위험한 이유가 같다. 브라우저가 쿠키를 자동으로 붙여준다. 개발자가 매번 토큰을 챙겨 넣지 않아도 인증이 유지되는 이유이기도 하고, 공격자가 만든 페이지에서 우리 서버로 요청을 보냈을 때도 쿠키가 함께 전송되는 이유이기도 하다. 사용자가 로그인해 있는 상태라면 그 요청은 정상 요청처럼 처리된다.

Spring Security는 그래서 CSRF 보호를 기본으로 켜둔다. 쿠키 기반 인증을 쓰는 서버에서는 CSRF가 예외적인 위협이 아니라 기본 조건에 가깝다. 그래서 로컬에서 POST 요청이 403으로 막힌다고 해서 반사적으로 csrf().disable()을 넣기 전에, 지금 만드는 게 어떤 서비스인지 확인하는 게 순서다.

  • 브라우저 쿠키로 인증하는 서비스라면 CSRF 보호를 유지하고 토큰을 함께 보내도록 만든다.
  • 쿠키를 쓰지 않고 헤더 기반 토큰만 사용하는 API라면 비활성화가 타당할 수 있다.

비활성화는 취향으로 고를 게 아니라 인증 정보가 어디에 실려 오는지를 보고 정한다.

[관련 포스팅: CSRF 보호와 Spring Security 설정]

CORS와 쿠키 전달

프론트엔드와 백엔드의 출처(origin)가 다르면 쿠키가 자동으로 오가지 않는다. 다음 설정이 함께 맞아야 한다.

  • 프론트엔드 요청에 자격 증명 포함 옵션: fetch(url, { credentials: 'include' })
  • 서버 응답 헤더: Access-Control-Allow-Credentials: true
  • 허용 Origin을 와일드카드가 아니라 구체적인 값으로 명시
  • 쿠키의 SameSite=NoneSecure 설정

이 조합에서 자주 막힌다. Access-Control-Allow-Origin: *와 자격 증명 포함 요청은 함께 쓸 수 없다. 브라우저는 credentials: 'include' 요청에 대해 와일드카드 응답을 거부하고, 응답을 받아도 사용하지 않는다. 아무 출처에나 쿠키를 실어 보내는 상황을 막기 위한 규칙이다. 따라서 https://admin.example.com처럼 허용할 출처를 명시해야 한다.

로컬 개발 환경에서 이 조합이 특히 까다롭다. Secure 쿠키는 HTTPS를 요구하는데 로컬은 보통 HTTP이고, SameSite=NoneSecure를 요구한다. 개발용 프록시로 출처를 같게 맞추는 방식이 결과적으로 설정을 단순하게 만드는 경우가 많았다.

[관련 포스팅: CORS 설정과 Preflight 요청]

세션 저장소

단일 서버라면 세션을 서버 메모리에 두는 기본 동작으로도 충분하다. 서버가 여러 대로 늘어나면 이야기가 달라진다.

사용자의 첫 번째 요청이 로드밸런서를 거쳐 서버 A로 갔고, 서버 A가 메모리에 세션을 만들었다. 다음 요청은 서버 B로 갈 수 있다. 서버 B의 메모리에는 그 세션 ID에 대한 기록이 없으므로 인증되지 않은 요청으로 취급된다. 사용자 입장에서는 멀쩡히 쓰다가 갑자기 로그인 화면으로 튕기는 현상으로 나타나고, 요청이 어느 서버로 갔는지에 따라 재현이 되기도 하고 안 되기도 한다.

해결 방향은 크게 셋이다.

  • Sticky Session: 로드밸런서가 같은 사용자를 항상 같은 서버로 보낸다. 애플리케이션을 고치지 않아도 되지만, 그 서버가 재시작되거나 빠지면 세션이 사라진다.
  • Redis 기반 공용 세션 저장소: 세션을 서버 밖 공용 저장소에 둔다. 어느 서버가 받아도 동일하게 인증되고 재시작에도 세션이 유지되지만, 저장소가 새로운 의존성이자 장애 지점이 된다. 그래서 저장소 자체의 가용성도 함께 생각해야 하는데, 그 이야기는 단일 Redis 장애에서 Sentinel까지에서 정리한 내용과 이어진다.
  • Spring Session: 위 구조를 애플리케이션 코드 변경을 최소화하면서 적용하게 해주는 추상화다. 의존성과 설정을 추가하면 HttpSession을 그대로 쓰면서 저장 위치를 Redis 등으로 바꿀 수 있다.

지금 단계에서 세 방법의 우열을 따질 필요는 없었다. 중요한 건 세션 인증을 고르면 서버 확장 시점에 세션 저장 전략을 반드시 정해야 한다는 사실을 미리 알아두는 일이었다.

로드밸런서가 요청을 세 대의 애플리케이션 서버로 분산하고 세 서버가 모두 공용 Redis 세션 저장소를 참조하는 구조, 각 서버의 로컬 메모리 세션은 사용하지 않음


7. Spring Security에서는 무엇이 달라지는가

두 방식의 차이는 Spring Security 설정에서 개발자가 떠안는 책임의 양으로 갈린다.

Basic Auth는 설정 한 줄로 끝난다.

1
http.httpBasic(Customizer.withDefaults());

인증 정보 파싱, 검증, 실패 응답을 프레임워크가 다 처리한다. 개발자가 정의할 게 사실상 없다.

세션 인증에서는 필요한 요소가 늘어난다.

  • 로그인 API 또는 로그인 폼 (인증이 시작되는 지점)
  • AuthenticationManager (자격 증명 검증 위임)
  • 사용자 조회와 비밀번호 검증 (UserDetailsService, PasswordEncoder)
  • 인증 성공 결과를 담는 SecurityContext
  • 세션 생성 및 유지 정책
  • 로그아웃 처리 (세션 무효화, 쿠키 삭제)
  • 인증이 필요한 URL의 인가 설정

설정으로 옮기면 대략 이런 형태가 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/login").permitAll()
            .requestMatchers("/admin/**").hasRole("ADMIN")
            .anyRequest().authenticated()
        )
        .formLogin(form -> form.loginProcessingUrl("/login"))
        .logout(logout -> logout
            .logoutUrl("/logout")
            .invalidateHttpSession(true)
            .deleteCookies("JSESSIONID")
        )
        .sessionManagement(session -> session
            .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
        );
    return http.build();
}

전체 구현을 옮길 생각은 없다. 이 코드에서 보고 싶었던 건 흐름의 차이다. Basic Auth에서는 매 요청이 곧 인증이라 인증 시점이 따로 없다. 세션 인증에서는 로그인이라는 진입점, 인증 상태를 담아두는 곳, 상태를 끝내는 로그아웃 지점이 각각 필요하다. 인증이 한 번의 판정에서 생애주기를 가진 상태로 바뀐다.

참고로 인증 처리가 요청 흐름의 어느 단계에서 일어나는지는 Filter를 이해하면서 정리한 생각 — 인증은 어디서 시작되는가에서 정리해둔 내용과 겹친다. Basic Auth든 세션이든 결국 필터 체인 안에서 처리된다는 점은 같고, 달라지는 건 그 안에서 무엇을 읽고 어디에 상태를 두는지다.


8. GitHub Actions와는 왜 별개의 문제인가

처음에 헤맸던 부분을 정리해둔다.

GitHub Actions는 배포 과정을 자동화하는 도구다. 하는 일은 이런 순서다.

1
2
3
4
5
코드 체크아웃
→ 빌드
→ 테스트
→ 서버에 파일 전달
→ 애플리케이션 재시작

반면 Basic Auth를 세션 인증으로 바꾸는 작업은 다음 영역에 속한다.

1
2
3
4
5
6
Spring Security 설정 변경
로그인 API 구현
세션 생성 및 저장
쿠키 설정
로그아웃 처리
CSRF 및 CORS 설정

두 목록에 겹치는 항목이 하나도 없다. 워크플로우 YAML을 어떻게 고쳐도 로그인 API가 생기지 않고, 세션 저장소가 붙지 않으며, 쿠키에 HttpOnly가 붙지 않는다. 인증 구조는 애플리케이션 코드와 설정에 있고, GitHub Actions는 그렇게 바뀐 코드를 서버로 옮겨 실행시키는 역할을 한다.

배포 도구가 관여하는 자리가 아예 없는 것은 아니다. 세션 저장소로 Redis를 쓴다면 접속 정보 같은 환경별 설정 주입은 배포 영역의 일이다. 다만 그건 인증 구조를 바꾸는 일이 아니라, 바뀐 구조가 각 환경에서 동작하게 만드는 일이다. 파이프라인이 초록불인데 애플리케이션은 정상 동작하지 않는 상황도 이 경계를 헷갈릴 때 생기는데, 비슷한 착각을 GitHub Actions는 성공했는데 애플리케이션이 실행되지 않은 이유에서도 한 번 겪었다.

증상이 배포 이후에 보인다고 해서 원인이 배포 설정에 있는 것은 아니다. 이번 건은 증상을 처음 마주친 위치가 아니라, 그 동작을 정의한 자리를 찾아야 했다.


9. JWT와의 관계

세션 인증을 정리하다 보면 JWT를 안 볼 수 없다. 짧게만 적어둔다.

세션은 서버가 인증 상태를 보관하고 세션 ID로 그 상태를 찾는 방식이다. JWT는 인증 정보를 토큰 자체에 담아, 서버가 상태를 저장하지 않고도 서명 검증만으로 요청을 처리하게 하는 방식이다. 서버의 상태 저장 부담을 줄이는 방향이다.

다만 JWT가 세션보다 무조건 낫다고 볼 수는 없다. 서버가 상태를 갖지 않기 때문에 발급된 토큰을 중간에 폐기하기 어렵고, 만료·갱신(Refresh Token)·탈취 대응 같은 문제를 별도로 설계해야 한다. 상태를 없앤 대가로 다른 복잡성이 생기는 셈이다. 이 부분은 세션 기반 인증에서 JWT 기반 인증으로 전환하면서 정리한 것들JWT 인증 필터에서 검증 책임을 분리한 이유에서 다뤄본 주제라 여기서는 여기까지만 둔다.


10. 학습 후 생각

여기부터는 사실 정리가 아니라 개인적인 감상이다.

처음에는 Basic Auth와 세션 인증의 차이를 보안 수준의 차이로 생각했다. Basic Auth는 허술하고 세션은 제대로 된 방식이라는 식의 이해였다. 정리해보니 실제 차이는 인증 정보를 어떻게 전달하고 상태를 어디서 관리하는가라는 구조의 문제였다. 매 요청에 자격 증명을 보내느냐, 한 번 검증하고 그 상태를 서버가 들고 있느냐의 차이다.

인증 방식 변경이 배포 설정 문제가 아니라는 것도 이번에 확실히 정리됐다. 배포 파이프라인을 만지던 시기와 겹치면 모든 문제가 파이프라인 문제처럼 보인다. 하지만 로그인과 로그아웃이 존재하는지는 애플리케이션 설계와 Spring Security 설정이 결정한다.

Basic Auth를 쓸 이유가 없어진 것도 아니었다. 스크립트가 호출하는 단순한 내부 API에는 여전히 가장 저렴한 선택이다. 로그인 상태와 로그아웃이 필요한 웹 서비스에 붙이려니 맞지 않았을 뿐이다.

세션 방식을 고르면 함께 따라오는 항목이 있다는 점도 미리 알아둘 만하다. 쿠키 속성, CSRF, CORS, 세션 저장소는 나중에 붙이는 옵션이 아니라 세션 인증을 선택하는 순간 세트로 딸려오는 결정들이다. 이걸 모르고 시작하면 “로그인은 되는데 배포하니 세션이 자꾸 끊긴다” 같은 문제를 뒤늦게 만나게 된다.

결국 인증 방식은 구현이 얼마나 쉬운지로 고를 게 아니었다. 누가 어떤 방식으로 이 기능을 사용하는지, 서버를 몇 대로 운영할 것인지가 기준이었다. 편리함은 그다음이다.


참고할 만한 이전 글

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.