Laravel Honeypot
spatie/laravel-honeypot
폼을 통해 제출되는 스팸을 방지합니다
공개 사이트에 폼을 추가하면 스팸 봇이 가짜 값으로 폼을 제출하려 할 위험이 있습니다. 다행히 이런 봇의 대부분은 꽤 단순합니다. 제출 시 절대 값이 들어 있으면 안 되는 보이지 않는 필드를 폼에 추가하면 대부분의 봇을 막을 수 있습니다. 이런 필드를 허니팟(honeypot)이라고 부릅니다. 스팸 봇은 허니팟을 포함한 모든 필드를 그대로 채워 버립니다.
허니팟 필드에 값이 채워진 제출이 들어오면 이 패키지는 해당 요청을 폐기합니다. 또한 이 패키지는 폼을 제출하는 데 걸린 시간도 확인합니다. 또 다른 보이지 않는 필드에 담긴 타임스탬프를 이용하며, 폼이 말도 안 되게 짧은 시간 안에 제출되면 스팸 방지 로직이 동작합니다.
패키지를 설치한 뒤에는 폼에 x-honeypot Blade 컴포넌트를 추가하기만 하면 됩니다.
<form method="POST">
<x-honeypot />
<input name="myField" type="text">
</form>필요한 값을 뷰 레이어로 직접 전달하는 방식도 지원하므로, Inertia 기반 앱에도 허니팟 필드를 쉽게 추가할 수 있습니다.
후원하기
Spatie는 최고 수준의 오픈 소스 패키지를 만드는 데 많은 자원을 투자하고 있습니다. 유료 제품을 구매하여 후원하실 수 있습니다.
사용 중인 패키지를 알려 주며 고향에서 보낸 엽서를 보내 주시면 큰 힘이 됩니다. 주소는 연락처 페이지에서 확인할 수 있습니다. 받은 모든 엽서는 가상 엽서 벽에 게시합니다.
동영상 튜토리얼
Mailcoach 동영상 강좌의 일부인 이 영상에서 패키지를 설치하고 사용하는 방법을 볼 수 있습니다.
설치
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),
/*
* When enabled, the honeypot fields will be hidden using a style tag
* with a CSP nonce instead of an inline style attribute. This
* requires spatie/laravel-csp to be installed.
*/
'with_csp' => env('HONEYPOT_WITH_CSP', false),
];사용법
먼저 보호하려는 모든 폼에 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_name과 valid_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,
]);
}프론트엔드에서는 enabled, nameFieldName, validFromFieldName, encryptedValidFrom 키를 가진 honeypot 객체를 받게 됩니다.
다음은 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,
}),
}
}Jetstream(Vue/Inertia)에서 사용하기
동작시키려면 다음 단계를 따르세요.
bootstrap/app.php의->withMiddleware블록에\Spatie\Honeypot\ProtectAgainstSpam::class를 추가합니다. 예를 들면 다음과 같습니다.
$middleware->web(append: [
\App\Http\Middleware\HandleInertiaRequests::class,
\Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets::class,
// will silenty reload Login page
\Spatie\Honeypot\ProtectAgainstSpam::class, // <- this
]);또는
$middleware->web(append: [
\App\Http\Middleware\HandleInertiaRequests::class,
\Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets::class,
]);
// will show blank response page in iframe inside modal
$middleware->append(\Spatie\Honeypot\ProtectAgainstSpam::class);app/Http/Middleware/HandleInertiaRequests.php미들웨어의 share 블록에honeypot변수를 추가합니다. 예를 들면 다음과 같습니다.
public function share(Request $request): array
{
return array_merge(parent::share($request), [
// read more here https://laravel.com/docs/11.x/authorization#authorization-and-inertia
'honeypot' => new \Spatie\Honeypot\Honeypot(config('honeypot'))
]);
}- 원하는
Pages에 허니팟 요소를 추가합니다. 예를 들어resourses/js/Pages/Auth/Login.vue는 다음과 같이 수정합니다.
props를 다음과 같이 다시 정의합니다.
const props = defineProps({
canResetPassword: Boolean,
status: String,
honeypot: Object,
});form을 다음과 같이 다시 정의합니다.
const form = useForm({
email: '',
password: '',
remember: false,
[props.honeypot.nameFieldName]: '',
[props.honeypot.validFromFieldName]: props.honeypot.encryptedValidFrom,
});그리고 template에 다음과 같이 HTML을 추가합니다.
<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>Register.vue에서는 다음과 같이 변경합니다.
const props = defineProps({
honeypot: Object,
});
const form = useForm({
name: '',
email: '',
password: '',
password_confirmation: '',
terms: false,
[props.honeypot.nameFieldName]: '',
[props.honeypot.validFromFieldName]: props.honeypot.encryptedValidFrom,
});그리고
<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>이것으로 끝입니다.
Livewire에서 사용하기
이 패키지를 사용해 Livewire로 만든 폼에 대한 스팸 제출도 막을 수 있습니다.
먼저 Livewire 컴포넌트에 UsesSpamProtection 트레이트를 추가합니다.
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>콘텐츠 보안 정책(CSP)과 함께 사용하기
기본적으로 허니팟 필드는 인라인 style 속성으로 숨겨집니다. 콘텐츠 보안 정책(Content Security Policy)이 인라인 스타일을 허용하지 않는다면, 설정 파일의 with_csp를 true로 지정하세요(또는 HONEYPOT_WITH_CSP=true로 설정). 그러면 nonce가 포함된 <style> 태그로 필드를 숨깁니다.
이 기능은 @cspNonce 디렉티브에 의존하므로, 먼저 spatie/laravel-csp를 설치해야 합니다.
Volt 함수형 문법에서 사용하기
Volt 함수형 문법에서 이 패키지를 사용하려면, guessHoneypotDataProperty 메서드에서 HoneypotData 프로퍼티를 반환하세요.
use App\Models\User;
use Spatie\Honeypot\Http\Livewire\Concerns\HoneypotData;
use Spatie\Honeypot\Http\Livewire\Concerns\UsesSpamProtection;
use function Livewire\Volt\{uses, state, mount};
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를 작성하고, 그 클래스의 정규화된 이름(FQCN)을 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 이벤트가 발생합니다. 이 이벤트는 $request를 public 프로퍼티로 가지고 있습니다.
테스트
composer test변경 내역
최근 변경 사항은 CHANGELOG를 참고하세요.
대안
더 강력한 스팸 방지가 필요하다면 Google ReCaptcha나 Akismet 사용을 고려해 보세요.
기여하기
자세한 내용은 CONTRIBUTING을 참고하세요.
보안
보안과 관련된 버그를 발견했다면 이슈 트래커 대신 security@spatie.be로 메일을 보내 주세요.
크레딧
이 패키지는 Maksim Surguy의 Honeypot 패키지에서 영감을 받았습니다.
라이선스
MIT 라이선스(MIT)를 따릅니다. 자세한 내용은 License File을 참고하세요.
