giftcon_dev/app/Services/RecaptchaV3.php
2026-01-19 14:45:08 +09:00

112 lines
3.5 KiB
PHP

<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Throwable;
class RecaptchaV3
{
/**
* Google reCAPTCHA v3 server-side verify
*
* @return array Google response array
*/
public function verify(string $token, string $expectedAction, ?string $ip = null): array
{
$secret = (string) config('services.recaptcha.secret');
// secret 없으면 무조건 실패(운영 안전)
if ($secret === '') {
if ($this->isDev()) {
Log::warning('[recaptcha] secret missing', [
'expected_action' => $expectedAction,
]);
}
return [
'success' => false,
'error-codes' => ['missing-input-secret'],
];
}
try {
$resp = Http::asForm()
->timeout(3) // 너무 길게 잡지 말기
->connectTimeout(2)
->retry(1, 200) // 네트워크 순간 오류 대비(1회만)
->post('https://www.google.com/recaptcha/api/siteverify', [
'secret' => $secret,
'response' => $token,
'remoteip' => $ip,
]);
$data = $resp->json();
if (!is_array($data)) {
if ($this->isDev()) {
Log::warning('[recaptcha] invalid json', [
'status' => $resp->status(),
'body_snippet' => mb_substr((string)$resp->body(), 0, 200),
'expected_action' => $expectedAction,
]);
}
return [
'success' => false,
'error-codes' => ['invalid-json'],
];
}
// (선택) 개발환경에서만 핵심만 로깅 (token/secret 절대 로그 금지)
if ($this->isDev()) {
Log::info('[recaptcha] verify ok', [
'expected_action' => $expectedAction,
'success' => $data['success'] ?? null,
'score' => $data['score'] ?? null,
'action' => $data['action'] ?? null,
'hostname' => $data['hostname'] ?? null,
'error_codes' => $data['error-codes'] ?? null,
]);
}
return $data;
} catch (Throwable $e) {
if ($this->isDev()) {
Log::error('[recaptcha] verify exception', [
'expected_action' => $expectedAction,
'message' => $e->getMessage(),
]);
}
return [
'success' => false,
'error-codes' => ['http-exception'],
];
}
}
/**
* 정책 판단: success + action 일치 + score >= min_score
*/
public function isPass(array $data, string $expectedAction): bool
{
if (!($data['success'] ?? false)) return false;
// action은 v3에서 꼭 맞춰주는 게 좋음
if (($data['action'] ?? '') !== $expectedAction) return false;
$min = (float) config('services.recaptcha.min_score', 0.5);
$score = (float) ($data['score'] ?? 0);
return $score >= $min;
}
private function isDev(): bool
{
// 네 환경명에 맞춰 추가 가능: staging 등
return app()->environment(['local', 'development', 'staging']);
}
}