ํฌ๋กค๋ง
ํฌ๋กค๋ง์ ์ํํ๋ ๋ฐฉ๋ฒ๊ณผ ์๋น์ค๋ค์ด ํฌ๋กค๋ง์ ๊ฐ์งํ๊ณ ์ฐจ๋จํ๋ ๋ฐฉ์ด์ฑ
๋ค, ๊ทธ ๊ธฐ๋๊ธด ์ธ์๊ณผ ๊ฒฝํฅ์ฑ์ ๋ํด์ ์ ๋ฆฌํด๋ณธ๋ค.
ํฌ๋กค๋ง ๋์ ์์คํ
์ HTTP ๊ธฐ๋ฐ ์น ์์คํ
์ด๋ผ๊ณ ๊ฐ์ ํ๋ค.
์ ์ ํฌ๋กค๋ง๊ณผ ๋์ ํฌ๋กค๋ง
ํฌ๋กค๋ง์ ํฌ๊ฒ 2๊ฐ์ง ๋ฐฉ์์ผ๋ก ๋๋๋ค.
์ ์ ํฌ๋กค๋ง
์ ์ ํฌ๋กค๋ง์ ์คํฌ๋ํ(scrapping)์ด๋ผ๊ณ ๋ ํ๋ค.
์ด์ฐ๋์๋ ๋ชจ๋ ์ฌ์ดํธ๋ API๋ก ๋ฐ์ดํฐ๋ฅผ ๋ด๋ ค์ค๋ค.
์ ๊ธฐ์ ํ์ํ API๋ง API ํด๋ผ์ด์ธํธ๋ก ํธ์ถํ๊ณ , ๊ทธ ์๋ต๊ฐ์ ํ์ฑํด์ ์ ์ ํ ์์งํ๋ ๊ฒ์ด๋ค.

๊ทธ๋ฐ๋ฐ ์ด๋ฐ ์ ์ ํฌ๋กค๋ง์ ๋ฆฌ์์ค๋ฅผ ์ต์ํ์ผ๋ก ์๋ชจํ๊ณ ๋งค์ฐ ๋น ๋ฅด๋ค๋ ์ฅ์ ์ด ์์ง๋ง, ์ฌ์ดํธ์ ๋ฐ๋ผ์๋ ๊ตฌํํ๊ธฐ๊ฐ ๊ต์ฅํ ๋ฒ๊ฑฐ๋กญ๊ฑฐ๋, ํน์ ์ฐจ๋จ์ ํํผํ๊ธฐ ์ด๋ ต๊ฒ ๋ง๋ค์ด์ ธ์์ ์๋ ์๋ค.
B. ๋์ ํฌ๋กค๋ง
๋์ ํฌ๋กค๋ง์ ๊ทธ์ ๋ํ ๋์์ผ๋ก, ์ง์ง ๋ธ๋ผ์ฐ์ ๋ ๊ฐ๋ฒผ์ด ๋ธ๋ผ์ฐ์ ๊ตฌํ์ ์ฌ์ฉํด์ ์ฌ์ดํธ๋ฅผ ์ง์ ์ด๊ณ , ๊ทธ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ๋ฐฉ๋ฒ์ ๋งํ๋ค. ๋ํ์ ์ธ ๊ตฌํ์ฒด๋ก๋ ์ ๋ ๋์, puppeteer, playwright ๋ฑ์ด ์๋ค.
์
๋ ๋์์ ๋๋ถ๋ถ์ ์ธ์ด์์ ์ฌ์ฉ ๊ฐ๋ฅํ์ง๋ง, puppeteer๋ฅผ ๋น๋กฏํ ์ฌํ ์์ง๋ค์ node.js ํ๊ฒฝ์์ ์ ๊ณต๋๋ค. ๊ทธ๋์ ์ฌ์ค ํฌ๋กค๋ง์ ํ๋ค๊ณ ํ๋ฉด nodejs ํ๊ฒฝ์ ์ ํํ๋๊ฒ ๋์์ด ๋ง์ ํธ์ด๋ค.
๊ฐ๊ฐ์ด ๋์ ๋ฐฉ์์ด ์กฐ๊ธ์ฉ ๋ค๋ฅด๊ณ , ๊ฒฝ์ฐ์ ๋ฐ๋ผ์ ํฌ๋กค๋ฌ ์ฐจ๋จ ํจํด์ ํํผํ๊ธฐ์ ์ฉ์ดํ ์ด์ ์ด ์์ ์๋ ์๋ค.
headless ๋ชจ๋
๊ทธ๋ฐ๋ฐ ๋, ์ฌ์ดํธ๋ฅผ ์ง์ ํ๋ฉด์ผ๋ก ์ด๊ณ ๋ ๋๋งํ๋๊ฑด ๋ฆฌ์์ค ์๋ชจ๊ฐ ๊ทน์ฌํ ๋ฌธ์ ๊ฐ ์๋ค. ๊ทธ๋์ ์ด๋ฐ ํฌ๋กค๋ง ์์ง๋ค์ headless ๋ชจ๋๋๊ฑธ ํตํด์ ํ๋ฉด ์ฒ๋ฆฌ๋ฅผ ์๋ตํ๋ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ค. ์ด๊ฑธ ์จ์ผ ๊ทธ๋๋ง ์กฐ๊ธ ๋นจ๋ผ์ง๊ณ ๋ฆฌ์์ค ์๋ชจ๋ฅผ ์ค์ผ ์ ์๋ค.
์ ๊ทธ๋ผ, ์ด์ ๋ถํฐ๋ ์๋น์ค๋ค์ด ํฌ๋กค๋ฌ๋ฅผ ๊ฐ์งํ๊ณ ์ฐจ๋จํ๋ ๋ฐฉ๋ฒ๋ก ๋ค์ ํ๋์ฉ ๋ค๋ค๋ณด๊ฒ ๋ค.
์๋ฒ ํ๊ฒฝ ๊ตฌ์ฑ (with Rust hyper)
์ผ๋จ ๋ฐฉ์ด์์ ์
์ฅ์์๋ ํ์ธ์ ํ๊ธฐ ์ํด์, ํฌ๋กค๋ง ๋์์ด ๋๋ ๋ชจํ ์๋ฒ๋ ํ๋ ๋์ฐ๊ฒ ๋ค.
์ธ์ด๋ Rust๋ก ์ ํํ๋ค. ํน๋ณํ ์ด์ ๋ ์๋ค.
[package]
name = "just_test"
version = "0.1.0"
edition = "2021"
[dependencies]
hyper = { version = "1", features = ["full"] }
tokio = { version = "1", features = ["full"] }
http-body-util = "0.1"
hyper-util = { version = "0.1", features = ["full"] }
flate2 = "1.0"use std::convert::Infallible;
use std::net::SocketAddr;
use http_body_util::Full;
use hyper::body::Bytes;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Request, Response, StatusCode};
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;
async fn hello(_: Request<hyper::body::Incoming>) -> Result<Response<Full<Bytes>>, Infallible> {
let response = Response::builder()
.status(StatusCode::OK)
.body(Full::new(Bytes::from(
"<html><body><h1>OK</h1></body></html>",
)))
.unwrap();
Ok(response)
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let addr = SocketAddr::from(([0, 0, 0, 0], 3333));
let listener = TcpListener::bind(addr).await?;
loop {
let (stream, _) = listener.accept().await?;
let io = TokioIo::new(stream);
tokio::task::spawn(async move {
if let Err(err) = http1::Builder::new()
.serve_connection(io, service_fn(hello))
.await
{
println!("Error serving connection: {:?}", err);
}
});
}
}
๊ทธ๋ฆฌ๊ณ ์คํํ๋ฉด

์ผ๋จ ์ ๋๋ค.
๊ทธ๋ฆฌ๊ณ ๊ณต๊ฒฉ์, ํฌ๋กค๋ฌ๋ nodejs ํ๊ฒฝ์ ์ฌ์ฉํ๊ฒ ๋ค.

์ด๊ฒ๋ ๋ง์ฐฌ๊ฐ์ง๋ก ์ ๊ธ์ด์ฌ ๊ฒ์ด๋ค.
์ด์ ํฌ๋กค๋ฌ๋ฅผ ๋ง์๋ณด์.
User-Agent ๊ธฐ๋ฐ ์ฐจ๋จ
์์ ๋ถํฐ ๊ต์ฅํ ๋ง์ด ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ด๋ค.
์์ฆ์๋ ์ด๊ฑธ ์ค์ ์ผ๋ก ๋ง์ง๋ ์์ง๋ง, ์ฌ์ ํ ์๋ฌดํผ ์์ผ๋ฉด ๋งํ๋ ๊ฒฝ์ฐ๊ฐ ๋ง๋ค.
๋ธ๋ผ์ฐ์ ๊ฐ ์ฌ์ดํธ์ ์ง์
ํ๋ค๋ฉด ๊ธฐ๋ณธ์ ์ผ๋ก ๋์ง๋ ์์ฒญ ํค๋๋ค์ด ์๋ค.
๊ทธ ์ค ๋ํ์ ์ธ ๊ฒ์ด ๋ธ๋ผ์ฐ์ ์์ฑ์ ๋ํ๋ด๋ User-Agent๋ค.

๋ธ๋ผ์ฐ์ ๋ฅผ ์ฌ์ฉํด์ ๋์ ํฌ๋กค๋ง์ ํ ๊ฒฝ์ฐ์๋ ์ด๊ฒ๋ ๋ธ๋ผ์ฐ์ ์ ๋๋ฑํ๊ฒ ๋ฃ์ด์ค์ ๋ฌธ์ ๊ฐ ์์ง๋ง, HTTP client๋ฅผ ์จ์ ์ฐ๋ฅธ๋ค๋ฉด ์ด์ํ ๊ฐ์ด ํฌํจ๋๊ฑฐ๋ ์์ ์๊ฐ ์๋ ์๋ค.
์๋ฅผ ๋ค์ด, ํด๋ผ์ด์ธํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ธ axios์ ๊ฒฝ์ฐ์๋ User-Agent ํค๋๋ก ์๊ธฐ์ฃผ์ฅ์ ํ๋ค.
ํค๋๋ฅผ ๋ช
์ํ์ง ์์๋ ์์์ ์ ๋ ๊ฒ ์์์ ์ผ๋ก ๋ณด๋ด๋ ์จ์ ๋์๋ค์ด ์จ์ด์๋ค.
ํนํ User-Agent์ axios/1.7.9์ฒ๋ผ ๋ฒ์ ์ด ๋ฐํ์๋๊ฑด ๋๋ฌด ๋ปํ๋ค.
์ด๊ฑด nodejs/axios๋ง ๊ทธ๋ฐ๊ฑด ์๋๊ณ , ๋๋ถ๋ถ ์ธ์ด๋ค์ HTTP client ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค์ด ๋ค ์ ๋ฐ ๋์์ ๊ฐ์ง๊ณ ์๋ค.
์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ ค๋ฉด User-Agent ํค๋๋ฅผ ๋ช
์์ ์ผ๋ก null๋ก ๋ณด๋ด์ ์์ ์ฌ๋ผ์ง๊ฒ ํ๊ฑฐ๋, ์๋๋ฉด ์ง์ง ๋ธ๋ผ์ฐ์ ์์ ๋ณด๋ด๋ User-Agent๋ฅผ ๋๊ฐ์ด ๋ณด๋ด์ฃผ๋ ๋ฐฉ๋ฒ์ด ์๋ค.
๋ณดํต์ ํ์๊ฐ ๋ ์ข์ ์ ํ์ด๋ค.
๊ทธ๋ฆฌ๊ณ ์์ ์๋ ๊ฐ์ User-Agent๋ก ๊ณผ๋ํ ์์ฒญ์ด ๋ค์ด์ค๋ฉด ํด๋น User-Agent์ IP ์์ผ๋ก ์ฐจ๋จ์ ๊ฑฐ๋ ๊ฒฝ์ฐ๊ฐ ์์๋ค. ์ด๋ด ๋๋ User-Agent๋ฅผ ๋๋ค์ผ๋ก ํ ๋นํด์ ํํผํ๋ฉด ํํผ๊ฐ ๋๋๋ฐ, ์์ฆ์ ๊ทธ๋ฐ ๊ณณ์ด ๊ฑฐ์ ์์ ๊ฒ์ด๋ค.
IP ๊ธฐ๋ฐ ์ฐจ๋จ
์ด๋๋์ ๋๋ ํฌ๋กค๋ฌ๋ฅผ ๋ง๋ ๊ฐ์ฅ ๊ฐ๋ ฅํ ๋ฐฉ๋ฒ ์ค ํ๋๋ IP ๊ธฐ๋ฐ์ผ๋ก ํต์ ๋ฅผ ๊ฐํ๋ ๊ฒ์ด๋ค.

์๋ฒ๋ ๋น์ฐํ ํด๋ผ์ด์ธํธ์ ๊ณต์ธ IP๋ฅผ ์ ์ ์๊ณ , ํน์ IP์์ ๊ณผ๋ํ ์์ฒญ์ด๋ ์์ฌ์ค๋ฌ์ด ์์ฒญ ํจํด์ด ๋ฐ๊ฒฌ๋๋ฉด ์ฐจ๋จ์ ๋จน์ผ ์ ์๋ค.
๊ณต์ธ IP๋ ๋ฐ๊ฟ๊ฐ๋ฉด์ ์ฌ์ฉํ๊ธฐ ์ด๋ ต๊ธฐ ๋๋ฌธ์ ๋๋ถ๋ถ์ ์ํฉ์์ ์ ํจํ ๋ฐฉ๋ฒ์ด๊ณ , ๋ ๋๋ถ๋ถ์ ํฌ๋กค๋ฌ ํ์ง ์์คํ
๋ค์ IP์ ๊ธฐ๋ฐํ ์ฐจ๋จ์ ์ํํ๋ค.
๋ฌผ๋ก ๊ทธ๋ ๋ค๊ณ ํด์ ํํผ ๋ฐฉ๋ฒ์ด ์๋๊ฑด ๋ ์๋๋ค.
๊ณต์ธ IP๋ฅผ ์ฌ๊ธฐ์ ๊ธฐ์ ์๋ฉ ๊ธ์ด๋ชจ์์ ๋์๊ฐ๋ฉด์ ์๋ฉด ๋ ๋๋๊ฑฐ๊ธฐ ๋๋ฌธ์ด๋ค.
AWS, GCP ๊ฐ์ ํผ๋ธ๋ฆญ ํด๋ผ์ฐ๋ ์๋น์ค๋ฅผ ์ด๋ค๋ฉด, ์ธ์คํด์ค๋ฅผ ๋์ธ๋๋ง๋ค ์๋ก์ด ๊ณต์ธ IP๋ฅผ ๋ฐ๊ธ๋ฐ๋๋ค. ์ฐจ๋จ๋นํ๋ฉด ์๋ก ์ฌ์์ํด์ ์ฐ๋ฅด๋ฉด ๋ ๋๋ ๊ฒ์ด๋ค.
๊ฒ๋ค๊ฐ AWS Lambda, GCP Function ๊ฐ์ ์๋ฒ๋ฆฌ์ค ์๋น์ค๋ค์ ์คํ๋ ๋๋ง๋ค ๋ฌด์์์ ์์น์์ ๋ฌด์์์ ๊ณต์ธ IP๋ฅผ ๊ฐ์ง๊ณ ์ ๋์ํ๋ค. ์ด๋ฐ๊ฑธ ํ๋ก์๋ก ์ฐ๋ ๊ฒ๋ ์ธ๋งํ ๋ฐฉ๋ฒ์ด๋ค.
๊ทผ๋ฐ ๊ทธ๋ ๋ค๊ณ ํด์ ํด๋ผ์ฐ๋๋ก ์ฐ๋ฅด๋ ๊ฒ๋ ๋ง๋ฅ์ ์๋๋ค.
AWS ๊ฐ์ ์ ๋ช
ํ ํด๋ผ์ฐ๋ ์๋น์ค๋ค์ IP ๋์ญ์ ์ ํด์ ธ์๊ณ ์ ์๋ ค์ ธ์๋ค. ๊ทธ๋ฌ๋๊น, ๊ทธ ๋์ญ์ ํต์งธ๋ก ์ฌ์ ์ ์ฐจ๋จํด๋ฒ๋ฆฌ๋ฉด ๋๋ ๊ฒ์ด๋ค. ์ค์ ๋ก ๋ง์ ํฌ๋กค๋ฌ ํ์ง ์๋ฃจ์
๋ค์ด ์์ฒด์ ์ธ IP ๋ธ๋๋ฆฌ์คํธ๋ฅผ ๋ค๊ณ ์ ์ฐจ๋จ์ ๋จน์ธ๋ค. ๊ทธ๋์ ์์ฆ์ AWS IP๋ก ์ ๊ทผ์ด ์๋๋ ์๋น์ค๋ค์ด ๊ฝค ์๋ค.
CAPTCHA
๊ทผ๋ฐ ์์ฌ์ค๋ฌ์ด ํจํด์ด ๋ฐ๊ฒฌ๋์๋ค๊ณ ํด์, ๋ฌด์์ ์ฐจ๋จ์ ๊ฑธ์ด๋ฒ๋ฆฌ๋ ๊ฒ๋ ๋ง๋ฅ ์ข์ ๋ฐฉ๋ฒ์ด๋ผ๊ณ ๋ณผ ์๋ ์๋ค.
์ง์ง ์ผ๋ฐ ์ฌ์ฉ์์ธ๋ฐ ์ด์ฉ๋ค ์ฐํ์ ์ต์ธํ๊ฒ ์ฐจ๋จ๋นํ ์๋ ์๊ณ , ๊ณต์ธ IP์ ํจ๊ป ๋ฌถ์ธ ๋ฌด๊ณ ํ ํผํด์๋ค์ด ์๊ธธ ์ ์๊ธฐ ๋๋ฌธ์ด๋ค.
๊ทธ๋์ ๋ง์ ํฌ๋กค๋ฌ ์ฐจ๋จ ์๋ฃจ์
๋ค์ IP์ ์ฐจ๋จ์ ๊ฑธ๋, ์ฌ๋์์ ํ์ธํด์ ์ฐจ๋จ์ ํ์ด์ฃผ๋ ์์คํ
์ ๋์
ํ๋ค.
๊ทธ๊ฒ ๋ฐ๋ก CAPTCHA๋ผ๋ ์์คํ
์ด๋ค.

ํ๋ก๊ทธ๋จ์ด ํ๊ธฐ ์ด๋ ต๊ณ , ์ฌ๋์ ์ฝ๊ฒ ํ ์ ์๋ ๋ฌธ์ ๋ฅผ ์ถ์ ํด์, ๊ทธ๊ฑธ ํ๋ฉด ์ฐจ๋จ์ ํ์ด์ฃผ๋ ๊ฒ์ด๋ค.
์ด๊ธฐ์๋ ๋ค ์ ๋ ๊ฒ ๊ทธ๋ฅ ๊พน ๋๋ฅด๋ ์บก์ฑ ๋ผ์ ํ๊ธฐ ์ฌ์ ๋ค. ๊ทธ๋ฅ ์ ๋ ๋์ ๋์์ ์ขํ ์ฐ๊ณ ํด๋ฆญ๋ง ํด๋ ๋๋ ๊ฒ์ด๊ธฐ ๋๋ฌธ์ด๋ค.
๊ทธ๋ฐ๋ฐ ์์ฆ์๋ ๋ฌด์จ ๋๋๊ทธํด์ ํผ์ฆ์ ํธ๋ ๊ฒ๋ ์๊ณ , ๋ฐฉํฅ ๋ง์ถ๋ ๊ฒ๋ ์๊ณ , ์ผ๋ฐ์ ์ผ๋ก ์ฒ๋ฆฌํ๊ธฐ ์ด๋ ค์ด ์บก์ฑ ๋ค์ด ๋ง์์ ธ์ ๊ณต๊ฒฉ์ ์ ์ฅ์์๋ ๋งค์ฐ ์ด๋ ค์ด ๊ฒฝ์ฐ๊ฐ ๋ง์์ก๋ค.
์ด๊ฒ๋ ์์ฆ์ ๋ฅ๋ฌ๋ ๊ธฐ๋ฐ์ผ๋ก ๋ซ๋ ์๋๋ ์๊ณ ๊ณ์ํด์ ์ฐฝ๊ณผ ๋ฐฉํจ์ ์ธ์์ด ์ง์๋๊ธด ํ๋๋ฐ, ๋์ฒด๋ก๋ ๋ฐฉ์ด์๊ฐ ์ข ๋ ์ ๋ฆฌํ ๊ฒ ๊ฐ๋ค.
์ฟ ํค ๊ธฐ๋ฐ์ ํจํด ์ถ์
๋ง์ ์ฌ์ดํธ๋ ์ฟ ํค๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์ฌ์ฉ์์ ํจํด์ ์ถ์ ํ ์ ์๋ค.
๊ทธ๋ฆฌ๊ณ ์ฟ ํค๊ฐ ์์ผ๋ฉด ์ ๊ทผ์ ์์ ์ ํํ๊ธฐ๋ ํ๋ค.
์๋ฅผ ๋ค๋ฉด, ์ฟ ํค๊ฐ ๊ฐ์๋ฐ ๊ฐ์๊ธฐ ์ฌ์ฉ์์ IP ์ฃผ์๊ฐ ํ๊ตญ์์ ๋ฏธ๊ตญ IP๋ก ๋ฐ๋๋ค๊ฑฐ๋ ํ๋ฉด ๋ฐ๋ก ์์ฌ์ค๋ฌ์ด ์ฌ์ฉ์ด๋ผ๊ณ ํ๋จํ ์ ์๋ค.
๊ทธ๋์ ์ค์ ๋ก ๊ทธ๋ฐ์ง์ ํ๋ฉด ์ฐจ๋จ๋นํ๋ ์ฌ์ดํธ๊ฐ ๋งค์ฐ ๋ง๋ค.
์ด๋ฒคํธ ์ถ์
์ฌ์ฉ์์ ํ๋์จ์ด ๊ธฐ๋ฐ ํ๋์ผ๋ก ํฌ๋กค๋ฌ๋ฅผ ์๋ณํ๋ ๋ฐฉ๋ฒ๋ ์๋ค.
๋ง์ฐ์ค ์ด๋, ํด๋ฆญ, ํค ์ ๋ ฅ ๋ฑ์ ๋ฆฌ์ค๋๋ฅผ ๋ฑ๋กํด์, ์ถฉ๋ถํ ๋ง์ฐ์ค ๋์ ์ด๋ฒคํธ์ ํจ๊ป ์ฌ์ดํธ ์ด๋์ด ๋๋ค๋ฉด ์ ์์ ์ธ ์ฌ์ฉ์๋ก ํ๋จํ๊ณ , ๋ง์ฐ์ค ํด๋ฆญ ์ด๋ฒคํธ๊ฐ ์๋๋ฐ ์ฌ์ดํธ ์ด๋๋ง ๋๋ฉด ํฌ๋กค๋ง์ผ๋ก ํ๋จํ๊ณ ์ฐจ๋จ์ ๋จน์ด๋ ๊ฒ์ด๋ค.
์ค์ ๋ก Cloudflare๊ฐ ์ ๊ทน ํ์ฉํ๋ ๋ฐฉ๋ฒ์ด๋ค.
ํฌ๋กค๋ฌ ํ์ง ๊ธฐ๋ฒ: Fingerprinting
ํฌ๋กค๋ฌ๋ฅผ ํ์งํ๋ ๋ฐฉ๋ฒ์ ์ฌ๋ฌ๊ฐ์ง๊ฐ ์๋๋ฐ, ๋ ํํ๊ฒ ์ฌ์ฉํ๋ ์ ๊ทผ๋ฒ์ด Fingerprint๋ผ๋ ๊ธฐ๋ฒ์ด๋ค.
์ฝ๊ฒ ๋งํด ์ด๊ฑด ํด๋น ์์ฒญ์๋ฅผ ์๋ณํ ์ ์๋ ๊ณ ์ ๋ฒํธ ๊ฐ์๊ฑธ ๋ง๋ค์ด๋ด๋ ๊ฒ์ธ๋ฐ, ๊ณ์ธต์ด ๋ช๊ฐ์ง๊ฐ ์๋ค.
์ฌ๊ธฐ์ ๋ค๋ฃจ์ง ์์ ๊ฒ๋ ๋ช๊ฐ์ง๊ฐ ๋ ์๋ค.
์์ฐํผ ์์ง๋, ์์ฑ๋ fingerprinting์ ๊ธฐ๋ฐ์ผ๋ก ํ๋ง์ ์ ์ฅ์ด์ง์ ํฌ๋กค๋ฌ๊ฐ ํ๋ ๊ฑฐ์ง๋ง์ ์๋ณํ๋ ๊ฒ์ ์๋ค.
TLS ํ๋กํ ์ฝ ์์ค Fingerprinting
TLS ํธ๋์
ฐ์ดํฌ๋ก ์ํธํ ์์
์ ํ๋ ์์ ์ ํจํท๋ค์ ๋ถ์ํด์ Fingerprint๋ฅผ ๋ง๋๋ ๊ธฐ๋ฒ์ด๋ค.
์ด๊ฑด ๋จ์ํ ์ฌ์ฉ์๋ฅผ ๊ณ ์ ํ๊ฒ ์๋ณํ๋๊ฒ ๊ทธ์น๋๊ฒ ์๋๋ผ, ์ค์ ๋ก ๋ธ๋ผ์ฐ์ ๋ง๊ณ ์ด์ํ ํด๋ผ์ด์ธํธ๋ก ์ ๊ทผํ๋์ง๊น์ง๋ ํ์ธํ ์ ์๋ค.
์ผ๋ฐ์ ์ธ ๋ธ๋ผ์ฐ์ ๊ฐ TLS ์ํธํ๋ฅผ ์ฒ๋ฆฌํ๋ ๋ฐฉ์๊ณผ, ๊ธฐํ ํด๋ผ์ด์ธํธ๋ค์ด TLS๋ฅผ ์ฒ๋ฆฌํ๋ ๋ฐฉ์์ด ๋์ฒด๋ก ๋ฌ๋ผ์ ๊ฐ๋ฅํ ๋ถ๋ถ์ด๋ค.
๊ทธ๋์ User-Agent์ ๋ค์ด์๋ ๋ธ๋ผ์ฐ์ ์ ๋ณด์, TLS Fingerprint๋ก ์์๋ธ ๋ธ๋ผ์ฐ์ ์ ๋ณด๊ฐ ์ผ์นํ๋์ง๋ฅผ ์์๋ด๊ณ , ๋ค๋ฅด๋ค๋ฉด ํฌ๋กค๋ฌ๋ก ํ๋จํด์ ์ฐจ๋จ์ ๋จน์ผ ์ ์๋ ๊ฒ์ด๋ค. ๋ํ์ ์ผ๋ก Cloudflare๊ฐ ์ด๋ ๊ฒ ํ๋ค.
Canvas ๊ธฐ๋ฐ Fingerprinting
canvas๋ ๊ธฐํ ํ๋์จ์ด ์ ๋ณด ๋ฑ์ ํ์ฉํ๋ฉด ํด๋น ๋จธ์ ์ด๋ ๋ธ๋ผ์ฐ์ ์์๋ ๊ฑฐ์ ๊ณ ์ ํ ๊ฐ์ ์ป์ ์ ์๋ค.
์๋๋ ๊ทธ์ ๋ํ ๊ฐ๋จํ ์์ ์ฝ๋๋ค.
function generateFingerprintFromCanvas() {
// canvas ์์ฑ
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const txt = "abz190#$%^@ยฃรฉรบ";
ctx.fillStyle = "rgb(255,0,255)";
ctx.beginPath();
ctx.rect(20, 20, 150, 100);
ctx.fill();
ctx.stroke();
ctx.closePath();
ctx.beginPath();
ctx.fillStyle = "rgb(0,255,255)";
ctx.arc(50, 50, 50, 0, Math.PI * 2, true);
ctx.fill();
ctx.stroke();
ctx.closePath();
ctx.textBaseline = "top";
ctx.font = '17px "Arial 17"';
ctx.textBaseline = "alphabetic";
ctx.fillStyle = "rgb(255,5,5)";
ctx.rotate(0.03);
ctx.fillText(txt, 4, 17);
ctx.fillStyle = "rgb(155,255,5)";
ctx.shadowBlur = 8;
ctx.shadowColor = "red";
ctx.fillRect(20, 12, 100, 5);
const src = canvas.toDataURL();
return src;
}
generateFingerprintFromCanvas();
์ด๊ฑด ์ง์ง ๋์ถฉ ๋ง๋ค์ด์ ธ์๋๊ฑฐ๋ผ ํฐ ์๋ฏธ๋ ์๋๋ฐ, ์๋ฌดํผ device ์์ค์์ ๋ถ๋ณ์ธ ๊ฐ์ ๋ง๋ค ์ ์๋ค๋ ๊ฒ์ด๋ค.
Cloudflare์ ๊ฒฝ์ฐ์๋ ์ ์ ๊ธฐ๋ก์ ๊ธฐ๋ฐ์ผ๋ก User-Agent์ canvas fingerprint ๊ธฐ๋ก์ ์์๋จ๋ค๊ฐ, User-Agent์ canvas fingerprint ๊ฐ ์์ด ์ผ์นํ์ง ์์ผ๋ฉด ํฌ๋กค๋ฌ๋ก ๊ฐ์งํ๋ ํธ๋ฆญ์ ์ฌ์ฉํ๋ค.
ํค๋๋ฆฌ์ค ๋ธ๋ผ์ฐ์ ๊ฐ์ง (puppeteer)
์ด๊ฒ๋ ํ๋์ ์์๋ง ๋ค์ด๋ณด๊ฒ ๋ค. ๋๋ ์์ด ๋์ค๊ธฐ ๋๋ฌธ์ด๋ค.
์๋ฒ์์ ๋ฟ๋ ค์ฃผ๋ ํด๋ผ์ด์ธํธ์ธก ์คํฌ๋ฆฝํธ์ ํค๋๋ฆฌ์ค ๋ธ๋ผ์ฐ์ ์๋ง ์กด์ฌํ๋ ๊ฐ์ ๊ฐ์งํ๋ ์ฝ๋๋ฅผ ์ฌ์ผ๋ฉด, ์ด ๋ํ ๊ฐ๋ ฅํ ํฌ๋กค๋ง ๋ฐฉ์ด๋ฒ์ด ๋ ์ ์๋ค.
์น๋๋ผ์ด๋ฒ ํ์ฑํ ๊ฐ์ ๊ฒ์ ํต์ ๋ธ๋ผ์ฐ์ ์์๋ ์์ผ๋, puppeteer ๊ฐ์ ํค๋๋ฆฌ์ค ๋ธ๋ผ์ฐ์ ์๋ true๋ก ์กด์ฌํ๋ ๊ฐ์ด๋ค.
๊ทธ๋์ ์ ๊ฑธ ๊ฐ์งํ๋ฉด ๋ก๊ทธ๋ ์ฐ๊ณ ์ฐจ๋จ๋ ์ฐ๊ฒ ๋ง๋ค์๋ค.
์ฐจ๋จ API๋ ๋์ถฉ ์ด๋ ๊ฒ ๋ซ์๋ค.

๊ทธ๋ฆฌ๊ณ ๋ธ๋ผ์ฐ์ ์์ ๋์ฐ๋ฉด
์๋ฌด ๋ฌธ์ ์์ด ๋์ํ์ง๋ง
ํผํซํฐ์ด๋ก ๋์ฐ๋ฉด
import puppeteer from "puppeteer";
async function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function main() {
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.goto("http://localhost:3333");
await sleep(1000);
await page.setViewport({ width: 1080, height: 1024 });
const textContent = await page.evaluate(() => {
return document.querySelector("h1")?.textContent;
});
console.log(textContent);
}
main();
๋ฐ๋ก ์น๋๋ผ์ด๋ฒ๊ฐ ๊ฐ์ง๋ผ์ ์ฐจ๋จ ์คํฌ๋ฆฝํธ๊ฐ ๋์ํ ๊ฒ์ด๋ค.
์ด๊ฑด ๋น๋จ ํค๋๋ฆฌ์ค ๋ชจ๋์๋ง ํด๋น๋๋๊ฑด ์๋๊ณ , headless ๋ชจ๋๋ฅผ ๋๊ณ ์ผ๋ ๋ง์ฐฌ๊ฐ์ง๋ค.
๊ทธ๋ฆฌ๊ณ ์
๋ ๋์๊ณผ puppeteer๋ฅผ ํฌํจํ ๋๋ถ๋ถ์ ํฌ๋กค๋ง์ฉ ๋ธ๋ผ์ฐ์ ๊ฐ ์ ๋ถ ์ด ๋ฌธ์ ๊ฐ ์กด์ฌํ๋ค.
์ด ๋ฌธ์ ์ ํํด์๋ผ๋ฉด ํด๊ฒฐ๋ฒ์ด ์๋๊ฑด ์๋๋ค. ์คํฌ๋ฆฝํธ๋ฅผ ์ฐ๊ฒจ๋ฃ์ด์ ๋์ ์ผ๋ก ์ ๊ฐ์ ์กฐ์์ํฌ ์๋ ์๊ณ , ์คํ์์ ํ๋๊ทธ๋ฅผ ๋ฃ์ด์ webdriver๊ฐ ๋นํ์ฑํ๋๊ฒ์ฒ๋ผ ๋ณด์ด๊ฒ ํ ์ ์๋ค.
ํ์ง๋ง ๋ฌธ์ ๋, ์ด๋ฐ ํ์ง๋ฒ์ด ๊ฝค ๋ค์ํ๊ณ , ์ง๊ธ๋ ๋ณด๊ณ ๋๊ณ ์๋ค๋ ๊ฒ์ด๋ค.
ํค๋๋ฆฌ์ค ๋ธ๋ผ์ฐ์ ํ๊ฒฝ์๋ง ์กด์ฌํ๋ ํน์ํ ๋์์ด๋ ๊ฐ๋ค์ด ๋๋ฌด ๋ง๋ค๋ณด๋ ์ฌ๊ธฐ๋ ์ฐฝ๊ณผ ๋ฐฉํจ์ ์ธ์์ด ์ง์๋๊ณ ์๋ค.
๊ทธ๋์ ์ด๋ฐ ํด๋ผ์ด์ธํธ์ธก ๊ฒ์ฆ์ ํผํ๋ ๊ฒ์ ํํด์๋ ์์ ํค๋๋ฆฌ์ค ๋ธ๋ผ์ฐ์ ๋ฅผ ์ฐ์ง ์๊ณ HTTP Client๋ก ์ ์ ํ์ฑ์ ํ๋๊ฒ ํจ๊ณผ์ ์ผ ์ ์๋ค. ์คํฌ๋ฆฝํธ ๋์์ด ์๋๋๊น...!
๋ค์์ ๋ฐฉ๊ธ ์ฌ์ฉํ ์์ ์ฝ๋๋ค.
import puppeteer from "puppeteer";
async function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function main() {
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.goto("http://localhost:3333");
await sleep(1000);
await page.setViewport({ width: 1080, height: 1024 });
const textContent = await page.evaluate(() => {
return document.querySelector("h1")?.textContent;
});
console.log(textContent);
}
main();use std::convert::Infallible;
use std::net::SocketAddr;
use http_body_util::Full;
use hyper::body::Bytes;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Request, Response, StatusCode};
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;
const DETECT_PUPPETEER_SCRIPT: &str = r#"
<script>
let isBot = false;
if (navigator.webdriver) {
console.log(1)
isBot = true;
}
if (isBot) {
console.log("Your bot has been detected!")
fetch("/banned");
}
</script>
"#;
async fn hello(
request: Request<hyper::body::Incoming>,
) -> Result<Response<Full<Bytes>>, Infallible> {
let path = request.uri().path();
if path == "/banned" {
println!("์ฐจ๋จ๋จ!!");
let response = Response::builder()
.status(StatusCode::OK)
.body(Full::new(Bytes::from(
"<html><body><h1>Banned</h1></body></html>",
)))
.unwrap();
return Ok(response);
}
request.headers().iter().for_each(|(name, value)| {
println!("HEADER {}: {}", name.as_str(), value.to_str().unwrap());
});
let response_body = format!("<html><body><h1>OK</h1></body>{DETECT_PUPPETEER_SCRIPT}</html>")
.as_bytes()
.to_vec();
let response = Response::builder()
.status(StatusCode::OK)
.body(Full::new(Bytes::from(response_body)))
.unwrap();
Ok(response)
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let addr = SocketAddr::from(([0, 0, 0, 0], 3333));
let listener = TcpListener::bind(addr).await?;
loop {
let (stream, _) = listener.accept().await?;
let ip = stream.peer_addr()?.ip();
println!("Connection from: {}", ip);
let io = TokioIo::new(stream);
tokio::task::spawn(async move {
if let Err(err) = http1::Builder::new()
.serve_connection(io, service_fn(hello))
.await
{
println!("Error serving connection: {:?}", err);
}
});
}
}
ํฌ๋กค๋ง ๋์ ์ฌ์ดํธ๊ฐ ์ด๋ฐ ๋ฐฉ์ด๋ฒ์ ํ๋๊ฐ์ง๋ง ์ด๋ค๋ฉด ์ด๋ป๊ฒ ๋ซ์ ์ ์๊ฒ ์ง๋ง, ์ด ๋ชจ๋ ๋ฐฉ์ด๋ฒ์ ์ ์ฉํด๋จ๋ค๋ฉด ๋ซ๊ธฐ๊ฐ ์ ๋ง ์ด๋ ค์ธ ๊ฒ์ด๋ค.
์ด ์์ ํ๋ํ ๊ธธ์ด๋ค.
์ฐธ์กฐ
https://medium.com/@mayankchandel2567/how-does-cloudflare-bot-detection-work-d77179756cdc
https://murraycole.com/posts/web-crawling-stealth
https://deviceandbrowserinfo.com/learning_zone/articles/detecting-headless-chrome-puppeteer-2024
https://medium.com/@mayankchandel2567/exploring-methods-of-evading-datadomes-bot-protection-a-comprehensive-guide-for-2023-ef5274ee1698
https://piprogramming.org/articles/How-to-make-Selenium-undetectable-and-stealth--7-Ways-to-hide-your-Bot-Automation-from-Detection-0000000017.html
https://dev.gmarket.com/94