로그인 크롤링 (세션, 쿠키, CSRF)
크롤링을 어느 정도 익히고 나면 거의 반드시 마주치는 다음 벽이 로그인이 걸린 페이지다. BS4와 requests.get()으로 페이지를 받아 보면 정작 보고 싶은 데이터가 있는 자리에 "로그인이 필요합니다"라는 안내만 떨어진다. 본 글은 그 벽을 통과하는 세 가지 핵심 개념인 HTTP 세션 유지, 쿠키 기반 인증, CSRF 토큰 처리를 4학년 비정형 데이터 처리 수업에서 학교 LMS 알림 자동화를 시도하며 마주쳤던 본인 경험에 비추어 정리한다(출처: requests 공식 문서 Sessions). 제가 처음 로그인 페이지를 크롤러로 통과시키려 했을 때 가장 황당했던 게 "정확한 ID·비밀번호를 보내는데도 응답이 그대로 로그인 페이지로 돌아오는" 모습이었고, 지금 보면 그 황당함이 사실 CSRF 토큰의 존재를 모른다는 신호였다.

로그인이 BS4·Scrapy 다음 단계인 이유 (세션의 의미)
HTTP는 본래 무상태(stateless) 프로토콜이다. 여기서 무상태란 한 요청이 끝나면 서버가 그 요청을 보낸 클라이언트를 기억하지 않는다는 의미이며, 새 요청은 매번 처음 보는 손님 취급을 받는다는 뜻이다. 그런데 우리가 사이트에 한 번 로그인하면 그 뒤로 페이지를 옮겨 다녀도 계속 로그인 상태가 유지되는 경험을 한다. 그 경험을 가능하게 하는 장치가 세션(Session)과 쿠키(Cookie)다.
서버는 로그인이 성공한 시점에 사용자에게 고유한 식별 문자열을 발급해 응답 헤더의 Set-Cookie로 돌려준다. 브라우저는 이 쿠키를 로컬에 저장해 두었다가 같은 도메인으로 가는 모든 요청 헤더에 자동으로 실어 보낸다. 서버는 매 요청마다 그 쿠키를 보고 "이 사람이 아까 로그인한 그 사람"이라고 인식한다. 이 흐름이 끊기지 않게 클라이언트 측에서 쿠키를 자동으로 관리해 주는 객체가 requests.Session이다.
가장 단순한 형태는 다음과 같다.
import requests
from bs4 import BeautifulSoup
LOGIN_URL = "https://example.com/login"
DATA_URL = "https://example.com/dashboard"
session = requests.Session()
session.post(LOGIN_URL, data={"username": "myid", "password": "mypw"})
res = session.get(DATA_URL)
soup = BeautifulSoup(res.text, "lxml")
print(soup.select_one("h1").text)
Session 객체 안에 쿠키가 누적되기 때문에, 한 번 post로 로그인하고 나면 그 뒤의 get 요청들은 모두 로그인 상태를 자동으로 유지한다. 즉 Session을 만든 시점부터 세션이 끝날 때까지 같은 브라우저 한 명이 페이지를 옮겨 다니는 흐름과 거의 동일한 동작을 코드로 구현한 셈이다.
다만 위 코드는 거의 모든 실제 사이트에서 그대로는 통하지 않는다. 솔직히 제 경험상 학교 LMS·블로그 관리자·온라인 쇼핑몰 어디에 시도해도 첫 시도는 그대로 로그인 페이지로 돌아왔다. 이 시점이 바로 CSRF 토큰을 마주치는 순간이다.
requests.Session과 쿠키 기반 인증 (실전 함정)
로그인 폼이 단순한 ID·PW 두 필드만 받는 사이트는 사실 거의 없다. 거의 모든 현대 사이트는 보안 강화를 위해 폼 안에 hidden 필드를 숨겨 두는데, 가장 흔한 것이 csrf_token, authenticity_token, _token 같은 이름의 임시 토큰이다. 여기서 CSRF란 Cross-Site Request Forgery(사이트 간 요청 위조)의 약자로, 공격자가 사용자의 인증 상태를 악용해 의도하지 않은 요청을 보내게 만드는 공격 기법을 가리킨다(출처: OWASP CSRF 문서).
이 공격을 막기 위해 서버는 폼을 사용자에게 보낼 때마다 무작위 토큰 한 줄을 함께 끼워 보내고, 폼 제출 시 그 토큰이 그대로 돌아오는지를 검증한다. 토큰이 없거나 토큰이 일치하지 않으면 서버는 폼을 무시한다. 크롤러가 ID·PW만 던지면 토큰 검증에서 자동으로 걸리고, 그 결과 응답은 다시 로그인 페이지로 돌아오는 것이다.
해결 흐름은 두 단계다. 첫째, 로그인 페이지를 먼저 GET으로 받아 토큰 값을 파싱한다. 둘째, 그 토큰을 ID·PW와 함께 POST로 보낸다. 같은 Session 안에서 진행해야 토큰을 발급한 서버 세션과 토큰을 사용하는 클라이언트 세션이 일치한다.
import requests
from bs4 import BeautifulSoup
LOGIN_URL = "https://example.com/login"
DATA_URL = "https://example.com/dashboard"
session = requests.Session()
# 1) GET으로 로그인 페이지를 받아 CSRF 토큰 추출
login_page = session.get(LOGIN_URL)
soup = BeautifulSoup(login_page.text, "lxml")
csrf = soup.select_one("input[name='csrf_token']")["value"]
# 2) 토큰 + ID/PW를 같이 POST
payload = {
"username": "myid",
"password": "mypw",
"csrf_token": csrf,
}
session.post(LOGIN_URL, data=payload, headers={"Referer": LOGIN_URL})
# 3) 로그인 이후 페이지 접근
res = session.get(DATA_URL)
print(res.status_code, "/", res.url)
여기서 Referer 헤더를 같이 보낸 이유는 일부 사이트가 폼이 같은 도메인에서 제출됐는지를 추가 검증하기 때문이다. 솔직히 이건 시험엔 자주 안 나오지만 실무에서 가장 자주 막히는 지점이다. 본인이 학교 LMS 알림 자동화를 처음 시도했을 때 토큰까지 정확히 넣었는데도 막혔던 원인이 정확히 이 Referer 누락이었고, 그 한 줄을 추가하자 즉시 통과했다.
CSRF 토큰 다음의 세 가지 함정 (CAPTCHA, 2FA, JS 로그인)
CSRF 토큰까지 통과하면 다음 단계의 벽이 등장한다. 본인이 정리한 실전 빈도 순서는 다음과 같다.
첫 번째 함정은 CAPTCHA다. 이미지·체크박스·드래그 같은 사람-기계 구분 장치가 로그인 폼에 붙어 있다면, requests만으론 사실상 우회 불가능하다. 합법적 해결책은 브라우저 자동화 도구(Selenium·Playwright)로 전환하는 것이며, 그것도 사이트 정책에 따라 차단 가능성이 있다. 우회 서비스를 쓰는 옵션도 있지만 한국 법 맥락(부정경쟁방지법)에서 위험 신호가 분명해 권하지 않는다.
두 번째 함정은 2단계 인증(2FA)이다. 로그인 폼 이후 OTP·이메일 코드·SMS를 요구하는 사이트는 자동화 자체가 사실상 봉인된다. 본인 계정으로 학습용 자동화를 하는 경우라도 휴대폰 코드 입력 단계가 들어오면 사람이 개입해야 하며, 봇이 아예 통과할 수 없도록 설계된 것이 본래 목적이다.
세 번째 함정은 자바스크립트로만 로그인이 처리되는 SPA형 사이트다. 폼 제출 자체를 페이지 새로고침 없이 비동기 API 호출(fetch 또는 XMLHttpRequest)로 처리하는 구조이며, 이 경우 requests의 단순 폼 POST는 통하지 않는다. 해결 방법은 두 가지인데, 첫째는 그 API 엔드포인트를 개발자 도구의 네트워크 탭에서 찾아내 직접 JSON으로 호출하는 것(가벼움), 둘째는 Playwright·Selenium으로 실제 브라우저를 띄워 로그인 시키는 것(무겁지만 거의 모든 사이트에 통함)이다.
흔한 오해 하나를 짚으면, "로그인된 페이지의 데이터는 공개 데이터가 아니다"라는 점이다. 회원만 볼 수 있는 정보를 자동 수집·재배포하는 행위는 단순한 robots.txt 영역을 넘어 정보통신망법·저작권법 동시 적용 사정권에 들어간다(출처: 한국인터넷진흥원 KISA). 본인 계정으로 본인이 볼 수 있는 데이터만 자동화하는 것과, 타인의 인증 정보로 들어가는 것은 같은 코드처럼 보여도 법적으로 전혀 다른 행위로 평가된다.
로그인 크롤링은 세션·쿠키·CSRF라는 세 기둥만 정확히 이해하면 80%가 풀린다. 다만 그 위에 CAPTCHA·2FA·SPA가 얹히는 순간 도구 선택이 requests에서 Playwright로, 그리고 그 너머 "수집 자체를 포기하는 결정"으로 옮겨가야 한다는 점도 같이 알아 두어야 실무에서 시간 낭비를 줄일 수 있다.