跳转到主要内容

在 BookStack 中添加 reCAPTCHA v2 注册验证

请为此 Issue 点赞以在未来获得官方支持:https://github.com/BookStackApp/BookStack/issues/1114


本文对应版本:BookStack v22.04.2

说难也不难,因为作者已经提供了示例代码,但是你可能会遇到 CSP 问题。
BookStack 使用了 strict-dynamic 所以需要使用 CSP3 中记录的基于 nonce 的方法来插入脚本。
但是我们并不知道 BookStack 的 nonce 随机数变量是什么。很惭愧,我也是问了作者才知道的:Issue 3066
然后 reCAPTCHA V2 复选框会以 iframe 的形式插入,所以你需要在 .env 的 ALLOWED_IFRAME_SOURCES 中添加对应的源,
国际用户应该添加:https://www.google.com, https://www.gstatic.comhttps://www.google.com, https://recaptcha.google.com
国内用户应该添加:https://recaptcha.net , https://*.gstatic.cn , https://*.gstatic.com

recaptcha.net 是 Google 提供的官方代理:https://developers.google.com/recaptcha/docs/faq#can-i-use-recaptcha-globally
专门提供给中国大陆用户使用,其他地区使用 google.com 即可


首先在 Issues 1114 中我们可以找到作者提供的简单示例:https://github.com/BookStackApp/BookStack/commit/c49454da2833eb31b426e6b6ecfd790fad58567b

打开示例,对应路径将代码插入的你的 BookStack 中。
如果你的版本和我一样的话,也可以直接复制使用,记得更改变量。


1. 打开 /app/Http/Controllers/Auth/RegisterController.php 对照下文修改,我已经注释出来了。

代码
<?php

namespace BookStack\Http\Controllers\Auth;

use BookStack\Auth\Access\LoginService;
use BookStack\Auth\Access\RegistrationService;
use BookStack\Auth\Access\SocialAuthService;
use BookStack\Auth\User;
use BookStack\Exceptions\StoppedAuthenticationException;
use BookStack\Exceptions\UserRegistrationException;
use BookStack\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\RegistersUsers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rules\Password;
//自定义recaptcha
use GuzzleHttp\Client;
//自定义recaptcha

class RegisterController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Register Controller
    |--------------------------------------------------------------------------
    |
    | This controller handles the registration of new users as well as their
    | validation and creation. By default this controller uses a trait to
    | provide this functionality without requiring any additional code.
    |
    */

    use RegistersUsers;

    protected $socialAuthService;
    protected $registrationService;
    protected $loginService;

    /**
     * Where to redirect users after login / registration.
     *
     * @var string
     */
    protected $redirectTo = '/';
    protected $redirectPath = '/';

    /**
     * Create a new controller instance.
     */
    public function __construct(
        SocialAuthService $socialAuthService,
        RegistrationService $registrationService,
        LoginService $loginService
    ) {
        $this->middleware('guest');
        $this->middleware('guard:standard');

        $this->socialAuthService = $socialAuthService;
        $this->registrationService = $registrationService;
        $this->loginService = $loginService;

        $this->redirectTo = url('/');
        $this->redirectPath = url('/');
    }

    /**
     * Get a validator for an incoming registration request.
     *
     * @return \Illuminate\Contracts\Validation\Validator
     */
    protected function validator(array $data)
    {
        return Validator::make($data, [
            'name'     => ['required', 'min:2', 'max:255'],
            'email'    => ['required', 'email', 'max:255', 'unique:users'],
            'password' => ['required', Password::default()],
        ]);
    }

    /**
     * Show the application registration form.
     *
     * @throws UserRegistrationException
     */
    public function getRegister()
    {
        $this->registrationService->ensureRegistrationAllowed();
        $socialDrivers = $this->socialAuthService->getActiveDrivers();

        return view('auth.register', [
            'socialDrivers' => $socialDrivers,
        ]);
    }

    /**
     * Handle a registration request for the application.
     *
     * @throws UserRegistrationException
     * @throws StoppedAuthenticationException
     */
    public function postRegister(Request $request)
    {
        $this->registrationService->ensureRegistrationAllowed();
        $this->validator($request->all())->validate();

//自定义recaptcha
        $captcha = $request->get('g-recaptcha-response');
        $resp = (new Client())->post('https://www.recaptcha.net/recaptcha/api/siteverify', [
            'form_params' => [
                'response' => $captcha,
                'secret' => '你的 reCAPTCHA 秘钥',
            ]
        ]);
        $respBody = json_decode($resp->getBody());
        if (!$respBody->success) {
            return redirect()->back()->withInput()->withErrors([
                'g-recaptcha-response' => '请完成人机验证,如果未显示验证码,请联系管理员',
            ]);
        }
//自定义recaptcha
        
        $userData = $request->all();

        try {
            $user = $this->registrationService->registerUser($userData);
            $this->loginService->login($user, auth()->getDefaultDriver());
        } catch (UserRegistrationException $exception) {
            if ($exception->getMessage()) {
                $this->showErrorNotification($exception->getMessage());
            }

            return redirect($exception->redirectLocation);
        }

        $this->showSuccessNotification(trans('auth.register_success'));

        return redirect($this->redirectPath());
    }

    /**
     * Create a new user instance after a valid registration.
     *
     * @param array $data
     *
     * @return User
     */
    protected function create(array $data)
    {
        return User::create([
            'name'     => $data['name'],
            'email'    => $data['email'],
            'password' => Hash::make($data['password']),
        ]);
    }
}


2.打开 /resources/views/auth/register.blade.php 对照下文修改,我已经注释出来了。

代码
@extends('layouts.simple')

@section('content')
    <div class="container very-small">

        <div class="my-l">&nbsp;</div>

        <div class="card content-wrap auto-height">
            <h1 class="list-heading">{{ Str::title(trans('auth.sign_up')) }}</h1>

            <form action="{{ url("/register") }}" method="POST" class="mt-l stretch-inputs">
                {!! csrf_field() !!}

                <div class="form-group">
                    <label for="email">{{ trans('auth.name') }}</label>
                    @include('form.text', ['name' => 'name'])
                </div>

                <div class="form-group">
                    <label for="email">{{ trans('auth.email') }}</label>
                    @include('form.text', ['name' => 'email'])
                </div>

                <div class="form-group">
                    <label for="password">{{ trans('auth.password') }}</label>
                    @include('form.password', ['name' => 'password', 'placeholder' => trans('auth.password_hint')])
                </div>

<!-- 自定义recaptcha -->
                 <div class="form-group">
                 <label for="recaptcha">验证码</label>
                 <script src="https://recaptcha.net/recaptcha/api.js" nonce="{{ $cspNonce }}"></script>
                    <div class="g-recaptcha" data-sitekey="你的 reCAPTCHA 网站秘钥"></div>
                          <br/>
                    @if($errors->has('g-recaptcha-response'))
                        <div class="text-neg text-small">{{ $errors->first('g-recaptcha-response') }}</div>
                    @endif
                </div>
<!-- 自定义recaptcha -->

                <div class="grid half collapse-xs gap-xl v-center mt-m">
                    <div class="text-small">
                        <a href="{{ url('/login') }}">{{ trans('auth.already_have_account') }}</a>
                    </div>
                    <div class="from-group text-right">
                        <button class="button">{{ trans('auth.create_account') }}</button>
                    </div>
                </div>

            </form>

            @if(count($socialDrivers) > 0)
                <hr class="my-l">
                @foreach($socialDrivers as $driver => $name)
                    <div>
                        <a id="social-register-{{$driver}}" class="button outline svg" href="{{ url("/register/service/" . $driver) }}">
                            @icon('auth/' . $driver)
                            <span>{{ trans('auth.sign_up_with', ['socialDriver' => $name]) }}</span>
                        </a>
                    </div>
                @endforeach
            @endif

        </div>
    </div>
@stop


3.打开 .env 找到 ALLOWED_IFRAME_SOURCES ,修改为:

ALLOWED_IFRAME_SOURCES="https://recaptcha.net https://*.gstatic.cn https://*.gstatic.com"


4.大功告成