JMeter 소스 코드 뜯어보기: 왜 로컬 테스트에서 Secure 쿠키 전송이 실패할까?

최근 Keycloak 기반의 SSO(Single Sign-On) 연동 부하 테스트를 위해 JMeter 시나리오를 작성 업무를 맡게 되었다.

OIDC(OpenID Connect), OAuth2.0과 같이 널리 알려진 표준 인증 흐름을 따르는 시나리오라 큰 어려움이 없을 것이라 예상했으나, 로그인 시나리오 단계부터 예상치 못한 난관에 봉착했다.

분명 브라우저(Chrome) 환경에서는 문제없이 작동하는 로그인이, JMeter에서는 인증 요청 시 쿠키를 누락하며 실패하는 현상이 발생한 것이다. 응답 헤더로 Set-Cookie가 정상적으로 넘어오는 것을 확인했고, JMeter의 HTTP Cookie Manager까지 등록했음에도 불구하고 요청 헤더에는 쿠키가 실리지 않았다.

결론부터 말하자면, 이 문제는 JMeter가 의존하는 라이브러리중 하나인 Apache HttpClient의 RFC 스펙 준수 & Secure 쿠키의 보안 정책에 의해 발생한 이슈였다.

단순한 설정 실수인 줄 알았던 이 현상의 원인을 파악하기 위해, JMeter 소스 코드를 디버깅하며 찾아낸 분석 과정과 해결책을 공유하고자 한다.

문제 상황: 쿠키가 사리짐

Keycloak의 Authorization Code Grant 흐름을 JMeter로 재현하는 과정이었다. 당연히 쿠키 관리로 HTTP Cookie Manager(관련 문서)를 추가했기에 세션 쿠키가 자동으로 브라우저처럼 관리될 것이라 기대했다.

  1. /realms/${REALM}/protocol/openid-connect/auth 호출: 로그인 페이지 진입 및 세션 초기화
    • Response Header: Set-Cookie: AUTH_SESSION_ID=...; Secure; ...
  2. 로그인 요청: ID/PW 입력 후 POST 전송 (문제 발생 구간)
  3. 리다이렉트 및 토큰 발급: Code 획득 후 Token 교환

1번 과정에서 Keycloak은 AUTH_SESSION_ID를 포함한 쿠키들을 Set-Cookie 헤더로 구워줬다. 하지만 바로 이어지는 2번 인증 요청 헤더를 확인해보니, JMeter는 이 쿠키들을 전혀 실어 보내지 않고 있었다.

쿠키가 없으니 Keycloak은 세션을 찾지 못했고, 당연히 인증은 실패했다.

브라우저 vs JMeter, 무엇이 다른가?

  • 테스트 환경: 로컬 (http://localhost:8080)
  • 현상: 브라우저는 HTTP 환경에서도 쿠키를 잘 보내지만, JMeter는 보내지 않음.

시도해본 것들

처음에는 HTTP Cookie Manager의 정책(Policy) 문제라고 생각했다. 하지만 Standard, Standard-Strict, RFC 2965 등 다양한 정책으로 변경해 보았지만 결과는 동일했다.

또한, JMeter의 user.properties 설정을 통해 쿠키 검증을 완화할 수 있다 하여 아래 설정을 추가해 보았다.

# user.properties
CookieManager.check.cookies=false

하지만 이 설정 역시 문제를 해결해주지 못했다.

참고로 이후 디버깅을 하면서 알게된건데, CookieManager.check.cookies=false는 주로 응답받은 쿠키를 저장할 때의 유효성 검사를 완화하는 설정이지, 이미 저장된 Secure 쿠키를 HTTP 로 전송하는 것을 허용하는 설정은 아니었기 때문이었다. HC4CookieHandler#addCookieFromHeader를 살펴보면 boolean인 checkCookies로 분기를 타는 로직이 있다.

결국 직접 코드를 까보자는 생각으로 JMeter 5.2 소스 코드를 다운로드하여 IntelliJ Remote JVM Debug를 연결했다.

JMeter의 쿠키 처리는 CookieManager와 Apache HttpClient의 HC4CookieHandler가 핵심 역할을 한다. 디버깅을 통해 확인한 쿠키의 생명주기 다음과 같았다.

(1) 쿠키 저장: addCookieFromHeader

서버로부터 Set-Cookie 응답이 오면 HC4CookieHandler#addCookieFromHeader가 호출된다. 앞서 언급한 checkCookies 설정이 false라면 이 단계에서 몇몇 검증이 스킵될 수 있다.

디버깅 결과, isSecure 속성이 true인 쿠키(Keycloak 세션 등)가 JMeter의 내부 쿠키 저장소에는 정상적으로 저장되고 있음을 확인했다.

즉, 저장은 되는데 꺼내지 못하는 상황인 것.

(2) 전송 실패의 원인: getCookieHeaderForURL

다음 요청을 보내기 위해 JMeter는 getCookiesForUrl 메서드를 실행하여 해당 URL에 적합한 쿠키를 선별한다. 이때 내부적으로 cookieSpec.match(cookie, cookieOrigin)를 호출하는데, 여기서 결정적인 클래스인 BasicSecureHandler가 나온다.

// org.apache.http.impl.cookie.BasicSecureHandler.java
 
@Override
public boolean match(final Cookie cookie, final CookieOrigin origin) {
    // 논리 구조: !cookie.isSecure() || origin.isSecure()
    // 1. 쿠키가 Secure 속성이 없다면(False) -> HTTP HTTPS 상관없이 통과!
    // 2. 쿠키가 Secure라면 -> HTTPS 필수!
    return !cookie.isSecure() || origin.isSecure();
}

분석 결과

Apache HttpClientRFC 6265(HTTP State Management Mechanism)의 스펙을 따른다고 한다.

img.png

  1. 내 상황: 로컬 테스트 환경은 http://localhost이므로 origin.isSecure()false다.
  2. 쿠키 상태: Keycloak이 발급한 쿠키는 Secure 속성이 있으므로 cookie.isSecure()true다.
  3. 결과: 쿠키는 Secure이므로 HTTPS가 강제되는데 이를 미충족 시켜 일어난 일이었다.

브라우저는 개발 편의성을 위해 localhost에 한해 Secure 쿠키의 HTTP 전송을 예외적으로 허용하거나 경고만 띄우는 경우가 있지만, JMeter(정확히는 Apache HttpClient)는 규격을 칼같이 지키고 있었던 것이다.

이로 인해 JMeter 저장은 된 쿠키가 match 조건에서 탈락하여 요청 헤더에서 제외되었다.

(참고)쿠키 정책 별 동작 정리

JMeter의 HTTP Cookie Manager는 다양한 정책들을 지원하는데, 모두 Secure 속성에 대해서는 똑같이 동작했다.

정책들의 Cookie Secure 관련 처리를 정리해봤다. 크게 3가지로 분류된다.

  • Standard, Standard-Strict, Default, RFC 2109 등: 대부분 BasicSecureHandler를 공통으로 사용하므로, 위와 동일한 로직에 의해 localhost 환경에서 모두 차단됨.
  • RFC 2965: 해당 정책은 서버 응답 헤더에 Set-Cookie2가 있어야 동작하도록 설계되어 있음. 현재 Keycloak 응답에는 해당 헤더가 없으므로 유효한 쿠키로 인식되지 않음.
  • IgnoreCookies: 모든 쿠키 처리를 무시.

해결 방법

이 문제를 해결하기 위해 선택할 수 있는 방법 두가지를 소개해본다.

방법 1: 직접 쿠키 추출 및 주입 (채택한 방법)

HTTP Cookie Manager의 자동 관리에 의존하지 않고, 직접 쿠키 값을 추출하여 다음 요청 헤더에 심어주는 방식이다.

Regular Expression Extractor로 응답 헤더의 쿠키를 추출 및 변수화하여 이후 요청에 HTTP Header Manager를 이용해 직접 쿠키를 세팅해주면 된다.

  1. Regular Expression Extractor 추가: 로그인 페이지 응답 헤더(Set-Cookie)에서 필요한 세션ID(예: AUTH_SESSION_ID)를 정규식으로 추출하여 변수화.
    • Regular Expression: AUTH_SESSION_ID=(.+?);
    • Template: $1$
    • Match No: 1
  2. HTTP Header Manager 추가: 다음 요청(로그인 POST)의 HTTP Header Manager에 추출한 변수를 사용하여 Cookie 헤더를 직접 추가.
    • Name: Cookie
    • Value: AUTH_SESSION_ID=${AUTH_SESSION_ID}

서버 설정을 건드리지 않고 JMeter 수정만으로 빠르게 문제를 해결할 수 있다는 장점이 있다.

방법 2: 테스트 환경을 HTTPS로 구성하기

JMeter가 Secure 쿠키를 보내지 않는 근본적인 이유는 HTTP를 쓰기 때문이다. 로컬 환경에서 사설 인증서를 생성하여 HTTPS를 적용하면 origin.isSecure()true가 되어 문제가 깔끔하게 해결된다.