저번 포스팅에 회원관리와 관련된 페이지를 제작하며 무분별한 인증을 하지 않도록 막는 작업이 필요해 reCAPTCHA를 적용했다고 했는데 팀원들 사이에서도 v2와 v3에 대해 의견이 반으로 나뉘어 결국 현업에 계신 분께 무엇을 사용하면 좋을지 물어보았다.
그랬더니 반드시 CAPTCHA를 사용해야 하나요? 라는 답변이 돌아왔다.
CAPTCHA로 모든 것을 막을 수는 없기에 보안을 좀 더 추가하고자 위의 내용을 꼭 적용해보고 싶었고 추천해주신 방법을 하나씩 살펴보고 있는데...
짜잔! 예시를 보고 알아서 하세요!
example은 처음 보는 거라 이렇게 진짜 딱 예시 알아서 보라고 할 줄 몰랐고... 다른 사람이 짠 코드를 아무 것도 모르는 상태에서 하나씩 이해하려니 어디부터 봐야하는지, 뭐가 어떻게 동작하는지 하나도 눈에 들어오지 않고 탈탈 털린 채로 한 이틀은 그렇게 보낸 것 같다.
일단 악의적인 봇을 이용한 접근을 막는 방법에는 datadome과 botd를 사용하는 두 가지 예시가 주어졌다.
이중 botd를 선택한 이유는 간단했다.
datadome과 botd 모두 선택하려면 api key를 발급받아야 했는데 datadome은 홈페이지에 들어가자마자 Start a 30-day free trial! 이라는 문구가 날 반겼기 때문이다.
반대로 botd는 홈페이지에서 이메일만 입력하면 바로 public key와 secret key를 발급받을 수 있다.
Botd는 자바스크립트 봇 탐지를 위한 브라우저 라이브러리이다.
자동화 툴, 브라우저 스푸핑, 가상 머신을 쉽게 탐지해낼 수 있다.
vercel에서는 examples의 edge-functions에서 botd를 사용하는 bot-protection 예시를 제공한다.
나는 Next.js를 이용해 프로젝트를 진행 중이기 때문에 위 예제를 참고해 botd를 프로젝트에 적용하기로 했다.
.env.local
수정botd를 적용하기 앞서 제일 먼저 해야할 것은 api key를 발급받은 후 .env.local 파일에 넣어주는 것이다.
예시에서는 .env.example 파일을 확인할 수 있는데 이 내용을 복붙하거나 아래 명령어를 사용해 .env.local 파일로 바꿔주면 된다.
cp .env.example .env.local
api key를 발급받을 때 public key와 secret key의 두 가지 key가 발급되는데 .env에 들어가는 key는 한 가지이다.
여기서 잠깐 무엇을 넣어야 하는지 고민했는데 client에서만 사용할 것이기 때문에 public key를 넣어주었다.
+++
requestId를 사용하는 경우(index.ts>botd function
)에는 secret key를, 이외의 경우에는 public key를 사용한다.
헷갈릴 경우 key를 아무거나 넣어보면 어느 키를 써야하는지 에러로 친절하게 알려준다.
예제를 분석해본 결과 botd를 적용하는 방법은 두 가지가 있다.
middleware.ts
에 botd를 사용해 봇의 접근을 아예 차단한다.우선 첫 번째 방법인 script로 적용하는 방법부터 알아보도록 하자.
첫 번째 방법을 적용하기 위해 bot-protection-botd에서 확인해야 할 부분은 아래와 같다.
lib
ㄴbotd
ㄴconstants.ts
ㄴscript.tsx
pages
ㄴ_app.tsx
components
ㄴbotd-result.tsx
constants.ts는 botd에 대한 상수를 export하는 파일이다.
script.tsx는 botd provider를 제공하고 botd script를 통해 botd를 load한다.
const Botd: FC<ScriptProps> = ({ children, onLoad, ...scriptProps }) => {
const [ready, setReady] = useState(false)
return (
<BotdReady.Provider value={ready}>
{children}
<BotdScript
{...scriptProps}
onLoad={(e) => {
setReady(true)
if (onLoad) onLoad!(e)
}}
/>
</BotdReady.Provider>
)
}
export default Botd
먼저 export되는 컴포넌트를 확인해보면 위와 같다.
BotdReady.Provider로 감싸져 botd를 사용할 준비가 되어있는지 먼저 확인한다.
그리고 onLoad event handler에서 준비 상태를 true로 바꿔준 뒤 해당 이벤트를 실행한다.
이렇게만 보면 이해는 잘 가지 않는데 _app.tsx
를 확인하면 Botd 컴포넌트가 무슨 일을 하는지 알 수 있다.
// _app.tsx
return (
<Botd
onLoad={() => {
// You can do a general page check here with Botd. We
// are skipping this for demo purposes because each
// page is calling botDetect() and logging the result
//
// await botDetect()
}}
>
<Layout
title="Bot Protection with Botd (by FingerprintJS)"
path="edge-functions/bot-protection-botd"
deployButton={{ env: ['NEXT_PUBLIC_BOTD_API_TOKEN'] }}
>
<Component {...pageProps} />
</Layout>
</Botd>
)
예시에서는 모든 페이지에 사용하고자 _app.tsx에 Botd 컴포넌트를 추가했지만 일부분에서만 쓰일 것이라면 해당하는 컴포넌트나 페이지를 Botd 컴포넌트로 감싼 후 onLoad event handler로 botDetect
함수를 사용하면 된다.
botd의 동작 결과를 확인하기 위해 특정 페이지에 Botd를 사용해 보았다.
<Botd
onLoad={() => {
botDetect().then((result: any) => console.log(result));
}}>
<Container>
<LoginForm />
</Container>
</Botd>
그리고 얻은 결과는 다음과 같다.
위의 결과가 봇이 아닐 때이고 아래 결과가 봇일 때이다.
따라서 이 결과를 보고 bot이 탐지되었을 때 어떤 처리를 할 지 추가해주면 된다.
이 방법은 botd 컴포넌트를 사용하는 대신 middleware를 이용해 페이지 자체에 botd를 추가한다.
두 번째 방법을 적용하기 위해 bot-protection-botd에서 확인해야 할 부분은 아래와 같다.
lib
ㄴbotd
ㄴconstants.ts
ㄴindex.ts
middleware.ts
tsconfig.json
botdEdge와 botd 함수를 export한다.
사용할 목적에 따라 적절히 사용하면 된다.
공통적으로 토큰을 확인한 후 url에 따른 botd 결과를 반환한다.
index.ts를 그대로 가져올 경우 다음의 에러를 확인할 수 있다.
Type 'Headers' can only be iterated through when using
the '--downlevelIteration' flag or with a '--target' of 'es2015' or higher.ts(2802)
이 에러를 해결하기 위해 tsconfig.json에 "downlevelIteration": true
옵션을 추가한다.
import { NextRequest, NextResponse } from "next/server";
import { botdEdge } from "@lib/botd";
export default async function middleware(req: NextRequest) {
// Do light bot detection for all paths
const res = await botdEdge(req, {
// The request id is excluded for demo purposes because
// Botd remembers your request id and will always show
// you the /bot-detected page if you're a bot, and
// never if you have been identified as a human
useRequestId: false,
});
if (res && res.status !== 200) {
// Bot detected!
req.nextUrl.pathname = "/";
const rewrite = NextResponse.rewrite(req.nextUrl);
// Move Botd headers to the rewrite response
res.headers.forEach((v, k) => rewrite.headers.set(k, v));
return rewrite;
}
return res;
}
middleware에 botd를 호출하는 로직을 넣음으로써 모든 페이지에 대해 bot을 탐지하는 과정이 추가된다.
이후 bot이 탐지됐다면 bot 탐지 후 원하는 동작을 추가하면 되는데 예시에서는 지정된 url인 "/"로 rewrite하여 봇의 접근을 막았다.
두 가지 방법을 비교해보자면 아래와 같다.