실시간 소켓 통신을 하는 서버를 구현 중 봇을 막아야할 필요성을 느끼고 CloudeFlare 를 적용
사용자 경험을 개선하고 보안을 강화하기 위해 설계된 캡차(CAPTCHA) 대체 솔루션
사용자가 직접 문제를 해결하지 않아도 되도록 비침입적 방식으로 인간과 봇을 구별
기존 캡챠 방식은 사용자 경험 및 접근성에 영향을 끼치고, 봇들이 진화하면서 인간보다 더 문제를 잘 맞추게 되었다
이 시점에서 기존 캡챠 방식의 문제들을 완화한 CloudFlare turnstile 이 등장하였다
사용자가 웹페이지를 로드할 때 브라우저 환경과 행동 패턴, 네트워크 정보, 기기 정보 등을 분석한 정보들을 cloudFlare 에 전송
cloudFlare 의 머신러닝 모델이 받은 데이터들을 기반으로 봇인지 사람인지 판단
의심스러운 경우 추가적인 검증 절차를 거치게함 ( e.g. 사람이라면 체크박스를 체크해주세요. )

// index.html
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit"></script>
// Turnstile.tsx
useEffect(() => {
window.turnstile.ready(function () {
window.turnstile.render("#cfsc", {
// 발급받은 사이트 키
sitekey: `${import.meta.env.VITE_CF_SITE_KEY}`,
callback: async (token: any) => {
const response = await fetch(
`${import.meta.env.VITE_BACKEND_URL}/check-cf`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ token }),
}
);
const data = await response.json();
handleCf(data.status);
},
});
});
}, []);
return <div id="cfsc" className="mb-2" />;
app.post("/check-cf", async (req, res) => {
// 발급받은 시크릿 키
const secretKey = process.env.CF_SECRET_KEY;
// 프론트에서 callback으로 넘어온 token
const { token } = req.body;
try {
const response = await axios.post(
"https://challenges.cloudflare.com/turnstile/v0/siteverify",
{
secret: secretKey,
response: token,
// "remoteip": "사용자의 IP" // 필요시 추가
},
{
headers: { "Content-Type": "application/json" },
}
);
/*
{
"success": true,
"challenge_ts": "2022-02-28T15:14:30.096Z",
"hostname": "example.com",
"error-codes": [],
"action": "login",
"cdata": "sessionid-123456789"
}
*/
if (response.data.success) {
res.json({ status: 200, message: "success" });
} else {
res.json({ status: 500, message: "fail" });
}
} catch (e) {
console.error(e);
res.json({ status: 500, message: "error" });
}
});