使用情境

使用者在登入頁面時
輸入帳號密碼後
還必須要完成認證碼認證(g-recaptcha)
才能進行本次API資料傳送

流程架構

  1. 取得/產生 g-recaptcha 驗證區塊(我不是機器人),需透過google_site_key參數
  2. g-recaptcha驗證完後會得到token,將token和表單資料一併傳送到API Server
  3. 驗證token是否正確,會使用到google_secret_key
  4. API Server回傳結果
    • g-recaptcha token 正確
    • g-recaptcha token 驗證失敗 : secret_key錯誤 or token 錯誤
    • g-recaptcha token 空值沒填失敗

Client端實作 (HTML + JS)

參考 : Google recaptcha V2 - display

引入g-recaptcha script resource
參考教學有兩種render widget方法
這邊使用js onload callback方式
因為透過js控制 g-captcha會比較靈活
且你的 google_site_key 可以存在js config裡面
不用放在html裡

1
<script src='https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit' async defer></script>

實作onload event function
grecaptcha.render可以指令兩種DOM物件載入方法
一種是傳element id, 另一種是 document.getElementById(xxx)
這方法run成功後
你所指定的div element就會變成 g-recaptch 我不是機器人

1
2
3
4
5
6
7
8
9
<div id="g-recaptcha"></div>

<script>
var onloadCallback = function() {
  widgetId = grecaptcha.render('g-recaptcha', {
    'sitekey' : 'your_google_site_key',
  });
};
</script>

我們可以透過grecaptcha.getResponse()的方法
得到使用者驗證完g-recaptcha後的token

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function getPostdata() {
  let data = {};
  $('form').find('input').each(function(index, element){
    let key = $(element).attr('name');
    let value = $(element).val();
    data[key] = value;
  });
  
  data['google_recaptcha_token'] = grecaptcha.getResponse();

  return data;
}

POST登入API的json傳送範例大概會像是這樣
記得google_recaptcha_token的參數名稱需要與後端API一致

1
2
3
4
5
6
// Postdata Example
{
  "username":"foo",
  "password":"secret",
  "google_recaptcha_token":"HIUFJEWUIFHEWr32jrojsefodivu"
}

API端實作 (Laravel)

參考 : Google recaptcha V2 - verify

考慮到g-recaptcha這個方法可以重複被用在很多地方
所以我使用middleware的方式包裝起來
這個g-recaptcha middleware處理2件事情 :

  • required validation : 判斷google_recaptcha_token是否為空
  • verify token : 驗證token是否正確

app/Http/Middleware/GoogleRecapchaV2.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<?php

namespace App\Http\Middleware;

use Closure;
use Exception;
use GuzzleHttp\Client;

class GoogleRecapchaV2
{
    public function handle($request, Closure $next)
    {
        /* production 才驗證 recaptcha */
        if (config('app.env') === 'production') {
            $request->validate([
                'google_recaptcha_token' => 'required'
            ], [
                'required' => '請驗證「我不是機器人」'
            ]);

            if (!$this->verify($request->google_recaptcha_token)) {
                throw new Exception('g-recaptcha認證失敗');
            }
        }

        return $next($request);
    }

    private function verify(string $token = null) : bool
    {
        $url = 'https://www.google.com/recaptcha/api/siteverify';
        $postdata = [
            'secret' => config('settings.google_recaptcha_secret'),
            'response' => $googleRecaptchaToken,
        ];

        $client = new Client();
        $response = $client->request('POST', $url, [
            'form_params' => $postdata
        ]);

        $code = $response->getStatusCode();
        $content = json_decode($response->getBody()->getContents());

        if ($code === 200 && $content->success === true) {
            return true;
        }

        return false;
    }
}

加入新的Middelware變數定義 : g-recaptcha

app/Http/Kernel.php

1
2
3
protected $routeMiddleware = [
    'g-recaptcha' => \App\Http\Middleware\GoogleRecapchaV2::class,
];

middleware方法定義好後
之後你的route若是想加入g-recaptcha
只要套入middelware就好
這樣的做法會比較彈性靈活

app/routes/api.php

1
2
3
4
5
<?php

Route::post('/login', 'AuthController@login')->middleware('g-recapcha');
Route::get('/article', 'ArticleController@index');
Route::post('/article', 'ArticleController@store')->middleware('g-recapcha');

Summary

以前剛開始使用g-recaptcha的時候
只知道會使用別人寫好的laravel套件
沒有深入研究
後來前後端分離的經驗越來越多之後
慢慢可以釐清整個後端與前端API的傳送流程順序
通常只要牽扯到與第三方API互動時
流程會比較複雜些
建議大家先把API傳送流程架構畫好後
再來實作每個步驟功能
而且有問題時也可以拿來與有經驗的人討論