nextjs 13 google recaptcha 적용 및 text recaptcha 만들기

이명진·2024년 1월 22일
0
post-thumbnail

제작 이유

원래 안드로이드 개발이나, 앱 개발에서 google - recaptcha를 사용할수 있었다.
찾아보니 ios는 불가하다고 알려져 있다.

recaptcha 란 무엇인가?

나도 처음엔 몰랐는데 가끔 보면 봇이 아닙니다. 하고 이미지에서 무엇을 찾아라 하고 등장하는 것이 있다.
이것이 capcha 이다. 그리고 구글에서 만든게 re-captcha 이다.

이게 앱에서 사용할수 있게 라이브러리가 있는데 이제 웹앱이 자주 사용되는 개발이 많아지기도 해서
관리하기 불편하니 이제 서비스를 중단하고 웹앱으로 관리를 하라고 v3 버전이 나오게 되었다.
사진 찾기는 v2 버전이다.

v2 버전과 v3 버전의 차이점은
v2 버전은 이미지를 찾는것이고 v3 버전은 이제 구글이 봇인지 아닌지 특정 행동을 통해서 점수를 측정해서 전달해준다. 값은 0~1 값이고 0일수록 봇일 가능성이 높은 것이다.

이 값은 어떻게 측정하는지 잘 모르겠다.

아무튼 내가 지금 맡은 프로젝트가 해외에서 사용되는 앱이기 때문에 봇 공격이 잦았는데
이를 막기위해서는 captcah를 사용한다. 하지만 이제 v2 서비스가 중단되기 때문에 웹앱으로 관리를 하기 위해 v3를 적용해야 한다. 그래서 captcha에 대해서 공부하게 되었고 코드에 적용해 볼수 있었다.

만일 우리나라 앱이 었다면 휴대폰 본인인증이 있어서 봇따위는 여기서 차단할수 있기 때문에 우리 서비스에서는 captcha가 잘 사용되지 않는다. 생각해보니 웹에서도 사용할때 봇이 아닙니다 이용할때는 디스코드 같이 해외 에서 만든 곳에서 많이 해본것 같다.

쭉 공부하면서 기사를 찾다보니 요새는 봇들이 captcah를 더 빠르고 잘 푼다는 기사를 봤는데. 역시나 bot을 막기에는 조금더 어려운 방법이 필요할것 같다. 이렇게 되면 핸드폰 본인인증을 사용해서 봇을 막는 우리나라가 짱짱맨인것 같긴하다.

아무튼 개발에 대해서 넘어간다.

구글 공식문서에서는 node형식으로 코드를 알려준다. script로 코드를 관리하고 구글의 api를 찌르면 되는 방식이다.
자세한 방법은 공식문서를 참고해보자
https://developers.google.com/recaptcha/docs/v3?hl=ko

스크립트로 관리하는 것보다 라이브러리가 편할것 같아서 라이브러리를 선택하게 되었다.

라이브러리로 다행히 next-recaptcha-v3가 있어서 활용할수 있었다.
아래 명령어로 설치하면된다.

yarn add next-recaptcha-v3
npm install next-recaptcha-v3 --save

공식문서에 또한 코드의 예시가 있어서 공식 npm 문서를 참조해서 작성해도 완벽하다

https://www.npmjs.com/package/next-recaptcha-v3

하지만 검색을 통해서 자체 api를 통해서 코드를 작성하는 방법을 찾게 되었고 마음에 들어서
이분의 글을 참고해서 코드를 작성했다. 글 설명도 친절하다
https://peterkellner.net/2023/09/18/How-to-Use-Googles-ReCaptcha-V3-with-NextJS-13-and-the-New-App-Router/

회사 코드를 첨부할수 없어서 이분의 글의 코드를 빌려와서 코드를 정리해본다.

이분은 로그인 부분에서 captcha를 사용하고 싶어 하는것 같아서 로그인 부분에 구현을 하였다.
나는 따로 컴포넌트화 시켜서 사용하고 싶은 부분에서 사용하기로 했다.

//detail.ts
import axios from "axios";
import { useReCaptcha } from "next-recaptcha-v3";

  const { executeRecaptcha } = useReCaptcha();
  const [hearFromSponsors, setHearFromSponsors] = useState(false);
  const onHandleClick = () => {
    executeRecaptcha("enquiryFormSubmit").then((gReCaptchaToken) => {
      postCaptcha(gReCaptchaToken);
    });
  };
  const postCaptcha = async (gReCaptchaToken: string) => {
    try {
      const body: PostData = {
        firstName: "abc",
        lastName: "abc",
        email: "email@test.com",
        hearFromSponsors: hearFromSponsors,
        gRecaptchaToken: gReCaptchaToken,
      };
      const res = await axios.post("/api/contactFormSubmit", body);
      if (res.data.success && res.data.score > 0.5) {
        router.replace(“원하는 페이지 ”);
      }
    } catch (error) {
	
    }
  };

페이지 부분이다. 나는 라이브러리를 next-recaptcha-v3 썼기 때문에 improt 부분에서 다르다 .
useReCaptcha 훅을 통해서 executeRecaptcha를 따로 빼준다. 이 함수를 사용해서 토큰 값을 가져오는데 파라미터 값은 아무 값이나 입력해도 무방한것 같다.

토큰 값을 받아오고 나서 자체 api를 만든 값에 post형식으로 토큰을 전달해 준다.
결과 값에서 score에 0~1 값이 나오는데 이를 통해 처리하면 된다.
나는 0.5 이상만 뽑히길래 테스트를 위해서 일단 0.5이상일 경우를 분기 처리하였다.
‘인간이다’ 라고 결과 값을 준것인데 0.5이하의 값은 안나와서 테스트 해볼수가 없다.
라이브러리를 사용하니 훨씬 코드가 간단해졌다.

이 코드를 detail페이지 코드라고 칭하겠다.

detail페이지 코드를 또 감싸주어야 한다.
감싸는 컴포넌트는 아래와 같다

//wapper.ts
import { ReCaptchaProvider } from "next-recaptcha-v3";
type Props = {
  children: React.ReactNode | JSX.Element;
};
export default function GoogleCaptchaWrapper({ children }: Props) {
  const SiteKey = process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY;
  return (
    <ReCaptchaProvider reCaptchaKey={SiteKey}>{children}</ReCaptchaProvider>
  );
}

next-recaptcha-v3에서 ReCaptchaProvider 컴포넌트로 detail을 감싸준다. 이때 siteKey값이 필요한데
key값은 구글 recaptcah 공식문서에서 사이트를 저장하면 토큰값을 주는데 그 값을 사용하면된다.

자체 api부분이다. next api를 사용해서 자체 api를 제작했다.

import { NextResponse } from "next/server";
import axios from "axios";

export async function POST(request: Request, response: Response) {
  const secretKey = process?.env?.RECAPTCHA_SECRET_KEY; // 구글 recaptcha공식문서에서 사이트를 저장하면 주는 값이다. 

  const postData = await request.json();
  const { gRecaptchaToken, firstName, lastName, email, hearFromSponsors } =
    postData; // api를 호출하는  detail부분의 파라미터 값이다. 토큰값만 확인하자. 



  let res: any;
  const formData = `secret=${secretKey}&response=${gRecaptchaToken}`;
  try {
    res = await axios.post(
      "https://www.google.com/recaptcha/api/siteverify",
      formData,
      {
        headers: {
          "Content-Type": "application/x-www-form-urlencoded",
        },
      }
    );
  } catch (e) {
    console.log("recaptcha error:", e);
  }

  if (res && res.data?.success && res.data?.score > 0.5) {
    // Save data to the database from here
   // 성공한 부분 
    return NextResponse.json({
      success: true,
      firstName, lastName,
      score: res.data?.score,
    });
  } else {
    console.log("fail: res.data?.score:", res.data?.score);
    return NextResponse.json({ success: false, name, score: res.data?.score });
  }
}

이렇게 하면 v3를 웹으로 만들어 볼수 있다. 간단하기는 하지만 이게 정확하게 봇인지 아닌지 판단하는 것은 잘 모르겠다.

만약 사람이래도 추가적인 방법으로 체크하는 방법이 필요했는데 간단하면서도 강력한 텍스트 입력 방법을 나는 적용해서 코드를 작성해 보았다.

내가 제작한 코드의 구현은 아래와 같다

입력하고 submit을 누르면 alert이 뜨는데 gif로 촬영할때 alert이 다른 위치에 떠서 촬영되지 않았다.. reload누르면 변경되고 submit을 눌러 성공하면 다음 로직을 실행하고 만약 틀리면 input값이 삭제되고 테스트문자가 초기화 된다.

이 textvalidate 캡챠 부분은 검색하면 많이 나오기 때문에 상세한 방법은 생략한다.
나도 어느 분의 코드를 쭉 보고 nextjs 13버전에 맞게 적용시켰다.

핵심 코드만 가져와서 정리해본다.

// random으로 숫자를 지정하는 랜덤 제너레이터 
 const randomNumber = (min: number, max: number) =>
    Math.floor(Math.random() * (max - min + 1) + min);

  //랜덤된 숫자를 가지고 아스키코드를 활용해  텍스트를 만든다. 
  const textGenerator = () => {
    let generatedText = "";
    /* String.fromCharCode gives ASCII value from a given number */
    // total 9 letters hence loop of 3
    for (let i = 0; i < 3; i++) {
      //65-90 numbers are capital letters
      generatedText += String.fromCharCode(randomNumber(65, 90));
      //97-122 are small letters
      generatedText += String.fromCharCode(randomNumber(97, 122));
      //48-57 are numbers from 0-9
      generatedText += String.fromCharCode(randomNumber(48, 57));
    }
    return generatedText;
  };
  function drawStringOnCanvas(string: string) {
    if (!canvasRef.current) {
      return;
    }

    //canvas 를 이용해서 코드들을 그려주는 함수이다. 
    let ctx = canvasRef.current.getContext("2d");
    if (ctx) {
      //clear canvas
      ctx?.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
      const textColors = ["rgb(0,0,0)", "rgb(130,130,130)"];
      const letterSpace = 150 / string.length;
      //loop through string
      for (let i = 0; i < string.length; i++) {
        //Define initial space on X axis
        const xInitialSpace = 25;
        //Set font for canvas element
        ctx.font = "20px Roboto Mono";
        ctx.fillStyle = textColors[randomNumber(0, 1)];
        ctx.fillText(
          string[i],
          xInitialSpace + i * letterSpace,
          randomNumber(25, 40),
          100
        );
      }
    }
  }

출처 : https://www.youtube.com/watch?v=8T-Ao6jiqhw
유튜브 영상으로 작업 내용을 보여준다. 쭉 영상을 봐도 좋고 코드를 올려놓은 곳을 참고해도 좋다.

이렇게 captcha 부분을 완료 할수 있었다.

profile
프론트엔드 개발자 초보에서 고수까지!

0개의 댓글