bot-protection-botd 삽질 기록

cansweep·2022년 7월 25일
0
post-thumbnail

저번 포스팅에 회원관리와 관련된 페이지를 제작하며 무분별한 인증을 하지 않도록 막는 작업이 필요해 reCAPTCHA를 적용했다고 했는데 팀원들 사이에서도 v2와 v3에 대해 의견이 반으로 나뉘어 결국 현업에 계신 분께 무엇을 사용하면 좋을지 물어보았다.

answer

그랬더니 반드시 CAPTCHA를 사용해야 하나요? 라는 답변이 돌아왔다.

CAPTCHA로 모든 것을 막을 수는 없기에 보안을 좀 더 추가하고자 위의 내용을 꼭 적용해보고 싶었고 추천해주신 방법을 하나씩 살펴보고 있는데...

example

짜잔! 예시를 보고 알아서 하세요!

example은 처음 보는 거라 이렇게 진짜 딱 예시 알아서 보라고 할 줄 몰랐고... 다른 사람이 짠 코드를 아무 것도 모르는 상태에서 하나씩 이해하려니 어디부터 봐야하는지, 뭐가 어떻게 동작하는지 하나도 눈에 들어오지 않고 탈탈 털린 채로 한 이틀은 그렇게 보낸 것 같다.

sad

👀 Botd 선택 이유

일단 악의적인 봇을 이용한 접근을 막는 방법에는 datadome과 botd를 사용하는 두 가지 예시가 주어졌다.

이중 botd를 선택한 이유는 간단했다.

datadome과 botd 모두 선택하려면 api key를 발급받아야 했는데 datadome은 홈페이지에 들어가자마자 Start a 30-day free trial! 이라는 문구가 날 반겼기 때문이다.

botd

반대로 botd는 홈페이지에서 이메일만 입력하면 바로 public key와 secret key를 발급받을 수 있다.

Botd란?

Botd는 자바스크립트 봇 탐지를 위한 브라우저 라이브러리이다.
자동화 툴, 브라우저 스푸핑, 가상 머신을 쉽게 탐지해낼 수 있다.

🔍 bot-protection-botd를 분석해보자

vercel에서는 examples의 edge-functions에서 botd를 사용하는 bot-protection 예시를 제공한다.

bot-protection-botd

나는 Next.js를 이용해 프로젝트를 진행 중이기 때문에 위 예제를 참고해 botd를 프로젝트에 적용하기로 했다.

1. .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를 아무거나 넣어보면 어느 키를 써야하는지 에러로 친절하게 알려준다.

2-1. botd script 적용하기

예제를 분석해본 결과 botd를 적용하는 방법은 두 가지가 있다.

  1. script로 적용하기 => botd를 적용할 곳에 Script를 추가한다.
  2. middleware로 적용하기 => middleware.ts에 botd를 사용해 봇의 접근을 아예 차단한다.

우선 첫 번째 방법인 script로 적용하는 방법부터 알아보도록 하자.

첫 번째 방법을 적용하기 위해 bot-protection-botd에서 확인해야 할 부분은 아래와 같다.

lib
  ㄴbotd
    ㄴconstants.ts
    ㄴscript.tsx
pages
  ㄴ_app.tsx
components
  ㄴbotd-result.tsx

lib/botd/constant.ts

constants.ts는 botd에 대한 상수를 export하는 파일이다.

lib/botd/script.tsx

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>

그리고 얻은 결과는 다음과 같다.

not-bot

bot

위의 결과가 봇이 아닐 때이고 아래 결과가 봇일 때이다.

따라서 이 결과를 보고 bot이 탐지되었을 때 어떤 처리를 할 지 추가해주면 된다.

2-2. middleware로 적용하기

이 방법은 botd 컴포넌트를 사용하는 대신 middleware를 이용해 페이지 자체에 botd를 추가한다.

두 번째 방법을 적용하기 위해 bot-protection-botd에서 확인해야 할 부분은 아래와 같다.

lib
  ㄴbotd
    ㄴconstants.ts
    ㄴindex.ts
middleware.ts
tsconfig.json

lib/botd/index.ts

botdEdge와 botd 함수를 export한다.
사용할 목적에 따라 적절히 사용하면 된다.

공통적으로 토큰을 확인한 후 url에 따른 botd 결과를 반환한다.

tsconfig.json

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 옵션을 추가한다.

middleware.ts

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하여 봇의 접근을 막았다.

script vs middleware

두 가지 방법을 비교해보자면 아래와 같다.

  • script
    • 특정 페이지에 botd를 추가할 수 있다.
    • 페이지마다 bot 탐지 시 해야 할 동작을 설정할 수 있다.
    • 하지만 bot 탐지 결과가 나올 때까지 접근을 막고자 하는 페이지가 노출된다.
  • middleware
    • 전체 페이지에 botd를 추가할 수 있다.
    • 페이지마다 동작을 지정할 수는 없지만 공통적인 처리가 가능하다.
    • 페이지 로드 전에 botd를 통한 처리가 끝나므로 봇에게 페이지를 노출하지 않는다.
profile
하고 싶은 건 다 해보자! 를 달고 사는 프론트엔드 개발자입니다.

0개의 댓글