美文网首页程序员工具箱
Google reCAPTCHA 3 接入指南

Google reCAPTCHA 3 接入指南

作者: 许一沐 | 来源:发表于2021-06-09 15:24 被阅读0次

项目中有需要接入机器人防刷机制,使用了 Google reCAPTCHA 3

接入教程参考链接:


封装一个轻量的 PHP 库代码:

<?php
/**
 * Interface GRecaptchaErrorInterface
 */
interface GRecaptchaErrorInterface
{
    /**
     * Returns true if there were errors during request
     * @return bool
     */
    public function hasErrors(): bool;

    /**
     * Returns array of error messages
     * @return string[]
     */
    public function getErrors(): array;

    /**
     * Add error to the container
     * @param string $error
     * @return mixed
     */
    public function addError(string $error): void;
}

/**
 * Interface GRecaptchaHttpClientInterface
 */
interface GRecaptchaHttpClientInterface
{
    /**
     * Sends http request to Google recaptcha server
     * @param string $siteverifyUrl
     * @param string $secret
     * @param string $response
     * @param string $remoteip
     * @return GRecaptchaResponse
     */
    public function sendRequest(string $siteverifyUrl, string $secret, string $response, string $remoteip = ''): GRecaptchaResponse;
}

/**
 * Class GRecaptchaResponse
 * @author Sergey ksb@itksb.com
 */
final class GRecaptchaResponse implements GRecaptchaErrorInterface
{
    /** @var bool $success */
    private $success;
    /** @var float $score */
    private $score;
    /** @var string $hostname */
    private $hostname;
    /** @var string $action */
    private $action;

    /** @var string[] */
    private $errors = [];

    /**
     * Fill properties
     * @param bool $success
     * @param float $score
     * @param string $hostname
     * @param string $action
     * @return GRecaptchaResponse
     */
    public function fill(
        bool $success,
        float $score,
        string $hostname,
        string $action
    ): GRecaptchaResponse
    {
        $this->success = $success;
        $this->score = $score;
        $this->hostname = $hostname;
        $this->action = $action;
        return $this;
    }

    /**
     * @inheritDoc
     */
    public function hasErrors(): bool
    {
        return count($this->errors) > 0;
    }

    /**
     * @inheritDoc
     */
    public function getErrors(): array
    {
        return $this->errors;
    }

    /**
     * @inheritDoc
     */
    public function addError(string $error): void
    {
        if (mb_strlen($error)) {
            $this->errors[] = $error;
        }
    }

    /**
     * @return bool
     */
    public function isSuccess(): bool
    {
        return $this->success;
    }

    /**
     * @param bool $success
     */
    public function setSuccess(bool $success): void
    {
        $this->success = $success;
    }

    /**
     * @return float
     */
    public function getScore(): float
    {
        return $this->score;
    }

    /**
     * @param float $score
     */
    public function setScore(float $score): void
    {
        $this->score = $score;
    }

    /**
     * @return string
     */
    public function getHostname(): string
    {
        return $this->hostname;
    }

    /**
     * @param string $hostname
     */
    public function setHostname(string $hostname): void
    {
        $this->hostname = $hostname;
    }

    /**
     * @return string
     */
    public function getAction(): string
    {
        return $this->action;
    }

    /**
     * @param string $action
     */
    public function setAction(string $action): void
    {
        $this->action = $action;
    }

}

/**
 * Class GRecaptchaCurlHttpClient
 * @author Sergey ksb@itksb.com
 */
class GRecaptchaCurlHttpClient implements GRecaptchaHttpClientInterface
{
    const MISSING_INPUT_SECRET = 'missing-input-secret';
    const INVALID_INPUT_SECRET = 'invalid-input-secret';
    const MISSING_INPUT_RESPONSE = 'missing-input-response';
    const INVALID_INPUT_RESPONSE = 'invalid-input-response';
    const BAD_REQUEST = 'bad-request';
    const TIMEOUT_OR_DUPLICATE = 'timeout-or-duplicate';

    /**
     * Send validation request
     * @param string $siteverifyUrl
     * @param string $secret
     * @param string $response
     * @param string $remoteip
     * @return GRecaptchaResponse
     * @throws GRecaptchaGoogleResponseValidationException
     */
    public function sendRequest(string $siteverifyUrl, string $secret, string $response, string $remoteip = ''): GRecaptchaResponse
    {
        $returnResponse = new GRecaptchaResponse();
        /** @var bool $isValidInput */
        $isValidInput = mb_strlen($secret) && mb_strlen($response);
        if (!$isValidInput) {
            throw new InvalidArgumentException('One or more of required gRecaptcha arguments is empty.');
        }

        $validationRequestParams = ['secret' => $secret, 'response' => $response,];
        !empty($remoteip) && ($validationRequestParams['remoteip'] = $remoteip);

        $httpClient = curl_init();
        curl_setopt($httpClient, CURLOPT_URL, $siteverifyUrl);
        curl_setopt($httpClient, CURLOPT_HTTPHEADER, ['Content-Type: application/x-www-form-urlencoded;']);
        curl_setopt($httpClient, CURLOPT_TIMEOUT, 10);
        curl_setopt($httpClient, CURLOPT_POST, true);
        curl_setopt($httpClient, CURLOPT_POSTFIELDS, http_build_query($validationRequestParams));
        curl_setopt($httpClient, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($httpClient, CURLOPT_RETURNTRANSFER, true);
        /** @var string|bool $rawGoogleResponse the result on success, false on failure. */
        $rawGoogleResponse = curl_exec($httpClient);
        if (curl_errno($httpClient)) {
            $returnResponse->addError("Curl error: " . curl_error($httpClient));
            curl_close($httpClient); // release resource
            return $returnResponse;
        }
        curl_close($httpClient); // release resource

        try {
            $decodeAsAssociativeArray = true;
            $maxDepthOfDecoding = 3;
            /** @var array $response throws JsonException if something went wrong */
            $gDecodedResponse = json_decode(
                $rawGoogleResponse,
                $decodeAsAssociativeArray,
                $maxDepthOfDecoding,
                JSON_THROW_ON_ERROR
            );
        } catch (JsonException $e) {
            $returnResponse->addError("Json decoding error: " . $e->getMessage());
            return $returnResponse;
        }

        /** @var bool $responseSuccess */
        $responseSuccess = (bool)$gDecodedResponse['success'];
        /** @var float $responseScore */
        $responseScore = floatval($gDecodedResponse['score']);
        /** @var string $responseHostname */
        $responseHostname = $gDecodedResponse['hostname'];
        /** @var string $responseAction */
        $responseAction = $gDecodedResponse['action'];

        if ($responseSuccess === false) {
            if (isset($gDecodedResponse['error-codes'])) {
                /** @var string[] $responseAction */
                $responseErrorCodes = $gDecodedResponse['error-codes'];
                foreach ($responseErrorCodes as $errorCode) {
                    $returnResponse->addError(self::getErrorTextByCode($errorCode));
                }
                return $returnResponse;
            }
        }

        if (
            empty($responseSuccess) ||
            empty($responseScore) ||
            empty($responseHostname) ||
            empty($responseAction)
        ) {
            throw new GRecaptchaGoogleResponseValidationException(); // "Impossible" ? - do not trust Google - it is an evil :-)
        }

        $returnResponse->fill(
            $responseSuccess,
            $responseScore,
            $responseHostname,
            $responseAction
        );

        return $returnResponse;
    }

    /**
     * @param string $errorCode
     * @return string
     */
    public static function getErrorTextByCode(string $errorCode): string
    {
        $result = '';
        switch ($errorCode) {
            case static::INVALID_INPUT_RESPONSE :
                $result = 'The response parameter is invalid or malformed.';
                break;
            case static::MISSING_INPUT_SECRET:
                $result = 'The secret parameter is missing.';
                break;
            case static::INVALID_INPUT_SECRET:
                $result = 'The secret parameter is invalid or malformed.';
                break;
            case static::MISSING_INPUT_RESPONSE:
                $result = 'The response parameter is missing.';
                break;
            case static::BAD_REQUEST:
                $result = 'The request is invalid or malformed.';
                break;
            case static:: TIMEOUT_OR_DUPLICATE:
                $result = 'The response is no longer valid: either is too old or has been used previously.';
                break;
            default:
                $result = 'Unknown error code';
                break;
        }

        return $result;
    }

}


/**
 * Class GRecaptchaApi
 * @author Sergey ksb@itksb.com
 */
final class GRecaptchaApi
{
    /** @var string $recaptchaSiteverifyUrl */
    private $recaptchaSiteverifyUrl;
    /** @var string $recaptchaSecret */
    private $recaptchaSecret;
    /** @var float minScore */
    private $minScore;
    /** @var string $hostname */
    private $hostname;
    /** @var string $action */
    private $action;
    /** @var @var GRecaptchaHttpClientInterface $httpClient */
    private $httpClient;

    /**
     * GRecaptchaApi constructor.
     * @param string $recaptchaSecret
     * @param float $minScore
     * @param string $hostname
     */
    final public function __construct(
        string $recaptchaSiteverifyUrl,
        string $recaptchaSecret,
        float $minScore = 0.5,
        string $hostname = '',
        string $action = ''
    )
    {
        $this->setRecaptchaSiteverifyUrl($recaptchaSiteverifyUrl);
        $this->setRecaptchaSecret($recaptchaSecret);
        $this->setHostname($hostname);
        $this->setMinScore($minScore);
        $this->setAction($action);
        return $this;
    }

    /**
     * @param string $recaptchaSiteverifyUrl
     * @return $this
     */
    final public function setRecaptchaSiteverifyUrl(string $recaptchaSiteverifyUrl): GRecaptchaApi
    {
        if (empty($recaptchaSiteverifyUrl)) {
            throw new InvalidArgumentException('Recaptcha SiteverifyUrl is not set');
        };
        $this->recaptchaSiteverifyUrl = $recaptchaSiteverifyUrl;
        return $this;
    }

    /**
     * @param string $recaptchaSecret
     * @return $this
     */
    final public function setRecaptchaSecret(string $recaptchaSecret): GRecaptchaApi
    {
        if (empty($recaptchaSecret)) {
            throw new InvalidArgumentException('Recaptcha secret is not set');
        };
        $this->recaptchaSecret = $recaptchaSecret;
        return $this;
    }

    /**
     * @param string $hostname
     * @return $this
     */
    final public function setHostname(string $hostname): GRecaptchaApi
    {
        $this->hostname = $hostname;
        return $this;
    }

    /**
     * @param float $minScore
     * @return $this
     */
    final public function setMinScore(float $minScore): GRecaptchaApi
    {
        $this->minScore = $minScore;
        return $this;
    }

    /**
     * @param string $action
     * @return $this
     */
    public function setAction(string $action): GRecaptchaApi
    {
        $this->action = $action;
        return $this;
    }

    /**
     * Validates Google recaptcha
     * @param string $captchaResponse response from the client
     * @param string $clientIpAddress client IP address
     * @return bool
     */
    final public function validate(string $captchaResponse, string $clientIpAddress = ''): bool
    {
        /** @var bool $returnResult */
        $returnResult = false;
        // sanitizing input
        $captchaResponse = $this->sanitizeInCaptchaResponse($captchaResponse);
        $clientIpAddress = $this->sanitizeInClientIpAddress($clientIpAddress);

        /** @var bool $isValidInput */
        $isValidInput = mb_strlen($captchaResponse);
        if (!$isValidInput) {
            throw new InvalidArgumentException('One or more of required gRecaptcha arguments is empty.');
        }
        $httpClient = $this->getHttpClient();
        $gResponse = $httpClient->sendRequest(
            $this->recaptchaSiteverifyUrl,
            $this->recaptchaSecret,
            $captchaResponse,
            $clientIpAddress
        );
        if ($gResponse->hasErrors()) {
            throw new GRecaptchaValidationException(
                'Errors during request occured: '
                . implode('. ', $gResponse->getErrors())
            );
        }

        $returnResult = $gResponse->isSuccess();
        $returnResult = $returnResult && ($gResponse->getScore() >= $this->minScore);
        if (!empty($this->hostname)) {
            $returnResult = $returnResult && ($gResponse->getHostname() === $this->hostname);
        }
        if (!empty($this->action)) {
            $returnResult = $returnResult && ($gResponse->getAction() === $this->action);
        }

        return $returnResult;
    }

    private function sanitizeInCaptchaResponse(string $response)
    {
        return filter_var($response, FILTER_SANITIZE_STRING);
    }

    private function sanitizeInClientIpAddress(string $ip)
    {
        return filter_var($ip, FILTER_SANITIZE_STRING);
    }

    /**
     * @return GRecaptchaHttpClientInterface
     * @throws Exception
     */
    private function getHttpClient(): GRecaptchaHttpClientInterface
    {
        if (empty($this->httpClient)) {
            throw new  Exception('Http client is no set.');
        }
        return $this->httpClient;
    }

    /**
     * @param GRecaptchaHttpClientInterface $httpClient
     * @return GRecaptchaApi
     */
    final public function setHttpClient(GRecaptchaHttpClientInterface $httpClient): GRecaptchaApi
    {
        $this->httpClient = $httpClient;
        return $this;
    }


}

/**
 * Class GRecaptchaGoogleResponseValidationException
 * @author Sergey ksb@itksb.com
 */
final class GRecaptchaGoogleResponseValidationException extends Exception
{
    public function __construct()
    {
        parent::__construct('Grecaptcha resonse from Google does not contain required attributes');
    }
}

/**
 * Class GRecaptchaValidationException
 * @author Sergey ksb@itksb.com
 */
final class GRecaptchaValidationException extends Exception
{
}

前端接入代码:

<style>.grecaptcha-badge {display: none;}</style>
<script src="<?php echo $recaptcha_params['endpoint']?>/api.js?render=<?php echo $recaptcha_params['site_key']?>"></script>
<script>
            function checkSubmitByRecaptcha(){
                grecaptcha.ready(function() {
                    grecaptcha.execute("<?php echo $recaptcha_params['site_key']?>", {action: 'submit'}).then(function(token2) {
                        recaptcha_check(token2, "<?php echo $recaptcha_params['token1']?>")
                    });
                });
            }
        </script>

服务端接入:

$endpoint = getEndpoint($country_code);

        $api = new GRecaptchaApi(
            $endpoint . '/api/siteverify',
            $this->secret_key,
            0.5,
            '',
            'vakkk'
        );
        $api->setHttpClient(
            new GRecaptchaCurlHttpClient()
        );

        try {
            $result = $api->validate($token2, '');

            if ($result) {
                return true;
            }

        } catch (Exception $ex) {
            $errmsg = $ex->getMessage();
        }

private function getEndpoint($country_code)
    {
        if (is_china($country_code)) {
            return "https://www.recaptcha.net/recaptcha";
        }

        return 'https://www.google.com/recaptcha';
    }

相关文章

网友评论

    本文标题:Google reCAPTCHA 3 接入指南

    本文链接:https://www.haomeiwen.com/subject/ihueeltx.html