编程

[Laravel 扩展包]Laravel Honeypot(蜜罐):防止垃圾信息通过表单提交

608 2024-05-23 18:44:00

当将表单添加到公共网站时,垃圾信息机器人可能会试图使用虚假值提交表单。幸运的是,这些机器人中的大多数都相当愚蠢。您可以通过在表单中添加一个不可见的字段来阻止其中的大多数操作,该字段在提交时永远不应该包含值。这样的字段被称为蜜罐。这些垃圾信息机器人只会填充所有字段,包括蜜罐。

当提交的蜜罐字段已满时,此包将丢弃该请求。除此之外,这个包还检查提交表单所花的时间。这是通过在另一个不可见字段中使用时间戳来完成的。如果表单在短得离谱的时间内提交,反垃圾信息也会被触发。

安装完该包后,你只需将 x-honeypot blade 组件添加到表单中。

<form method="POST">
    <x-honeypot />
    <input name="myField" type="text">
</form>

该包也支持手动传递必要的值到视图层,这样你可以简单地将 honeypot 字段添加到 Inertia 驱动的应用中。

安装

通过 Composer 安装该包:

composer require spatie/laravel-honeypot

可选地,你可以发布该包的配置文件。

php artisan vendor:publish --provider="Spatie\Honeypot\HoneypotServiceProvider" --tag=honeypot-config

配置文件内容将会发布到 config/honeypot.php:

use Spatie\Honeypot\SpamResponder\BlankPageResponder;

return [
    /*
     * Here you can specify name of the honeypot field. Any requests that submit a non-empty
     * value for this name will be discarded. Make sure this name does not
     * collide with a form field that is actually used.
     */
    'name_field_name' => env('HONEYPOT_NAME', 'my_name'),

    /*
     * When this is activated there will be a random string added
     * to the name_field_name. This improves the
     * protection against bots.
     */
    'randomize_name_field_name' => env('HONEYPOT_RANDOMIZE', true),

    /*
     * When this is activated, requests will be checked if
     * form is submitted faster than this amount of seconds
     */
    'valid_from_timestamp' => env('HONEYPOT_VALID_FROM_TIMESTAMP', true),
    
    /*
     * This field contains the name of a form field that will be used to verify
     * if the form wasn't submitted too quickly. Make sure this name does not
     * collide with a form field that is actually used.
     */
    'valid_from_field_name' => env('HONEYPOT_VALID_FROM', 'valid_from'),

    /*
     * If the form is submitted faster than this amount of seconds
     * the form submission will be considered invalid.
     */
    'amount_of_seconds' => env('HONEYPOT_SECONDS', 1),

    /*
     * This class is responsible for sending a response to requests that
     * are detected as being spammy. By default a blank page is shown.
     *
     * A valid responder is any class that implements
     * `Spatie\Honeypot\SpamResponder\SpamResponder`
     */
    'respond_to_spam_with' => BlankPageResponder::class,

    /*
     * This class is responsible for applying all protection
     * rules for a request. By default uses `request()`.
     *
     * It throws the `Spatie\Honeypot\ExceptionsSpamException` if the
     * request is flagged as spam, or returns void if it succeeds.
     */
    'spam_protection' => \Spatie\Honeypot\SpamProtection::class,

    /*
     * When activated, requests will be checked if honeypot fields are missing,
     * if so the request will be stamped as spam. Be careful! When using the
     * global middleware be sure to add honeypot fields to each form.
     */
    'honeypot_fields_required_for_all_forms' => false,

    /*
     * This switch determines if the honeypot protection should be activated.
     */
    'enabled' => env('HONEYPOT_ENABLED', true),
];

使用

首先,你需要将 x-honeypot Blade 组件添加到你想保护的表单中

<form method="POST" action="{{ route('contactForm.submit') }}")>
    <x-honeypot />
    <input name="myField" type="text">
</form>

此外,你也可以使用 @honeypot Blade 指令:

<form method="POST" action="{{ route('contactForm.submit') }}")>
    @honeypot
    <input name="myField" type="text">
</form>

使用 Blade 组件或指令都会添加两个字段: my_namevalid_from_timestamp(你可以再配置文件中修改其名称)。

接下来,你必须在路由中使用处理表单提交的 Spatie\Honeypot\ProtectAgainstSpam 中间件。该中间件将拦截任何含有 my_name 非空值的请求。如果提交请求的速度快于包在 valid_from_timestamp 中生成的加密时间戳,它也会拦截该请求。

use App\Http\Controllers\ContactFormSubmissionController;
use Spatie\Honeypot\ProtectAgainstSpam;

Route::post('contact', [ContactFormSubmissionController::class, 'create'])->middleware(ProtectAgainstSpam::class);

如果你想集成 Spatie\Honeypot\ProtectAgainstSpam 与 Laravel 内置认证路由,可以使用合适的中间件组包装 Auth::routes() 声明(确保将 @honeypot 指令添加到认证表单中)。

use Spatie\Honeypot\ProtectAgainstSpam;

Route::middleware(ProtectAgainstSpam::class)->group(function() {
    Auth::routes();
});

如果应用中有很多由许多不同的控制器处理的表单,你可以选择将其注册为全局中间件。

// inside app\Http\Kernel.php

protected $middleware = [
   // ...
   \Spatie\Honeypot\ProtectAgainstSpam::class,
];

在 Inertia 中使用

使用 Inertia 时,必须手动传递蜜罐字段中使用的值。以下是一个示例:

// in a controller
public function create(\Spatie\Honeypot\Honeypot $honeypot) 
{
    return inertia('contactform.show', [
        'honeypot' => $honeypot,
    ]);
}

你的前端将获得一个带有以下键名的 honeypot 对象:enablednameFieldNamevalidFromFieldNameencryptedValidFrom

以下是如何使用 Vue 渲染这些值的示例:

<div v-if="honeypot.enabled" :name="`${honeypot.nameFieldName}_wrap`" style="display:none;">
    <input type="text" v-model="form[honeypot.nameFieldName]" :name="honeypot.nameFieldName" :id="honeypot.nameFieldName" />
    <input type="text" v-model="form[honeypot.validFromFieldName]" :name="honeypot.validFromFieldName" />
</div>

然后,在 Vue 组件中,将这些值添加到表单数据中:

props: ['honeypot'],

data() {
    return {
        form: this.$inertia.form({
            [this.honeypot.nameFieldName]: '',
            [this.honeypot.validFromFieldName]: this.honeypot.encryptedValidFrom,
        }),
    }
}

Livewire 中使用

你可以使用该包阻止垃圾提交到 Livewire 驱动的表单。

首先,将 UsesSpamProtection trait 添加到 Livewire 组件:

use Spatie\Honeypot\Http\Livewire\Concerns\UsesSpamProtection;

class YourComponent extends Component
{
    use UsesSpamProtection;

接下来,声明 HoneypotData 属性并在方法中调用处理表单提交的 protectAgainstSpam() 方法。

use Spatie\Honeypot\Http\Livewire\Concerns\HoneypotData;

class YourComponent extends Component
{
    // ...
    
    public HoneypotData $extraFields;
    
    public function mount()
    {
        $this->extraFields = new HoneypotData();
    }
 
   
    public function submit(): void 
    {
        $this->protectAgainstSpam(); // if is spam, will abort the request
    
        User::create($request->all());
    }
}

最后,在 Livewire Blade 组件中使用 x-honeypot

<form method="POST" action="{{ route('contactForm.submit') }}")>
    <x-honeypot livewire-model="extraFields" />
    <input name="myField" type="text">
</form>

Volt 函数语法中的用法

要在 Volt 函数语法中使用此包,请在 guessHoneypotDataProperty 方法中返回 HoneypotData 属性。 

uses(UsesSpamProtection::class);

state([
    // ...
    'extraFields' => null,
]);

mount(function () {
    $this->extraFields = new HoneypotData();
});

$guessHoneypotDataProperty = fn () => $this->extraFields;

$submit = function () {
    $this->protectAgainstSpam(); // if is spam, will abort the request
    
    User::create($request->all());
};

测试中禁用

默认情况下,任何在 1 秒内提交的受保护表单都将被标记为垃圾。当运行端到端测试时,应该尽可能快地运行,你可能不希望这样。

要禁用代码中的所有蜜罐,可以将 enabled 的配置值设置为 false

config()->set('honeypot.enabled', false)

自定义回复

当检测到垃圾提交时,默认情况下包将显示一个空白页面。你可以通过编写自己的 SpamResponse 并在 honeypot 配置文件的respond_to_spam_with 键中指定其完全限定的类名来自定义此行为。

有效的 SpamResponse 类应该实现  Spatie\Honeypot\SpamResponder\SpamResponder 接口。以下是一个示例:

namespace Spatie\Honeypot\SpamResponder;

use Closure;
use Illuminate\Http\Request;

interface SpamResponder
{
    public function respond(Request $request, Closure $next);
}

尽管垃圾邮件响应程序的主要目的是响应垃圾请求,但也可以在那里做其他事情。例如,可以使用 $request 上的属性来确定垃圾的来源(可能所有请求都来自同一个 IP),并设置一些逻辑来完全阻止该来源。

如果包错误地将请求判定为垃圾,那么可以在中间件中,通过将 $request 传递给 $next 闭包来生成默认响应。

// in your spam responder
$regularResponse = $next($request)

自定义生成的蜜罐字段

要自定义生成的输出内容,你可以发 honeypot 视图:

php artisan vendor:publish --provider="Spatie\Honeypot\HoneypotServiceProvider" --tag=honeypot-views

该视图位于 resources/views/vendor/honeypot/honeypotFormFields.blade.php。其默认内容为:

@if($enabled)
    <div id="{{ $nameFieldName }}_wrap" style="display:none;">
        <input name="{{ $nameFieldName }}" type="text" value="" id="{{ $nameFieldName }}">
        <input name="{{ $validFromFieldName }}" type="text" value="{{ $encryptedValidFrom }}">
    </div>
@endif

事件触发

检测到 垃圾时,会触发 Spatie\Honeypot\Events\SpamDetectedEvent 事件。它右一个 public 属性 $request