239 lines
5.4 KiB
Markdown
239 lines
5.4 KiB
Markdown
# Google reCAPTCHA v3 적용 메뉴얼 (Laravel 12)
|
|
|
|
이 문서는 **PIN FOR YOU / gifticon-platform**에서 Google reCAPTCHA v3(Score 기반)를 **공통 컴포넌트 + 공통 JS + 서버 검증 Rule**로 적용/운영하는 기준을 정리합니다.
|
|
|
|
---
|
|
|
|
## 0. 목표
|
|
|
|
- reCAPTCHA v3는 **사용자 화면에 체크박스가 나타나지 않습니다**. (백그라운드 토큰+점수)
|
|
- 폼/요청마다 **action**을 지정하고 서버에서 **action 일치 + score 기준**으로 판정합니다.
|
|
- 디버깅은 `storage/logs/google_recaptcha.log` 전용 로그로 확인합니다.
|
|
|
|
---
|
|
|
|
## 1. Google 콘솔 설정 (1회)
|
|
|
|
1. Google reCAPTCHA Admin Console에서 **v3**로 사이트 등록
|
|
2. 도메인 등록 (예: `four.syye.net`, `super.pinforyou.com` 등)
|
|
3. **Site Key / Secret Key** 발급
|
|
|
|
---
|
|
|
|
## 2. 환경변수/설정 (1회)
|
|
|
|
### 2.1 `.env`
|
|
|
|
```env
|
|
RECAPTCHA_SITE_KEY=xxxx
|
|
RECAPTCHA_SECRET_KEY=yyyy
|
|
RECAPTCHA_MIN_SCORE=0.5
|
|
RECAPTCHA_LOG_LEVEL=info
|
|
```
|
|
|
|
### 2.2 `config/services.php`
|
|
|
|
```php
|
|
'recaptcha' => [
|
|
'site_key' => env('RECAPTCHA_SITE_KEY'),
|
|
'secret' => env('RECAPTCHA_SECRET_KEY'),
|
|
'min_score' => (float) env('RECAPTCHA_MIN_SCORE', 0.5),
|
|
],
|
|
```
|
|
|
|
---
|
|
|
|
## 3. 로깅 분리 (1회)
|
|
|
|
### 3.1 `config/logging.php` 채널 추가
|
|
|
|
`channels` 배열에 아래를 추가:
|
|
|
|
```php
|
|
'google_recaptcha' => [
|
|
'driver' => 'single',
|
|
'path' => storage_path('logs/google_recaptcha.log'),
|
|
'level' => env('RECAPTCHA_LOG_LEVEL', 'info'),
|
|
'replace_placeholders' => true,
|
|
],
|
|
```
|
|
|
|
로그 파일 위치:
|
|
|
|
- `storage/logs/google_recaptcha.log`
|
|
|
|
---
|
|
|
|
## 4. 공통 파일 (프로젝트 공통)
|
|
|
|
### 4.1 Layout에 스택 추가
|
|
|
|
`resources/views/.../layout.blade.php` (프로젝트 전체 레이아웃)에서 `</body>` 직전에 추가:
|
|
|
|
```blade
|
|
@stack('recaptcha')
|
|
@stack('scripts')
|
|
```
|
|
|
|
### 4.2 Blade 컴포넌트 (hidden input만)
|
|
|
|
`resources/views/components/recaptcha-v3.blade.php`
|
|
|
|
```blade
|
|
<input type="hidden" name="g-recaptcha-response" value="">
|
|
```
|
|
|
|
### 4.3 공통 JS 함수
|
|
|
|
`public/assets/js/recaptcha-v3.js`
|
|
|
|
```js
|
|
(function () {
|
|
function ensureReady() {
|
|
return new Promise((resolve, reject) => {
|
|
if (!window.grecaptcha) return reject(new Error('grecaptcha not loaded'));
|
|
window.grecaptcha.ready(resolve);
|
|
});
|
|
}
|
|
|
|
window.recaptchaV3Token = async function (action, formEl) {
|
|
if (!window.__recaptchaSiteKey) throw new Error('__recaptchaSiteKey missing');
|
|
await ensureReady();
|
|
|
|
const token = await window.grecaptcha.execute(window.__recaptchaSiteKey, { action });
|
|
|
|
if (formEl) {
|
|
const input = formEl.querySelector('input[name="g-recaptcha-response"]');
|
|
if (input) input.value = token;
|
|
}
|
|
return token;
|
|
};
|
|
})();
|
|
```
|
|
|
|
---
|
|
|
|
## 5. 서버 검증 (필수)
|
|
|
|
### 5.1 Service
|
|
|
|
`app/Services/RecaptchaV3.php`
|
|
|
|
- Google `siteverify` 호출
|
|
- 정책 판단: `success` + `action` 일치 + `score >= min_score`
|
|
|
|
> (구현은 `app/Services/RecaptchaV3.php` 파일 참고)
|
|
|
|
### 5.2 Rule
|
|
|
|
`app/Rules/RecaptchaV3Rule.php`
|
|
|
|
- Validator에서 사용
|
|
- 개발환경에서만 전용 로그 채널로 기록
|
|
|
|
---
|
|
|
|
## 6. 페이지 적용 방법 (폼마다)
|
|
|
|
### 6.1 Blade 폼 안에 hidden input 추가
|
|
|
|
```blade
|
|
<form id="someForm" onsubmit="return false;">
|
|
@csrf
|
|
<x-recaptcha-v3 />
|
|
...
|
|
</form>
|
|
```
|
|
|
|
### 6.2 해당 페이지에서만 로더 로드 (권장)
|
|
|
|
페이지 하단에 추가:
|
|
|
|
```blade
|
|
@push('recaptcha')
|
|
<script>window.__recaptchaSiteKey = @json(config('services.recaptcha.site_key'));</script>
|
|
<script src="https://www.google.com/recaptcha/api.js?render={{ config('services.recaptcha.site_key') }}"></script>
|
|
<script src="{{ asset('assets/js/recaptcha-v3.js') }}"></script>
|
|
@endpush
|
|
```
|
|
|
|
> 폼 페이지가 많아지면 `web.layouts.auth` 같은 **폼 전용 레이아웃**에서 위 push를 공통화하면 더 편합니다.
|
|
|
|
### 6.3 fetch/AJAX 요청에 토큰 포함
|
|
|
|
```js
|
|
const token = await window.recaptchaV3Token('auth_register_phone_check', form);
|
|
|
|
await fetch(url, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrf, 'Accept': 'application/json' },
|
|
body: JSON.stringify({
|
|
...payload,
|
|
'g-recaptcha-response': token,
|
|
}),
|
|
});
|
|
```
|
|
|
|
### 6.4 서버 Validator에 Rule 추가
|
|
|
|
```php
|
|
use App\Rules\RecaptchaV3Rule;
|
|
|
|
Validator::make($request->all(), [
|
|
'g-recaptcha-response' => ['required', new RecaptchaV3Rule('auth_register_phone_check')],
|
|
]);
|
|
```
|
|
|
|
**주의:** 프론트 `action` 문자열과 서버 Rule의 `action`이 **완전히 동일**해야 통과합니다.
|
|
|
|
---
|
|
|
|
## 7. Action 네이밍 규칙 (권장)
|
|
|
|
일관된 규칙으로 운영하면 로그 분석/정책 튜닝이 쉬워집니다.
|
|
|
|
- `auth_register_phone_check`
|
|
- `auth_register_terms`
|
|
- `auth_login`
|
|
- `auth_findpw_send`
|
|
- `auth_findpw_verify`
|
|
- `cs_qna_create`
|
|
|
|
---
|
|
|
|
## 8. 운영 튜닝
|
|
|
|
- `RECAPTCHA_MIN_SCORE`는 초기 `0.3~0.5` 권장
|
|
- score가 낮은 정상 사용자가 나오면 점수 기준을 낮추고, **RateLimiter/IP 제한**과 병행
|
|
|
|
---
|
|
|
|
## 9. 정상 동작 확인
|
|
|
|
### 9.1 브라우저 확인
|
|
|
|
Network 탭에서 요청 payload에 아래가 포함되어야 합니다.
|
|
|
|
- `phone`
|
|
- `g-recaptcha-response`
|
|
|
|
### 9.2 서버 로그 확인
|
|
|
|
`storage/logs/google_recaptcha.log` 예시:
|
|
|
|
- `success: true`
|
|
- `score: 0.x`
|
|
- `action` 일치
|
|
|
|
---
|
|
|
|
## 10. 실패 테스트(개발용)
|
|
|
|
프론트에서 action을 일부러 틀리게 발급:
|
|
|
|
```js
|
|
await window.recaptchaV3Token('auth_register_phone_check__FAIL_TEST', form);
|
|
```
|
|
|
|
서버에서 action mismatch로 실패해야 정상입니다.
|