[{"data":1,"prerenderedAt":1770},["ShallowReactive",2],{"article-implement-recaptcha":3},{"id":4,"title":5,"body":6,"category":1754,"createdAt":1756,"description":1757,"extension":1758,"index":1759,"meta":1760,"navigation":162,"path":1761,"publish":162,"seo":1762,"series":1759,"seriesTitle":1759,"stem":1763,"tag":1764,"thumbnail":1768,"updatedAt":1759,"__hash__":1769},"articles\u002Farticles\u002Fimplement-recaptcha.md","reCAPTCHAのフロントエンド実装とバックエンド実装（PHP・Laravel）をスクラッチで行う方法",{"type":7,"value":8,"toc":1740},"minimark",[9,13,16,19,22,27,30,33,36,39,43,54,59,67,70,73,84,87,90,303,306,319,328,333,336,579,593,596,599,1101,1112,1134,1137,1140,1143,1146,1157,1160,1361,1372,1397,1415,1418,1457,1468,1471,1474,1519,1526,1529,1537,1562,1570,1579,1587,1593,1601,1604,1724,1727,1730,1733,1736],[10,11,12],"p",{},"こんにちはjunです。皆さんはフォームを作成する時にBot・スパム対策を行っていますか？フォームというのは少し知識があれば、簡単にbot的に送信することができます。",[10,14,15],{},"curlでPOSTすることもあれば、javascriptを実行して機械的にフォームを送信することがきるので、スパム（嫌がらせ）やサーバーへの過剰負荷の原因となります。",[10,17,18],{},"この様な機械的な操作を防ぐために、よく「ロボットではありません」「画像に表示されている文字を入力してください」みたいなbotでは簡単に処理できないものを用意します。",[10,20,21],{},"しかしこの様な機能は自分で実装するのは大変です。そんな時に便利なのがreCAPTCHAです。",[23,24,26],"h2",{"id":25},"recaptchaとは","reCAPTCHAとは？",[10,28,29],{},"reCAPTCHAはGoogleが無料で提供しているBot対策ツールです。現在v3までリリースされており、v2は画像を選択させたりチェックを入れるといったユーザーのアクションでbotかどうかを検証します。v3はその様なアクションを必要とせず、必要なスクリプトを入れるだけで検証ができます。これからの実装の場合はv3を入れることをお勧めします。",[23,31,32],{"id":32},"実装内容",[10,34,35],{},"今回の解説ではv3でのフロントエンドの実装とバックエンドの実装を解説していきます。そして利用するreCAPTCHAはエンタープライズではなく、無料のものを利用します。バックエンドにはLaravel6(php)を用いて説明します。バックエンドは基本的にやることはどの言語・フレームワークでも特に差異はありません。reCAPTCHAを使う時にライブラリを使用することもありますが、いうてそれほど難しくないので今回はライブラリを使用しないスクラッチで実装します。",[10,37,38],{},"それでは解説を始めます。",[23,40,42],{"id":41},"recaptchaのキーを手に入れる","reCAPTCHAのキーを手に入れる",[10,44,45,46,53],{},"reCAPTCHAはGooglenのAPIを使用することでbotか検証を行います。reCAPTCHAを利用するためにGoogleアカウントとreCAPTCHAのキーを登録します。",[47,48,52],"a",{"href":49,"rel":50},"https:\u002F\u002Fwww.google.com\u002Frecaptcha\u002Fadmin\u002Fcreate",[51],"nofollow","reCAPTCHAの登録ページ","にて保護対象のドメインを設定します。",[55,56],"image-render",{":src":57,":width":58},"'_mix\u002Fsch-2021-05-21 23.16.06.png'","'100%'",[10,60,61,62,66],{},"ラベルは管理用の名前、タイプはv3を使用します。保護対象のドメインを登録します。ドメインは複数設定できます。この時、本番のドメインと",[63,64,65],"code",{},"localhost","を登録しておくと開発・本番で利用できます。",[10,68,69],{},"設定を終わって「保存」しますと、キーが表示されますので保存しておきます。",[55,71],{":src":72,":width":58},"'_mix\u002Frecaptcha-key.jpeg'",[10,74,75,79,80,83],{},[76,77,78],"em",{},"このサイトキーは、ユーザー表示するHTMLコードで利用します。"," という方のキーはタグで利用し、正直みられても問題ありません。フロント側のキーはドメインと合わせて保護対処のサイトであるかのチェックのためにあるだけだからです。逆にバックの方である ",[76,81,82],{},"このサイトキーは、サイトとreCAPTCHA間の通信で利用します。"," は漏れてはいけません。",[23,85,86],{"id":86},"フロントエンドの実装",[10,88,89],{},"それではフロントエンドを実装していきます。かなり簡略化して書いています。以下の様なフォームがあったとしましょう。",[91,92,97],"pre",{"className":93,"code":94,"language":95,"meta":96,"style":96},"language-html shiki shiki-themes material-theme-ocean","\u003C!DOCTYPE html>\n\u003Chtml>\n    \u003Chead>\n      \u003C!---省略-->\n    \u003C\u002Fhead>\n\n    \u003Cbody>\n        \u003Cform method=\"post\">\n            \u003Cinput type=\"text\" name=\"test\" value=\"\">\n            \u003Cinput type=\"submit\" value=\"送信\">\n        \u003C\u002Fform>\n    \u003C\u002Fbody>\n\u003C\u002Fhtml>\n","html","",[63,98,99,119,129,140,147,157,164,174,200,243,274,284,293],{"__ignoreMap":96},[100,101,104,108,112,116],"span",{"class":102,"line":103},"line",1,[100,105,107],{"class":106},"sAklC","\u003C!",[100,109,111],{"class":110},"s-wAU","DOCTYPE",[100,113,115],{"class":114},"sJ14y"," html",[100,117,118],{"class":106},">\n",[100,120,122,125,127],{"class":102,"line":121},2,[100,123,124],{"class":106},"\u003C",[100,126,95],{"class":110},[100,128,118],{"class":106},[100,130,132,135,138],{"class":102,"line":131},3,[100,133,134],{"class":106},"    \u003C",[100,136,137],{"class":110},"head",[100,139,118],{"class":106},[100,141,143],{"class":102,"line":142},4,[100,144,146],{"class":145},"sC9rS","      \u003C!---省略-->\n",[100,148,150,153,155],{"class":102,"line":149},5,[100,151,152],{"class":106},"    \u003C\u002F",[100,154,137],{"class":110},[100,156,118],{"class":106},[100,158,160],{"class":102,"line":159},6,[100,161,163],{"emptyLinePlaceholder":162},true,"\n",[100,165,167,169,172],{"class":102,"line":166},7,[100,168,134],{"class":106},[100,170,171],{"class":110},"body",[100,173,118],{"class":106},[100,175,177,180,183,186,189,192,196,198],{"class":102,"line":176},8,[100,178,179],{"class":106},"        \u003C",[100,181,182],{"class":110},"form",[100,184,185],{"class":114}," method",[100,187,188],{"class":106},"=",[100,190,191],{"class":106},"\"",[100,193,195],{"class":194},"sfyAc","post",[100,197,191],{"class":106},[100,199,118],{"class":106},[100,201,203,206,209,212,214,216,219,221,224,226,228,231,233,236,238,241],{"class":102,"line":202},9,[100,204,205],{"class":106},"            \u003C",[100,207,208],{"class":110},"input",[100,210,211],{"class":114}," type",[100,213,188],{"class":106},[100,215,191],{"class":106},[100,217,218],{"class":194},"text",[100,220,191],{"class":106},[100,222,223],{"class":114}," name",[100,225,188],{"class":106},[100,227,191],{"class":106},[100,229,230],{"class":194},"test",[100,232,191],{"class":106},[100,234,235],{"class":114}," value",[100,237,188],{"class":106},[100,239,240],{"class":106},"\"\"",[100,242,118],{"class":106},[100,244,246,248,250,252,254,256,259,261,263,265,267,270,272],{"class":102,"line":245},10,[100,247,205],{"class":106},[100,249,208],{"class":110},[100,251,211],{"class":114},[100,253,188],{"class":106},[100,255,191],{"class":106},[100,257,258],{"class":194},"submit",[100,260,191],{"class":106},[100,262,235],{"class":114},[100,264,188],{"class":106},[100,266,191],{"class":106},[100,268,269],{"class":194},"送信",[100,271,191],{"class":106},[100,273,118],{"class":106},[100,275,277,280,282],{"class":102,"line":276},11,[100,278,279],{"class":106},"        \u003C\u002F",[100,281,182],{"class":110},[100,283,118],{"class":106},[100,285,287,289,291],{"class":102,"line":286},12,[100,288,152],{"class":106},[100,290,171],{"class":110},[100,292,118],{"class":106},[100,294,296,299,301],{"class":102,"line":295},13,[100,297,298],{"class":106},"\u003C\u002F",[100,300,95],{"class":110},[100,302,118],{"class":106},[10,304,305],{},"reCAPTCHAのフロントエンド 実装では",[307,308,309,313,316],"ul",{},[310,311,312],"li",{},"reCAPTCHAのソースを読み込む",[310,314,315],{},"送信ボタンを押したらreCAPTCHAと通信してトークンを手に入れるスクリプトを書く",[310,317,318],{},"reCAPTCHAのトークンをフォーム内容と一緒にバックエンドに送信する。",[10,320,321,322,327],{},"以上の実装を必要とします。",[47,323,326],{"href":324,"rel":325},"https:\u002F\u002Fdevelopers.google.com\u002Frecaptcha\u002Fdocs\u002Fv3#programmatically_invoke_the_challenge",[51],"本家のドキュメント","を参考にして進めていきましょう。",[329,330,332],"h3",{"id":331},"recaptchaのスクリプト読み込みとhtml調整","reCAPTCHAのスクリプト読み込みとHTML調整",[10,334,335],{},"まずはreCAPTCHAを有効にするためのスクリプトを読み込みます。そして一部フォームを編集します。",[91,337,339],{"className":93,"code":338,"language":95,"meta":96,"style":96},"\u003C!DOCTYPE html>\n\u003Chtml>\n    \u003Chead>\n      \u003C!---省略-->\n        \u003Cscript src=\"https:\u002F\u002Fwww.google.com\u002Frecaptcha\u002Fapi.js?render=YOUR_FRONT_KEY\">\u003C\u002Fscript>\u003C!---追加-->\n    \u003C\u002Fhead>\n\n    \u003Cbody>\n        \u003Cform method=\"post\" id=\"test-form\">\u003C!---追加-->\n            \u003Cinput type=\"text\" name=\"test\" value=\"\">\n            \u003Cinput type=\"hidden\" name=\"recaptcha\" value=\"\">\u003C!---追加-->\n            \u003Cinput type=\"submit\" value=\"送信\">\n        \u003C\u002Fform>\n    \u003C\u002Fbody>\n\u003C\u002Fhtml>\n",[63,340,341,351,359,367,371,401,409,413,421,453,487,525,553,561,570],{"__ignoreMap":96},[100,342,343,345,347,349],{"class":102,"line":103},[100,344,107],{"class":106},[100,346,111],{"class":110},[100,348,115],{"class":114},[100,350,118],{"class":106},[100,352,353,355,357],{"class":102,"line":121},[100,354,124],{"class":106},[100,356,95],{"class":110},[100,358,118],{"class":106},[100,360,361,363,365],{"class":102,"line":131},[100,362,134],{"class":106},[100,364,137],{"class":110},[100,366,118],{"class":106},[100,368,369],{"class":102,"line":142},[100,370,146],{"class":145},[100,372,373,375,378,381,383,385,388,390,393,395,398],{"class":102,"line":149},[100,374,179],{"class":106},[100,376,377],{"class":110},"script",[100,379,380],{"class":114}," src",[100,382,188],{"class":106},[100,384,191],{"class":106},[100,386,387],{"class":194},"https:\u002F\u002Fwww.google.com\u002Frecaptcha\u002Fapi.js?render=YOUR_FRONT_KEY",[100,389,191],{"class":106},[100,391,392],{"class":106},">\u003C\u002F",[100,394,377],{"class":110},[100,396,397],{"class":106},">",[100,399,400],{"class":145},"\u003C!---追加-->\n",[100,402,403,405,407],{"class":102,"line":159},[100,404,152],{"class":106},[100,406,137],{"class":110},[100,408,118],{"class":106},[100,410,411],{"class":102,"line":166},[100,412,163],{"emptyLinePlaceholder":162},[100,414,415,417,419],{"class":102,"line":176},[100,416,134],{"class":106},[100,418,171],{"class":110},[100,420,118],{"class":106},[100,422,423,425,427,429,431,433,435,437,440,442,444,447,449,451],{"class":102,"line":202},[100,424,179],{"class":106},[100,426,182],{"class":110},[100,428,185],{"class":114},[100,430,188],{"class":106},[100,432,191],{"class":106},[100,434,195],{"class":194},[100,436,191],{"class":106},[100,438,439],{"class":114}," id",[100,441,188],{"class":106},[100,443,191],{"class":106},[100,445,446],{"class":194},"test-form",[100,448,191],{"class":106},[100,450,397],{"class":106},[100,452,400],{"class":145},[100,454,455,457,459,461,463,465,467,469,471,473,475,477,479,481,483,485],{"class":102,"line":245},[100,456,205],{"class":106},[100,458,208],{"class":110},[100,460,211],{"class":114},[100,462,188],{"class":106},[100,464,191],{"class":106},[100,466,218],{"class":194},[100,468,191],{"class":106},[100,470,223],{"class":114},[100,472,188],{"class":106},[100,474,191],{"class":106},[100,476,230],{"class":194},[100,478,191],{"class":106},[100,480,235],{"class":114},[100,482,188],{"class":106},[100,484,240],{"class":106},[100,486,118],{"class":106},[100,488,489,491,493,495,497,499,502,504,506,508,510,513,515,517,519,521,523],{"class":102,"line":276},[100,490,205],{"class":106},[100,492,208],{"class":110},[100,494,211],{"class":114},[100,496,188],{"class":106},[100,498,191],{"class":106},[100,500,501],{"class":194},"hidden",[100,503,191],{"class":106},[100,505,223],{"class":114},[100,507,188],{"class":106},[100,509,191],{"class":106},[100,511,512],{"class":194},"recaptcha",[100,514,191],{"class":106},[100,516,235],{"class":114},[100,518,188],{"class":106},[100,520,240],{"class":106},[100,522,397],{"class":106},[100,524,400],{"class":145},[100,526,527,529,531,533,535,537,539,541,543,545,547,549,551],{"class":102,"line":286},[100,528,205],{"class":106},[100,530,208],{"class":110},[100,532,211],{"class":114},[100,534,188],{"class":106},[100,536,191],{"class":106},[100,538,258],{"class":194},[100,540,191],{"class":106},[100,542,235],{"class":114},[100,544,188],{"class":106},[100,546,191],{"class":106},[100,548,269],{"class":194},[100,550,191],{"class":106},[100,552,118],{"class":106},[100,554,555,557,559],{"class":102,"line":295},[100,556,279],{"class":106},[100,558,182],{"class":110},[100,560,118],{"class":106},[100,562,564,566,568],{"class":102,"line":563},14,[100,565,152],{"class":106},[100,567,171],{"class":110},[100,569,118],{"class":106},[100,571,573,575,577],{"class":102,"line":572},15,[100,574,298],{"class":106},[100,576,95],{"class":110},[100,578,118],{"class":106},[10,580,581,584,585,588,589,592],{},[63,582,583],{},"\u003Cscript src=\"https:\u002F\u002Fwww.google.com\u002Frecaptcha\u002Fapi.js?render=YOUR_FRONT_KEY\">\u003C\u002Fscript>","の",[63,586,587],{},"YOUR_FRONT_KEY","に先ほど取得したフロント用のキーを入れます。\n",[63,590,591],{},"\u003Cinput type=\"hidden\" name=\"recaptcha\" value=\"\">","にはreCAPTCHAのトークンを挿入してバックエンドに送ります。HTMLフォームであればこの様にしますが、axiosなどの場合はトークンの値をjsを用いて送信するので、このHTMLは要りません。",[329,594,595],{"id":595},"トークンを取得するスクリプトを記述",[10,597,598],{},"「送信ボタン」を押した時にreCAPTCHAにAPIを飛ばしてbotかどうかの判定用トークンを取得します。以下の様なスクリプトを記述します。",[91,600,602],{"className":93,"code":601,"language":95,"meta":96,"style":96},"\u003C!DOCTYPE html>\n\u003Chtml>\n    \u003Chead>\n      \u003C!---省略-->\n      \u003Cscript src=\"https:\u002F\u002Fwww.google.com\u002Frecaptcha\u002Fapi.js?render=YOUR_FRONT_KEY\">\u003C\u002Fscript>\n    \u003C\u002Fhead>\n\n    \u003Cbody>\n        \u003Cform method=\"post\" id=\"test-form\">\u003C!---追加-->\n            \u003Cinput type=\"text\" name=\"test\" value=\"\">\n            \u003Cinput type=\"hidden\" name=\"recaptcha\" value=\"\">\u003C!---追加-->\n            \u003Cinput type=\"submit\" value=\"送信\">\n        \u003C\u002Fform>\n    \u003C\u002Fbody>\n\n    \u003Cscript>\n        function checkCaptcha(e) {\n            e.preventDefault();\n            grecaptcha.ready(function() {\n                grecaptcha.execute('YOUR_FRONT_KEY', {action: 'submit'}).then(function(token) {\n                    document.getElementById(\"recaptcha\").value=token;\n                    document.getElementById(\"test-form\").submit();\n                });\n            });\n        }\n        document.getElementById(\"test-form\").addEventListener('submit', checkCaptcha);\n        \u003C\u002Fscript>\n\u003C\u002Fhtml>\n",[63,603,604,614,622,630,634,657,665,669,677,707,741,777,805,813,821,825,834,857,876,896,958,990,1017,1027,1037,1043,1083,1092],{"__ignoreMap":96},[100,605,606,608,610,612],{"class":102,"line":103},[100,607,107],{"class":106},[100,609,111],{"class":110},[100,611,115],{"class":114},[100,613,118],{"class":106},[100,615,616,618,620],{"class":102,"line":121},[100,617,124],{"class":106},[100,619,95],{"class":110},[100,621,118],{"class":106},[100,623,624,626,628],{"class":102,"line":131},[100,625,134],{"class":106},[100,627,137],{"class":110},[100,629,118],{"class":106},[100,631,632],{"class":102,"line":142},[100,633,146],{"class":145},[100,635,636,639,641,643,645,647,649,651,653,655],{"class":102,"line":149},[100,637,638],{"class":106},"      \u003C",[100,640,377],{"class":110},[100,642,380],{"class":114},[100,644,188],{"class":106},[100,646,191],{"class":106},[100,648,387],{"class":194},[100,650,191],{"class":106},[100,652,392],{"class":106},[100,654,377],{"class":110},[100,656,118],{"class":106},[100,658,659,661,663],{"class":102,"line":159},[100,660,152],{"class":106},[100,662,137],{"class":110},[100,664,118],{"class":106},[100,666,667],{"class":102,"line":166},[100,668,163],{"emptyLinePlaceholder":162},[100,670,671,673,675],{"class":102,"line":176},[100,672,134],{"class":106},[100,674,171],{"class":110},[100,676,118],{"class":106},[100,678,679,681,683,685,687,689,691,693,695,697,699,701,703,705],{"class":102,"line":202},[100,680,179],{"class":106},[100,682,182],{"class":110},[100,684,185],{"class":114},[100,686,188],{"class":106},[100,688,191],{"class":106},[100,690,195],{"class":194},[100,692,191],{"class":106},[100,694,439],{"class":114},[100,696,188],{"class":106},[100,698,191],{"class":106},[100,700,446],{"class":194},[100,702,191],{"class":106},[100,704,397],{"class":106},[100,706,400],{"class":145},[100,708,709,711,713,715,717,719,721,723,725,727,729,731,733,735,737,739],{"class":102,"line":245},[100,710,205],{"class":106},[100,712,208],{"class":110},[100,714,211],{"class":114},[100,716,188],{"class":106},[100,718,191],{"class":106},[100,720,218],{"class":194},[100,722,191],{"class":106},[100,724,223],{"class":114},[100,726,188],{"class":106},[100,728,191],{"class":106},[100,730,230],{"class":194},[100,732,191],{"class":106},[100,734,235],{"class":114},[100,736,188],{"class":106},[100,738,240],{"class":106},[100,740,118],{"class":106},[100,742,743,745,747,749,751,753,755,757,759,761,763,765,767,769,771,773,775],{"class":102,"line":276},[100,744,205],{"class":106},[100,746,208],{"class":110},[100,748,211],{"class":114},[100,750,188],{"class":106},[100,752,191],{"class":106},[100,754,501],{"class":194},[100,756,191],{"class":106},[100,758,223],{"class":114},[100,760,188],{"class":106},[100,762,191],{"class":106},[100,764,512],{"class":194},[100,766,191],{"class":106},[100,768,235],{"class":114},[100,770,188],{"class":106},[100,772,240],{"class":106},[100,774,397],{"class":106},[100,776,400],{"class":145},[100,778,779,781,783,785,787,789,791,793,795,797,799,801,803],{"class":102,"line":286},[100,780,205],{"class":106},[100,782,208],{"class":110},[100,784,211],{"class":114},[100,786,188],{"class":106},[100,788,191],{"class":106},[100,790,258],{"class":194},[100,792,191],{"class":106},[100,794,235],{"class":114},[100,796,188],{"class":106},[100,798,191],{"class":106},[100,800,269],{"class":194},[100,802,191],{"class":106},[100,804,118],{"class":106},[100,806,807,809,811],{"class":102,"line":295},[100,808,279],{"class":106},[100,810,182],{"class":110},[100,812,118],{"class":106},[100,814,815,817,819],{"class":102,"line":563},[100,816,152],{"class":106},[100,818,171],{"class":110},[100,820,118],{"class":106},[100,822,823],{"class":102,"line":572},[100,824,163],{"emptyLinePlaceholder":162},[100,826,828,830,832],{"class":102,"line":827},16,[100,829,134],{"class":106},[100,831,377],{"class":110},[100,833,118],{"class":106},[100,835,837,840,844,847,851,854],{"class":102,"line":836},17,[100,838,839],{"class":114},"        function",[100,841,843],{"class":842},"sdLwU"," checkCaptcha",[100,845,846],{"class":106},"(",[100,848,850],{"class":849},"s7ZW3","e",[100,852,853],{"class":106},")",[100,855,856],{"class":106}," {\n",[100,858,860,864,867,870,873],{"class":102,"line":859},18,[100,861,863],{"class":862},"s0W1g","            e",[100,865,866],{"class":106},".",[100,868,869],{"class":842},"preventDefault",[100,871,872],{"class":110},"()",[100,874,875],{"class":106},";\n",[100,877,879,882,884,887,889,892,894],{"class":102,"line":878},19,[100,880,881],{"class":862},"            grecaptcha",[100,883,866],{"class":106},[100,885,886],{"class":842},"ready",[100,888,846],{"class":110},[100,890,891],{"class":114},"function",[100,893,872],{"class":106},[100,895,856],{"class":106},[100,897,899,902,904,907,909,912,914,916,919,922,925,928,931,933,935,938,940,942,945,947,949,951,954,956],{"class":102,"line":898},20,[100,900,901],{"class":862},"                grecaptcha",[100,903,866],{"class":106},[100,905,906],{"class":842},"execute",[100,908,846],{"class":110},[100,910,911],{"class":106},"'",[100,913,587],{"class":194},[100,915,911],{"class":106},[100,917,918],{"class":106},",",[100,920,921],{"class":106}," {",[100,923,924],{"class":110},"action",[100,926,927],{"class":106},":",[100,929,930],{"class":106}," '",[100,932,258],{"class":194},[100,934,911],{"class":106},[100,936,937],{"class":106},"}",[100,939,853],{"class":110},[100,941,866],{"class":106},[100,943,944],{"class":842},"then",[100,946,846],{"class":110},[100,948,891],{"class":114},[100,950,846],{"class":106},[100,952,953],{"class":849},"token",[100,955,853],{"class":106},[100,957,856],{"class":106},[100,959,961,964,966,969,971,973,975,977,979,981,984,986,988],{"class":102,"line":960},21,[100,962,963],{"class":862},"                    document",[100,965,866],{"class":106},[100,967,968],{"class":842},"getElementById",[100,970,846],{"class":110},[100,972,191],{"class":106},[100,974,512],{"class":194},[100,976,191],{"class":106},[100,978,853],{"class":110},[100,980,866],{"class":106},[100,982,983],{"class":862},"value",[100,985,188],{"class":106},[100,987,953],{"class":862},[100,989,875],{"class":106},[100,991,993,995,997,999,1001,1003,1005,1007,1009,1011,1013,1015],{"class":102,"line":992},22,[100,994,963],{"class":862},[100,996,866],{"class":106},[100,998,968],{"class":842},[100,1000,846],{"class":110},[100,1002,191],{"class":106},[100,1004,446],{"class":194},[100,1006,191],{"class":106},[100,1008,853],{"class":110},[100,1010,866],{"class":106},[100,1012,258],{"class":842},[100,1014,872],{"class":110},[100,1016,875],{"class":106},[100,1018,1020,1023,1025],{"class":102,"line":1019},23,[100,1021,1022],{"class":106},"                }",[100,1024,853],{"class":110},[100,1026,875],{"class":106},[100,1028,1030,1033,1035],{"class":102,"line":1029},24,[100,1031,1032],{"class":106},"            }",[100,1034,853],{"class":110},[100,1036,875],{"class":106},[100,1038,1040],{"class":102,"line":1039},25,[100,1041,1042],{"class":106},"        }\n",[100,1044,1046,1049,1051,1053,1055,1057,1059,1061,1063,1065,1068,1070,1072,1074,1076,1078,1081],{"class":102,"line":1045},26,[100,1047,1048],{"class":862},"        document",[100,1050,866],{"class":106},[100,1052,968],{"class":842},[100,1054,846],{"class":862},[100,1056,191],{"class":106},[100,1058,446],{"class":194},[100,1060,191],{"class":106},[100,1062,853],{"class":862},[100,1064,866],{"class":106},[100,1066,1067],{"class":842},"addEventListener",[100,1069,846],{"class":862},[100,1071,911],{"class":106},[100,1073,258],{"class":194},[100,1075,911],{"class":106},[100,1077,918],{"class":106},[100,1079,1080],{"class":862}," checkCaptcha)",[100,1082,875],{"class":106},[100,1084,1086,1088,1090],{"class":102,"line":1085},27,[100,1087,279],{"class":106},[100,1089,377],{"class":110},[100,1091,118],{"class":106},[100,1093,1095,1097,1099],{"class":102,"line":1094},28,[100,1096,298],{"class":106},[100,1098,95],{"class":110},[100,1100,118],{"class":106},[10,1102,1103,1104,1107,1108,1111],{},"フォームの送信ボタンが押された時（submitイベント発火時）に",[63,1105,1106],{},"checkCaptcha","の関数が実行される様に設定します。",[63,1109,1110],{},"e.preventDefault();","を使用してそのままフォームが送信されない様にします。",[10,1113,1114,1115,1118,1119,1122,1123,1126,1127,1129,1130,1133],{},"reCAPTCHAのスクリプトによって",[63,1116,1117],{},"grecaptcha","というオブジェクトが使用できる様になり、その中の",[63,1120,1121],{},"grecaptcha.execute()","にてAPIを実行します。第一引数にフロントのキー、第二引数にアクションを入力します。Promiseなので",[63,1124,1125],{},"then(token)","内のコールバックでトークンを",[63,1128,591],{},"に突っ込みます。そしてフォームを",[63,1131,1132],{},"submit()","にて送信します。",[10,1135,1136],{},"これでフロントの実装は完了です。フロントでの動きをみてreCAPTCHAはbotかどうかを判断し、この送信を一意なトークンで保存しているのです。トークンはバックエンドでの検証で利用します。",[23,1138,1139],{"id":1139},"バックエンドの実装",[10,1141,1142],{},"それではバックエンドの実装をすすめます。Laravelのコントローラーでの記述を想定しています。バリデーションなどは各自設定してください。",[10,1144,1145],{},"バックエンドで行うことは",[307,1147,1148,1151,1154],{},[310,1149,1150],{},"フロントからきたトークンをreCAPTCHAのAPIに送信",[310,1152,1153],{},"reCAPTCHAの結果を取得する",[310,1155,1156],{},"結果（スコア）を用いてbotかの判断をする",[10,1158,1159],{},"以上となります。コードは以下の通りです。",[91,1161,1165],{"className":1162,"code":1163,"language":1164,"meta":96,"style":96},"language-php shiki shiki-themes material-theme-ocean","class Controller extends BaseController\n{\n    use AuthorizesRequests, DispatchesJobs, ValidatesRequests;\n\n    public function checkRecaptcha(Request $request){\n        try {\n            $client = new \\GuzzleHttp\\Client([\n                'headers' => [\n                    'Content-Type' => 'application\u002Fjson',\n                ],\n            ]);\n    \n            $promise = $client->postAsync('https:\u002F\u002Fwww.google.com\u002Frecaptcha\u002Fapi\u002Fsiteverify',\n            [\n                'form_params' =>[\n                    'secret'=>env('RECAPTCHA_SERVER_KEY'),\n                    'response'=>$request->recaptcha\n                ]\n            ]);\n    \n            $res = Promise\\Utils::settle($promise)->wait();\n            $isFulfilled = isset($res[0]['value']);\n            if(!$isFulfilled) throw new \\Exception('RECAPTCHA SERVER returns error');\n    \n            $result = json_decode($res[0]['value']->getBody()->getContents(),true);\n            \n            if(isset($result['error-codes'])){\n                if($result['error-codes'][0] === 'timeout-or-duplicate') return false;\n                throw new \\Exception('RECAPTCHA SERVER returns error:'.$result['error-codes'][0]);\n            }\n\n            return $result['score'] > 0.5 && $result['success'];\n        }catch (\\Exception $e) {\n            report($e);\n            return false;\n        }\n    }\n}\n","php",[63,1166,1167,1172,1177,1182,1186,1191,1196,1201,1206,1211,1216,1221,1226,1231,1236,1241,1246,1251,1256,1260,1264,1269,1274,1279,1283,1288,1293,1298,1303,1309,1315,1320,1326,1332,1338,1344,1349,1355],{"__ignoreMap":96},[100,1168,1169],{"class":102,"line":103},[100,1170,1171],{},"class Controller extends BaseController\n",[100,1173,1174],{"class":102,"line":121},[100,1175,1176],{},"{\n",[100,1178,1179],{"class":102,"line":131},[100,1180,1181],{},"    use AuthorizesRequests, DispatchesJobs, ValidatesRequests;\n",[100,1183,1184],{"class":102,"line":142},[100,1185,163],{"emptyLinePlaceholder":162},[100,1187,1188],{"class":102,"line":149},[100,1189,1190],{},"    public function checkRecaptcha(Request $request){\n",[100,1192,1193],{"class":102,"line":159},[100,1194,1195],{},"        try {\n",[100,1197,1198],{"class":102,"line":166},[100,1199,1200],{},"            $client = new \\GuzzleHttp\\Client([\n",[100,1202,1203],{"class":102,"line":176},[100,1204,1205],{},"                'headers' => [\n",[100,1207,1208],{"class":102,"line":202},[100,1209,1210],{},"                    'Content-Type' => 'application\u002Fjson',\n",[100,1212,1213],{"class":102,"line":245},[100,1214,1215],{},"                ],\n",[100,1217,1218],{"class":102,"line":276},[100,1219,1220],{},"            ]);\n",[100,1222,1223],{"class":102,"line":286},[100,1224,1225],{},"    \n",[100,1227,1228],{"class":102,"line":295},[100,1229,1230],{},"            $promise = $client->postAsync('https:\u002F\u002Fwww.google.com\u002Frecaptcha\u002Fapi\u002Fsiteverify',\n",[100,1232,1233],{"class":102,"line":563},[100,1234,1235],{},"            [\n",[100,1237,1238],{"class":102,"line":572},[100,1239,1240],{},"                'form_params' =>[\n",[100,1242,1243],{"class":102,"line":827},[100,1244,1245],{},"                    'secret'=>env('RECAPTCHA_SERVER_KEY'),\n",[100,1247,1248],{"class":102,"line":836},[100,1249,1250],{},"                    'response'=>$request->recaptcha\n",[100,1252,1253],{"class":102,"line":859},[100,1254,1255],{},"                ]\n",[100,1257,1258],{"class":102,"line":878},[100,1259,1220],{},[100,1261,1262],{"class":102,"line":898},[100,1263,1225],{},[100,1265,1266],{"class":102,"line":960},[100,1267,1268],{},"            $res = Promise\\Utils::settle($promise)->wait();\n",[100,1270,1271],{"class":102,"line":992},[100,1272,1273],{},"            $isFulfilled = isset($res[0]['value']);\n",[100,1275,1276],{"class":102,"line":1019},[100,1277,1278],{},"            if(!$isFulfilled) throw new \\Exception('RECAPTCHA SERVER returns error');\n",[100,1280,1281],{"class":102,"line":1029},[100,1282,1225],{},[100,1284,1285],{"class":102,"line":1039},[100,1286,1287],{},"            $result = json_decode($res[0]['value']->getBody()->getContents(),true);\n",[100,1289,1290],{"class":102,"line":1045},[100,1291,1292],{},"            \n",[100,1294,1295],{"class":102,"line":1085},[100,1296,1297],{},"            if(isset($result['error-codes'])){\n",[100,1299,1300],{"class":102,"line":1094},[100,1301,1302],{},"                if($result['error-codes'][0] === 'timeout-or-duplicate') return false;\n",[100,1304,1306],{"class":102,"line":1305},29,[100,1307,1308],{},"                throw new \\Exception('RECAPTCHA SERVER returns error:'.$result['error-codes'][0]);\n",[100,1310,1312],{"class":102,"line":1311},30,[100,1313,1314],{},"            }\n",[100,1316,1318],{"class":102,"line":1317},31,[100,1319,163],{"emptyLinePlaceholder":162},[100,1321,1323],{"class":102,"line":1322},32,[100,1324,1325],{},"            return $result['score'] > 0.5 && $result['success'];\n",[100,1327,1329],{"class":102,"line":1328},33,[100,1330,1331],{},"        }catch (\\Exception $e) {\n",[100,1333,1335],{"class":102,"line":1334},34,[100,1336,1337],{},"            report($e);\n",[100,1339,1341],{"class":102,"line":1340},35,[100,1342,1343],{},"            return false;\n",[100,1345,1347],{"class":102,"line":1346},36,[100,1348,1042],{},[100,1350,1352],{"class":102,"line":1351},37,[100,1353,1354],{},"    }\n",[100,1356,1358],{"class":102,"line":1357},38,[100,1359,1360],{},"}\n",[10,1362,1363,1364,1367,1368,1371],{},"recaptchaとのAPI通信には",[63,1365,1366],{},"Guzzle","を使用していますが、とにかくAPI通信ができれば大丈夫です。APIは",[63,1369,1370],{},"https:\u002F\u002Fwww.google.com\u002Frecaptcha\u002Fapi\u002Fsiteverify","にPOSTを送信します。POSTには以下の値が必要です、",[91,1373,1375],{"className":1162,"code":1374,"language":1164,"meta":96,"style":96},"'form_params' =>[\n    'secret'=>env('RECAPTCHA_SERVER_KEY'),\n    'response'=>$request->recaptcha\n]\n",[63,1376,1377,1382,1387,1392],{"__ignoreMap":96},[100,1378,1379],{"class":102,"line":103},[100,1380,1381],{},"'form_params' =>[\n",[100,1383,1384],{"class":102,"line":121},[100,1385,1386],{},"    'secret'=>env('RECAPTCHA_SERVER_KEY'),\n",[100,1388,1389],{"class":102,"line":131},[100,1390,1391],{},"    'response'=>$request->recaptcha\n",[100,1393,1394],{"class":102,"line":142},[100,1395,1396],{},"]\n",[10,1398,1399,1402,1403,1406,1407,1409,1410,1414],{},[63,1400,1401],{},"env('RECAPTCHA_SERVER_KEY')","はバックエンドで使用するrecaptchaキーです。",[63,1404,1405],{},"$request->recaptcha","は",[63,1408,591],{},"で挿入されたフロントで取得したrecaptchaのトークンです。このトークンとキーを合わせて、 ",[1411,1412,1413],"strong",{},"保護対象のサーバーであり、検証を行うフォーム送信","　を判別しています。",[10,1416,1417],{},"通信が成功すると以下の様なレスポンスが戻ります。",[91,1419,1421],{"className":1162,"code":1420,"language":1164,"meta":96,"style":96},"[\n  \"success\"=> true, \n  \"score\"=> 0.8,\n  \"action\"=> string,\n  \"challenge_ts\"=> timestamp,\n  \"hostname\"=> string,\n]\n",[63,1422,1423,1428,1433,1438,1443,1448,1453],{"__ignoreMap":96},[100,1424,1425],{"class":102,"line":103},[100,1426,1427],{},"[\n",[100,1429,1430],{"class":102,"line":121},[100,1431,1432],{},"  \"success\"=> true, \n",[100,1434,1435],{"class":102,"line":131},[100,1436,1437],{},"  \"score\"=> 0.8,\n",[100,1439,1440],{"class":102,"line":142},[100,1441,1442],{},"  \"action\"=> string,\n",[100,1444,1445],{"class":102,"line":149},[100,1446,1447],{},"  \"challenge_ts\"=> timestamp,\n",[100,1449,1450],{"class":102,"line":159},[100,1451,1452],{},"  \"hostname\"=> string,\n",[100,1454,1455],{"class":102,"line":166},[100,1456,1396],{},[10,1458,1459,1460,1463,1464,1467],{},"一番重要なのは",[63,1461,1462],{},"\"score\"=> 0.8","です。このスコアは入力したリクエストがbotか人間かのスコアを示しており、1に近いほど人間が入力しています。逆に0.1あたりはbotの入力です。どこまで厳しくするかはお任せしますが、私は0.5以上であれば人間のリクエストであるとしています。",[63,1465,1466],{},"return $result['score'] > 0.5"," としてfalseであればリクエストを拒否したり、エラーを返す様にします。フォーム系で汎用的に使用できる様に私はサービスプロバイダにしています。",[329,1469,1470],{"id":1470},"エラー処理",[10,1472,1473],{},"エラーの場合は以下の様なレスポンスがきます。（例です）",[91,1475,1477],{"className":1162,"code":1476,"language":1164,"meta":96,"style":96},"[\n  \"success\"=> false, \n  \"action\"=> string,\n  \"challenge_ts\"=> timestamp,\n  \"hostname\"=> string,\n  \"error-codes\": [\n      0=>'timeout-or-duplicate'\n  ] \n]\n",[63,1478,1479,1483,1488,1492,1496,1500,1505,1510,1515],{"__ignoreMap":96},[100,1480,1481],{"class":102,"line":103},[100,1482,1427],{},[100,1484,1485],{"class":102,"line":121},[100,1486,1487],{},"  \"success\"=> false, \n",[100,1489,1490],{"class":102,"line":131},[100,1491,1442],{},[100,1493,1494],{"class":102,"line":142},[100,1495,1447],{},[100,1497,1498],{"class":102,"line":149},[100,1499,1452],{},[100,1501,1502],{"class":102,"line":159},[100,1503,1504],{},"  \"error-codes\": [\n",[100,1506,1507],{"class":102,"line":166},[100,1508,1509],{},"      0=>'timeout-or-duplicate'\n",[100,1511,1512],{"class":102,"line":176},[100,1513,1514],{},"  ] \n",[100,1516,1517],{"class":102,"line":202},[100,1518,1396],{},[10,1520,1521,1522,1525],{},"このエラーはAPIの通信が失敗したり、必要なパラメーターが不足していたりなどのエラーです。 ",[1411,1523,1524],{},"リクエストがBotである"," という意味ではないので注意。Botかの判定はあくまで成功時に取得するscoreで判定します。",[329,1527,1528],{"id":1528},"エラーの説明",[10,1530,1531,1534,1536],{},[1411,1532,1533],{},"missing-input-secret",[63,1535,1401],{},"のようなサーバー側のrecaptchaのキーを忘れている。",[91,1538,1540],{"className":1162,"code":1539,"language":1164,"meta":96,"style":96},"'form_params' =>[\n    'secret'=>env('RECAPTCHA_SERVER_KEY'), \u002F\u002F このへん\n    'response'=>$request->recaptcha\n]\n",[63,1541,1542,1546,1554,1558],{"__ignoreMap":96},[100,1543,1544],{"class":102,"line":103},[100,1545,1381],{},[100,1547,1548,1551],{"class":102,"line":121},[100,1549,1550],{},"    'secret'=>env('RECAPTCHA_SERVER_KEY'),",[100,1552,1553],{}," \u002F\u002F このへん\n",[100,1555,1556],{"class":102,"line":131},[100,1557,1391],{},[100,1559,1560],{"class":102,"line":142},[100,1561,1396],{},[10,1563,1564,1567,1569],{},[1411,1565,1566],{},"invalid-input-secret",[63,1568,1401],{},"が不正。間違っているキーを使用している。キーが正しいか、保護対象のドメインとして登録しているかを確認。",[10,1571,1572,1575,1578],{},[1411,1573,1574],{},"missing-input-response",[63,1576,1577],{},"'response'=>$request->recaptcha","を忘れている、空文字。",[10,1580,1581,1584,1586],{},[1411,1582,1583],{},"invalid-input-response",[63,1585,1577],{},"の値が不正。型などを確認。",[10,1588,1589,1592],{},[1411,1590,1591],{},"bad-request","\nPOSTで送っているかを確認。",[10,1594,1595,1598,1600],{},[1411,1596,1597],{},"timeout-or-duplicate",[63,1599,1577],{},"の値を二回送っているか、フロントのトークンが２分以上経過した。",[10,1602,1603],{},"フロントで取得したトークンはバックエンドでのこの検証を行うともう一度利用することができません。またこのトークンは",[91,1605,1609],{"className":1606,"code":1607,"language":1608,"meta":96,"style":96},"language-javascript shiki shiki-themes material-theme-ocean","grecaptcha.execute('YOUR_FRONT_KEY', {action: 'submit'}).then(function(token) {\n    document.getElementById(\"recaptcha\").value=token;\n    document.getElementById(\"test-form\").submit();\n});\n","javascript",[63,1610,1611,1661,1690,1716],{"__ignoreMap":96},[100,1612,1613,1615,1617,1619,1621,1623,1625,1627,1629,1631,1633,1635,1637,1639,1641,1643,1645,1647,1649,1651,1653,1655,1657,1659],{"class":102,"line":103},[100,1614,1117],{"class":862},[100,1616,866],{"class":106},[100,1618,906],{"class":842},[100,1620,846],{"class":862},[100,1622,911],{"class":106},[100,1624,587],{"class":194},[100,1626,911],{"class":106},[100,1628,918],{"class":106},[100,1630,921],{"class":106},[100,1632,924],{"class":110},[100,1634,927],{"class":106},[100,1636,930],{"class":106},[100,1638,258],{"class":194},[100,1640,911],{"class":106},[100,1642,937],{"class":106},[100,1644,853],{"class":862},[100,1646,866],{"class":106},[100,1648,944],{"class":842},[100,1650,846],{"class":862},[100,1652,891],{"class":114},[100,1654,846],{"class":106},[100,1656,953],{"class":849},[100,1658,853],{"class":106},[100,1660,856],{"class":106},[100,1662,1663,1666,1668,1670,1672,1674,1676,1678,1680,1682,1684,1686,1688],{"class":102,"line":121},[100,1664,1665],{"class":862},"    document",[100,1667,866],{"class":106},[100,1669,968],{"class":842},[100,1671,846],{"class":110},[100,1673,191],{"class":106},[100,1675,512],{"class":194},[100,1677,191],{"class":106},[100,1679,853],{"class":110},[100,1681,866],{"class":106},[100,1683,983],{"class":862},[100,1685,188],{"class":106},[100,1687,953],{"class":862},[100,1689,875],{"class":106},[100,1691,1692,1694,1696,1698,1700,1702,1704,1706,1708,1710,1712,1714],{"class":102,"line":131},[100,1693,1665],{"class":862},[100,1695,866],{"class":106},[100,1697,968],{"class":842},[100,1699,846],{"class":110},[100,1701,191],{"class":106},[100,1703,446],{"class":194},[100,1705,191],{"class":106},[100,1707,853],{"class":110},[100,1709,866],{"class":106},[100,1711,258],{"class":842},[100,1713,872],{"class":110},[100,1715,875],{"class":106},[100,1717,1718,1720,1722],{"class":102,"line":142},[100,1719,937],{"class":106},[100,1721,853],{"class":862},[100,1723,875],{"class":106},[10,1725,1726],{},"の実行から２分以内で利用する必要があります。そのためページがリロードされた瞬間ときに実行していると、フォーム入力中に時間切れになったります。そのためsubmit時に実行することをお勧めします。",[23,1728,1729],{"id":1729},"実装まとめ",[10,1731,1732],{},"以上がrecaptchaの実装方法です。recaptchaはあくまでBotかどうかの判断のみをしているので、実際にリクエストを通すかはアプリケーション側の仕事です。フロントとバックでの実装が少し面倒ですが、recaptchaの機能を自前で実装しようとするとそこそこ、面倒なのと実績のあるGoogle様に検証してもらうのも結構安心です。",[10,1734,1735],{},"バックエンドはLaravelを想定しますが、他のフレームワークや言語でもやることは特に変わりません。上手くご自身の環境に置き換えてください。",[1737,1738,1739],"style",{},"html pre.shiki code .sAklC, html code.shiki .sAklC{--shiki-default:#89DDFF}html pre.shiki code .s-wAU, html code.shiki .s-wAU{--shiki-default:#F07178}html pre.shiki code .sJ14y, html code.shiki .sJ14y{--shiki-default:#C792EA}html pre.shiki code .sC9rS, html code.shiki .sC9rS{--shiki-default:#464B5D;--shiki-default-font-style:italic}html pre.shiki code .sfyAc, html code.shiki .sfyAc{--shiki-default:#C3E88D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sdLwU, html code.shiki .sdLwU{--shiki-default:#82AAFF}html pre.shiki code .s7ZW3, html code.shiki .s7ZW3{--shiki-default:#BABED8;--shiki-default-font-style:italic}html pre.shiki code .s0W1g, html code.shiki .s0W1g{--shiki-default:#BABED8}",{"title":96,"searchDepth":131,"depth":131,"links":1741},[1742,1743,1744,1745,1749,1753],{"id":25,"depth":121,"text":26},{"id":32,"depth":121,"text":32},{"id":41,"depth":121,"text":42},{"id":86,"depth":121,"text":86,"children":1746},[1747,1748],{"id":331,"depth":131,"text":332},{"id":595,"depth":131,"text":595},{"id":1139,"depth":121,"text":1139,"children":1750},[1751,1752],{"id":1470,"depth":131,"text":1470},{"id":1528,"depth":131,"text":1528},{"id":1729,"depth":121,"text":1729},[1755],"devstack","2021-05-21","reCAPTCHAのbot検証をフロントエンド とLaravelでのバックエンドの実装を行います。","md",null,{},"\u002Farticles\u002Fimplement-recaptcha",{"title":5,"description":1757},"articles\u002Fimplement-recaptcha",[1164,1765,1766,1767],"js","laravel","security","_mix\u002Flaravel-recaptcha.jpg","AYarFTQUZE-KIUOU5A3co7uCUDW9hqT091VEnFYUN6Q",1780987150229]