[기록] reCaptcha 적용(F/E : NextJS, B/E : Spring Boot)

Geunhyung Pyun·2023년 5월 25일

기록

목록 보기
2/3

환경

  • Spring Boot 3.0.1
  • Java 17
  • Next.js 13
  • React 18.2.0

구조

  1. 클라이언트에서 site key를 들고 reCaptcha API 서버에 토큰을 요청한다.
  2. 발급받은 토큰을 서버에 회원 정보와 함께 전달한다.
  3. 서버에서는 secret key와 함께 전달받은 토큰을 reCaptcha API 서버에 보내 유효성을 검증한다.
  4. 결과를 서버에 반환한다.

구축 과정

  1. reCaptcha 발급사이트에서 어느 도메인에 사용할 것인지를 등록하고 유형과 함께 제출하여 site key, secret key를 발급받는다.

  1. 클라이언트에 먼저 reCaptcha 관련 모듈을 설치하자. 패키지 관리자를 pnpm으로 했기 때문에 명령어는 pnpm, 관리 파일이 pnpm-lock.yaml이다.
pnpm install next-recaptcha-v3
# pnpm-lock.yml
dependencies:
  next-recpatch-v3:
    specifier: 13.2.4
    version: 13.2.4(@babel/core@7.21.8)(react-dom@18.2.0)(react@18.2.0)

해당 depency가 있으면 설치가 완료된 것이다.

  1. _app.jsx(typescript인 경우 _app.tsx)에서 ReCaptchaProvider로 감싼다.
// _app.jsx
import { ReCaptchaProvider } from 'next-recaptcha-v3';

export default function App({Component, pageProps}) {
  return (
    <ReCaptchaProvider reCaptchaKey={사이트키}
      scriptProps={{
        async: true,
        defer: false,
        appendTo: "head",
        nonce: undefined
      }}
    >
      <Component {...pageProps} />
	</ReCaptchaProvider>
  )
}
  1. reCaptcha를 적용할 곳에 다음과 같이 작성한다.
const {executeRecaptcha} = useRecaptcha() // reCaptcha API 서버로 보낸다. 라는 훅을 선언

const token = executeRecaptcha('login') // reCaptcha API 서버에 토큰 발급을 요청. 안에 있는 값은 실행한 액션 값으로 임의로 주면 된다.

이를 로그인에 사용하면 이렇게 된다.

// 모듈
import { useCallback } from "react";
import { useForm } from "react-hook-form";
import axios from "axios";
import { useReCaptcha } from "next-recaptcha-v3";

const Login = () => {
  const {handleSubmit, register, reset} = useForm()
  const {executeRecaptcha} = useRecaptcha()
  
  const onSubmit = useCallback(async (data) => {
    const token = executeRecaptcha('login') // reCaptcha API 서버에 토큰을 요청. 토큰을 받는다.
    const sendData = {
      token: token,
      id: data.id,
      password: data.password
    } // 토큰을 사용자 정보와 함께 보낸다
    
    axios.post(`{process.env.NEXT_PUBLIC_URL}/login`, sendData, {
      withCredentials: true
    }).then((res) => {
      console.log(res)
    }).catch((error) => {
      console.log(error)
    })
  }, [executeRecaptcha])
  // 발급받은 토큰과 사용자 정보를 백엔드로 보낸다.
  
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <input type="text"
          placeholder="id"
          {...register("userId")} />
      </div>
      <div>
        <input type="password"
          placeholder="pw"
          {...register("password")} />
      </div>
      <button type="submit" >login</button>
    </form>
  )
}

export default Login
  1. 백엔드에서는 reCaptcha와 관련된 dependecy는 설치하지 않았다. 다만 하드코딩을 피하기 위해 application.yml에 설정을 해놨다. 설정한 항목은 reCaptcha 인증 url, secret key이다. 키 이름은 상관이 없다.
recaptcha:
  secret_key: 시크릿 키
  verify_url: "https://www.google.com/recaptcha/api/siteverify"
  1. 프론트에서 요청한 컨트롤렁에서 받은 토큰을 이용해서 서비스 로직에서는 다음과 같이 작성한다.
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@Service
public class CaptchaService {
	@Value("${recaptcha.verify_url}")
    private String url;
    @Value("${recaptcha.secret_key}")
    private String key;
    
    private static final double HALF = 0.5;
    
    public boolean verifyToken(String token) { // token은 프론트에서 보낸 요청받은 token이다.
    	try {
            HttpHeaders httpHeaders= new HttpHeaders();
            httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

            MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
            map.add("secret", key);
            map.add("response", token);

            HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map, httpHeaders);
            RestTemplate restTemplate = new RestTemplate();

            ResponseEntity<String> response = restTemplate.postForEntity(url, request, String.class); // 해당 url로 token과 secret key를 전송. 유효성 검증.

            JsonObject jsonObject = JsonParser.parseString(response.getBody()).getAsJsonObject();
            return String.valueOf(jsonObject.get("success")).equals("true") && Double.parseDouble(String.valueOf(jsonObject.get("score"))) >= HALF;
            // success이거나 점수가 0.5 이상인 경우 통과
        } catch (Exception e) {
            return false;
        }
    }
    
}

response의 body에는 이렇게 결과가 떨어진다.

{
"success": true,
"challenge_ts": "2023-05-25T06:46:19Z",
"hostname": "localhost",
"score": 0.9,
"action": "login"
}

score는 0~1 사이의 값으로 1에 가까우면 인간이라는 뜻이다.

참고

https://tony950620.tistory.com/105

profile
개발자를 원하는 사람.

2개의 댓글

comment-user-thumbnail
2024년 7월 22일

Can you push your all codes into github?

Thanks

1개의 답글