본문으로 건너뛰기
취준생을 위한 n8n 실전 워크플로우 레시피 · 30/44시리즈 보기 →

로그인이 필요한 사이트도 CSRF 토큰과 세션 쿠키로 긁어오기 — 매주 통계 페이지에 자동 로그인하는 n8n 워크플로우

· 약 9분
Datapopcorn CEO / AI automation educator
이 글과 연결된 글 보기
7편 · 20개 연결
100%
n8n Data Table 처음 써보기 — 트렌드 3개를 합쳐 매일 아침 분석 주제 뽑는 워크플로우n8n으로 정적 웹서버 운영하기 — 수료증 발급부터 진위 확인까지, 멱등 처리로 안전하게벡터DB 없이 실시간으로 사이트를 읽는 챗봇 — 경복대학교 안내 챗봇 해부n8n으로 반복업무 자동화 포트폴리오 만들기로그인이 필요한 사이트도 CSRF 토큰과 세션 쿠키로 긁어오기 — 매주 통계 페이지에 자동 로그인하는 n8n 워크플로우구글시트에 쌓인 데이터를 인스타그램 카드뉴스로 — n8n 시트 인스타그램 자동발행서브워크플로우를 Tool로 감싸 AI Agent에게 서버 점검시키기 — 그리고 시스템 프롬프트만으로는 부족한 이유

드래그로 이동 · 휠/두 손가락으로 확대 · 방향키로 글 선택 · 0으로 맞춤. 실선: 본문 링크, 점선: 시리즈·주제의 대표 연결.

표시된 글 7편 목록
전체 글 연결 탐색

로그인해야만 보이는 페이지도, 로그인 폼의 CSRF 토큰과 로그인 뒤 발급되는 세션 쿠키만 코드로 다루면 n8n이 매주 대신 로그인해서 읽어옵니다. API도 없고 로그인 장벽까지 있는 사이트에서 정기 리포트를 뽑아야 할 때 쓰는 패턴이고, 이 워크플로우는 그 패턴과 함께 "이렇게 만들면 안 된다"는 반면교사도 같이 보여줍니다.

이 워크플로우 하나로 보여줄 수 있는 것​

지금까지 이 시리즈는 공개 페이지를 긁는 법과 막힌 페이지를 우회하는 법을 다뤘습니다. 이번 편은 세 번째 변주, "로그인이 막는 페이지"입니다. 면접에서 "API 없는 사이트에서 데이터를 어떻게 가져왔나요?"라는 질문에 "로그인 세션을 코드로 직접 다뤄서 가져왔습니다"라고 답할 수 있게 해주는 워크플로우입니다.

여기 들어간 역량은 세 가지입니다.

  • CSRF 방어 우회 — 로그인 폼에 숨어 있는 보안 토큰을 읽어서 로그인 요청에 함께 실어 보내는 경험
  • 세션 쿠키 관리 — 로그인 성공 응답에서 세션 쿠키를 뽑아 이후 모든 요청의 인증 수단으로 쓰는 경험
  • 보안 반면교사 — 이 워크플로우가 실제로 저지른 실수(비밀번호 하드코딩)를 보고, 왜 잘못됐고 뭘로 바꿔야 하는지 설명하는 경험

세 번째가 특히 중요합니다. 잘 만든 예제만 보면 "이렇게 만들면 된다"만 배웁니다. 이 워크플로우는 실제로 운영 중인 사이트를 대상으로 만든 것이라 완벽하지 않고, 그 불완전함이 오히려 "이건 왜 문제인가"를 설명할 재료가 됩니다.

배경 — API도 없고 로그인도 막는 통계 페이지​

공개된 페이지라면 앞선 편처럼 GET 요청 한 번으로 HTML을 받아올 수 있습니다. 하지만 사이트 관리자용 통계 페이지 같은 곳은 로그인한 사람에게만 보입니다. 공식 API가 없으니 REST 호출로 끝날 일이 아니고, 로그인 없이 URL만 요청하면 로그인 페이지로 튕겨 나갑니다.

이 워크플로우는 위키독스에 올라간 전자책의 주간 통계 페이지를 대상으로 합니다. 매주 월요일 아침, 로그인해서 통계를 확인하는 절차 전체를 워크플로우 하나가 대신합니다. n8n 설치와 기본 노드 사용법은 n8n으로 반복업무 자동화 포트폴리오 만들기에 정리돼 있으니, 이 글에서는 로그인 흐름만 다룹니다.

구조 — Trigger / Task / Target​

  • Trigger — 매주 월요일 09:00 스케줄
  • Task — 로그인 페이지를 먼저 읽어 CSRF 토큰을 확보 → 그 토큰으로 로그인 → 로그인 응답에서 세션 쿠키를 확보 → 그 쿠키로 통계 페이지를 인증된 상태로 읽기 → HTML에서 숫자만 정규식으로 추출
  • Target — Slack API를 HTTP Request 노드로 직접 호출

만들기​

1. HTTP Request — 로그인 페이지 GET​

https://wikidocs.net/accounts/login/을 GET으로 요청합니다. 이때 Response 옵션에서 Full Response를 켜 둡니다. 이 옵션이 없으면 다음 노드로는 응답 본문만 넘어가고 응답 헤더는 사라지는데, 바로 다음 단계에서 헤더 안의 쿠키가 필요합니다.

로그인도 하기 전에 이 페이지부터 읽는 이유는 CSRF 토큰 때문입니다. 로그인 폼 HTML 안에 name="csrfmiddlewaretoken"이라는 숨은 입력 필드가 있고, 동시에 응답 헤더에는 csrftoken 쿠키가 함께 발급됩니다. 이 필드 이름은 Django 계열 프레임워크의 CSRF 보호 방식과 같아서, 이 사이트도 같은 방식을 쓰는 것으로 보입니다. 이런 CSRF 보호는 쿠키 값과 폼 값이 쌍을 이뤄야 유효하다고 보기 때문에, 로그인 직전 매번 이 페이지를 새로 읽어 두 값을 한 쌍으로 확보해야 합니다.

2. Code — CSRF 파싱​

Code 노드에서 방금 받은 응답의 헤더와 본문을 동시에 뒤집니다.

const body = $input.first().json.body;
const headers = $input.first().json.headers;
const setCookie = headers['set-cookie'];
const cookieStr = Array.isArray(setCookie) ? setCookie.join('; ') : (setCookie || '');
const csrftokenCookie = (cookieStr.match(/csrftoken=([^;,\s]+)/) || [])[1] || '';
const csrfToken = (body.match(/name="csrfmiddlewaretoken"\s+value="([^"]+)"/) || [])[1];
if (!csrfToken) throw new Error('CSRF token not found');
return [{ json: { csrftokenCookie, csrfToken } }];

쿠키 값은 set-cookie 헤더에서, 폼 값은 본문 HTML에서 각각 정규식으로 뽑습니다. 토큰을 못 찾으면 바로 에러를 던져, 빈 값이 다음 단계로 넘어가 로그인이 조용히 실패하는 대신 이 시점에서 실행이 멈추고 실패 이력에 남습니다.

3. HTTP Request — 로그인 POST​

같은 URL로 POST 요청을 보냅니다. 헤더에는 Referer와 Cookie를 넣는데, Cookie 값은 csrftoken={{ $json.csrftokenCookie }}처럼 앞서 뽑은 쿠키 값을 그대로 실어 보냅니다. 바디는 form-urlencoded로 세 필드를 보냅니다.

필드값
csrfmiddlewaretoken{{ $json.csrfToken }} (표현식)
login로그인 계정 (고정 문자열)
password비밀번호 (고정 문자열)

login과 password 두 필드는 표현식이 아니라 그냥 문자열로 고정돼 있습니다. 왜 문제인지는 아래 "한계"에서 자세히 다룹니다.

옵션에서 두 가지를 눈여겨봐야 합니다. Redirect의 Follow Redirects를 끄고(false), Response의 Never Error를 켰습니다(true). 로그인에 성공하면 이런 사이트는 대개 302 리다이렉트로 응답하면서 그 응답 자체에 세션 쿠키를 Set-Cookie로 실어 보냅니다. n8n의 HTTP Request 노드에는 브라우저 같은 쿠키 저장소가 없어서, 리다이렉트를 그대로 따라가면 302 응답의 헤더를 다음 노드가 받지 못하는 문제가 있습니다. 이 두 옵션은 정확히 그 문제를 피하려는 조합으로 보입니다. 리다이렉트를 따라가지 않고 302 응답을 그대로 받되, 302는 200이 아니므로 에러로 처리하지 않게 열어 둔 것입니다.

4. Code — 세션 쿠키 파싱​

const headers = $input.first().json.headers;
const setCookie = headers['set-cookie'];
const cookieStr = Array.isArray(setCookie) ? setCookie.join('; ') : (setCookie || '');
const sessionId = (cookieStr.match(/sessionid=([^;,\s]+)/) || [])[1];
if (!sessionId) throw new Error('Login failed: no sessionid');
return [{ json: { sessionId } }];

로그인 POST 응답 헤더에서 sessionid 쿠키를 뽑습니다. 여기서도 못 찾으면 에러를 던지는데, 이 자체가 로그인 성공 여부를 판정하는 역할을 겸합니다. 비밀번호가 틀리면 sessionid가 발급되지 않으니 이 노드에서 실행이 멈추고, 뒤의 통계 페이지 요청은 아예 시도되지 않습니다.

5. HTTP Request — 통계 페이지 GET​

이제 인증된 요청입니다. 헤더에 Cookie: sessionid={{ $json.sessionId }}를 실어 통계 페이지를 GET합니다. URL의 ?days=7 쿼리 파라미터는 최근 7일치 통계를 요청하는 값으로, 매주 월요일이라는 스케줄 주기와 맞아떨어집니다. 이 쿠키 없이 URL만 열면 로그인 페이지로 돌아가지만, 쿠키를 실으면 통계 HTML이 그대로 응답으로 옵니다.

6. Code — HTML 파싱​

공식 API가 없으니 받은 HTML 문자열에서 정규식으로 필요한 숫자만 뽑아냅니다.

const stats = {};
const statRe = /<div class="stat-number">([\d,.%\s초]+)<\/div>\s*<div class="stat-label">([^<]+)<\/div>/g;
let sm;
while ((sm = statRe.exec(html)) !== null) stats[sm[2].trim()] = sm[1].trim();

같은 방식으로 기간 표시, 인기 페이지 TOP 5, 유입 경로 TOP 5도 각각 다른 정규식으로 뽑아 슬랙 메시지 문자열 하나로 조립합니다. 총 페이지뷰·총 방문자·평균 체류시간·평균 이탈률은 표 형태로, 인기 페이지와 유입 경로는 목록 형태로 들어갑니다. 사이트 HTML의 클래스 이름(stat-number, stat-label, page-title, view-count)에 그대로 의존하는 방식이라, 지난 HTML 스크래핑 편에서 다룬 것과 같은 약점을 안고 있습니다.

7. HTTP Request — Slack 전송​

https://slack.com/api/chat.postMessage로 POST합니다. 헤더에 Authorization: Bearer {SLACK_BOT_TOKEN}을 넣고, 바디는 JSON으로 채널과 메시지 텍스트를 담습니다. n8n에는 Slack 전용 노드가 따로 있어 credential만 골라두면 이 인증 헤더를 알아서 붙여주는데, 이 워크플로우는 그 대신 HTTP Request로 직접 호출해 봇 토큰이 노드 파라미터 화면에 그대로 드러나 있습니다. 아래 "한계"에서 다루는 문제와 뿌리가 같습니다.

원본 캔버스에 남아 있는 흔적​

이 워크플로우에는 스티키 노트가 하나도 없습니다. 대신 노드 이름과 구조 자체에 몇 가지 흔적이 남아 있습니다.

Code 노드 세 개의 이름이 전부 "~ 파싱"으로 끝납니다. CSRF 파싱, sessionid 파싱, HTML 파싱. 각 단계가 직전 응답에서 무엇을 뽑아내는지 이름만 봐도 알 수 있어서, 처음 여는 사람도 실행해보지 않고 흐름을 따라갈 수 있습니다.

원본 JSON의 active 값은 false입니다. 즉 이 워크플로우는 지금 켜져 있지 않은 상태로 저장돼 있습니다. 왜 꺼져 있는지는 export 파일만으로는 알 수 없어서 추측하지 않습니다.

그리고 로그인 POST 노드의 바디 파라미터를 열어 보면 계정과 비밀번호가 표현식이 아니라 고정 문자열로 그대로 들어 있습니다. 이 부분이 다음 섹션의 핵심입니다.

한계​

비밀번호를 코드에 그대로 적어둔 것 — 이 워크플로우의 가장 큰 문제입니다. n8n의 HTTP Request 노드는 Basic Auth, Header Auth, OAuth2 같은 Generic Credential Type을 지원하지만, CSRF 토큰까지 함께 보내야 하는 이런 커스텀 로그인 흐름은 이 타입 어디에도 딱 맞지 않습니다. 표준 인증 방식이 아니다 보니 만든 사람이 그냥 파라미터 칸에 문자열로 적어 넣은 것으로 보이는데, 이러면 워크플로우를 화면 공유하거나 JSON을 내보내는 순간 비밀번호가 그대로 노출됩니다.

대안은 두 가지입니다. 하나는 n8n Variables($vars)로, {{ $vars.WIKIDOCS_PASSWORD }}처럼 참조하면 값은 인스턴스 설정에만 저장되고 워크플로우 파라미터 화면이나 export 파일에는 남지 않습니다. 다만 n8n Cloud의 Pro·Enterprise 플랜, 셀프호스팅이라면 Enterprise 라이선스에서만 쓸 수 있는 유료 기능입니다(2026년 8월 기준, n8n 공식 문서). 유료 플랜을 쓸 수 없다면 최소한 별도 Set 노드 하나에 자격증명을 몰아넣어, 로그인 로직이 담긴 Code·HTTP 노드 본문에는 값이 안 보이게 분리하는 절충안을 쓸 수 있습니다. Set 노드도 export하면 값이 그대로 남는다는 한계는 있지만, 화면 공유 중 실수로 노출될 위험은 줄어듭니다. Slack 봇 토큰도 같은 문제라, 헤더에 직접 적는 대신 Slack credential을 쓰면 토큰 값이 파라미터 화면에 나타나지 않습니다.

그 밖에도 세 가지가 더 있습니다.

  • 실패 알림 없음 — 세션 쿠키 파싱 노드가 로그인 실패 시 에러를 던지긴 하지만, 이를 잡아 슬랙으로 알리는 흐름이 따로 없습니다. 이 시리즈 다른 편에서 다룬 Error Trigger 패턴을 붙이면 로그인이 막힌 주에도 실패 알림 정도는 받을 수 있습니다.
  • HTML 구조 의존 — 정규식이 사이트의 클래스 이름에 강하게 의존합니다. 마크업이 바뀌면 정규식은 에러 없이 빈 값을 돌려주기 때문에, 실패가 아니라 "숫자가 이상하게 온다"는 형태로 조용히 드러납니다.
  • 한 주라도 실행이 끊기면 공백 — 스케줄이 매주 한 번뿐이라, 한 주라도 실행이 실패하면 그 주 데이터는 그냥 사라집니다. days=7 대신 더 긴 기간을 요청해 여유를 둘 수 있습니다.

포트폴리오·면접 포인트​

이 워크플로우를 포트폴리오에 올린다면, 원본 그대로가 아니라 비밀번호를 Set 노드나 $vars로 분리한 버전으로 캡처해서 올리는 걸 권합니다. 그 자체가 "그냥 따라 만든 사람"과 "왜 문제인지 알고 고친 사람"을 가르는 지점입니다.

면접에서 나올 만한 질문은 두 가지입니다. "왜 세션이 자동으로 유지되지 않고 매번 쿠키를 직접 넘겨야 했나요?"에는 HTTP Request 노드에 브라우저 같은 쿠키 저장소가 없어서, 앞 요청에서 받은 쿠키를 코드로 꺼내 다음 요청 헤더에 다시 실어야 한다고 답하면 됩니다. "비밀번호가 코드에 그대로 있던데요?"에는 credential 타입이 이런 커스텀 로그인 흐름에는 맞지 않아서 생긴 문제라는 것과, $vars나 별도 Set 노드로 분리하는 대안까지 설명하면 됩니다.

다음 액션​

공개 페이지, 막힌 페이지, 로그인이 막는 페이지까지 다뤘으니 스크래핑 쪽 변주는 이걸로 한 바퀴 돕니다. 다음 편은 방향을 바꿔, 슬랙 대화를 며칠 치 쌓아 두었다가 주간 리포트로 뽑아내는 워크플로우로 이어집니다. 이번 편이 "밖에서 데이터를 가져오는" 흐름이었다면, 다음 편은 "안에 쌓인 대화를 리포트로 바꾸는" 흐름입니다.