オンライン決済では、不正利用への対策が常に求められます。
2022年のStripeによる調査では、この2年でクレジットカードの不正利用が増加し、より巧妙化していることが判明しました。
https://stripe.com/jp/guides/state-of-online-fraud
その中でも、調査対象者の40%以上が、「カードテスティング攻撃」を受けたことがあると回答しています。カードテスティングは、盗難カードの情報が有効で買い物に使用できるかどうかを確かめるために行われます。不正使用者は盗難クレジットカードの情報を購入し、それらのカードを使って認証や購入処理を行い、どのカードが現在も有効であるかを判断します。
カードテスティングによるビジネスへの影響
カードテスティングを受けると、「不審請求の請求(チャージバック申請)」が増加する恐れがあります。実際に支払いが発生するカードフォームでテスティングが行われた場合、支払いが成功し、本来のカード所有者に対して請求が行われます。カード所有者が見覚えのない請求に気づくと、カード会社に対してチャージバックの申請を行い、それによってStripeアカウントオーナーは不審請求への対応や返金処理、そして追加の手数料支払いなどが発生します。
また、カードテスティングによって所有するStripeアカウントでの「支払い拒否率」が高まると、カード発行会社とカードネットワークでのビジネスの評価の低下につながります。また、不審請求の請求件数が一定の割合を超えると、カードネットワークスごとに運用されるモニタリングプログラムに登録されます。モニタリングプログラムに登録されると、不審請求の申請や不正使用のレベルが持続的に下がるまで、月ごとに反則金と追加手数料が発生する場合があります。
この他にも、botによるカードテスティング行為が集中することで、インフラへの負荷が高まるリスクなども存在します。
Stripeでのカードテスティング対策
StripeではCheckoutやPayment Links、Elementsを利用している場合、疑わしい支払いに対して自動的にCAPTCHAを自動実行します。
また、決済に対して以下の情報を含めることで、カードテスティングモデルの性能に大きな影響を与えることができます。
- Stripe SDKの高度な不正使用検出機能
- IPアドレス
- 顧客のメールアドレス
- 顧客の名前
- 請求先住所
Stripe Checkoutで追加情報を集める方法
Stripe Checkoutでは、デフォルトで氏名とメールアドレスの入力欄が用意されています。
請求先住所についても、billing_address_collection
をrequired
に設定することで、入力欄を追加できます。
await stripe.checkout.sessions.create({
+ billing_address_collection: 'required',
line_items: [{
price: 'price_xxxx',
quantity: 1,
}],
success_url: 'https://example.com',
mode: 'payment'
})
Stripe Elementsで追加情報を集める方法
Stripe Elementsでは、AddressElement
を利用します。
<form id="payment-form">
<div id="payment-element"></div>
+ <div id="address-element"></div>
<button type="submit">Sign in</button>
</form>
<script src="https://js.stripe.com/v3/"></script>
<script>
const stripe = Stripe('pk_test_から始まるStripe公開可能キーを設定する');
const elementsAppearance = {
theme: 'light'
};
const options = {
mode: 'payment',
amount: 1000,
currency: 'jpy',
appearance: elementsAppearance,
};
const elements = stripe.elements(options);
const paymentElement = elements.create('payment');
paymentElement.mount('#payment-element');
+const addressElement = elements.create('address', {
+ mode: 'billing',
+ appearance: elementsAppearance,
+});
+addressElement.mount('#address-element');
</script>
これによって、氏名や請求先住所を決済情報に含めることができます。
Cloudflare Turnstileを利用した、「追加のカードテスティング対策」
Stripeが提供するカードテスティング対策に追加して、botからのアクセスをブロックしたい場合、Cloudflare Turnstileを追加することもできます。
Cloudflareが提供するコードスニペットを追加し、決済処理を実行する前にブロックしたり、3Dセキュア認証の要求や支払いのレビュー対象にすることができます。
Cloudflare Turnstileと、Stripe Elementsを連携させた決済フォームを作る方法
ここからは、Cloudflare TurnstileとStripe Elementsを連携させる方法を紹介します。
Honoでアプリをセットアップする
JavaScriptを利用したAPIとフロントエンドを作るため、Honoを利用してセットアップを行います。
$ npm create hono@latest stripe-turnstile
「どのプラットフォームで利用するか」を聞かれます。ローカル環境とデプロイを効率化するためのツール「Wrangler」を利用するため、cloudflare-workers
を選択しましょう。
create-hono version 0.2.6
? Which template do you want to use? › - Use arrow-keys. Return to submit.
↑ cloudflare-pages
❯ cloudflare-workers
deno
fastly
lagon
lambda-edge
netlify
nextjs
nodejs
vercel
以下のメッセージが表示されれば、セットアップ完了です。
✔ Which template do you want to use? › cloudflare-workers
cloned honojs/starter#main to /Users/sandbox/stripe-turnstile
✔ Copied project files
WranglerでCloudflareアカウントと接続する
ここからはWranglerを利用してローカル開発を行います。
CLIコマンドを、npmでインストールしましょう。
% npm install wrangler --save-dev
その後、wrangler login
コマンドでCloudflareアカウントにログインします。
% wrangler login
これでローカル開発とデプロイの準備ができました。
決済フォームを表示する
続いてStripe Elementsを利用した決済フォームを作成します。
JSXに対応したファイル拡張子に変更する
JSXを利用してHTMLを動的に作成しますので、src/index.ts
をsrc/index.tsx
に変更しましょう。
mv src/index.ts src/index.tsx
同時に、package.json
のscripts
に指定されているファイル名も変更しましょう。
"scripts": {
- "dev": "wrangler dev src/index.ts",
- "deploy": "wrangler deploy --minify src/index.ts"
+ "dev": "wrangler dev src/index.tsx",
+ "deploy": "wrangler deploy --minify src/index.tsx"
},
StripeのAPIキーと、Turnstileのキーを環境変数に登録しよう
StripeとCloudflare Turnstileそれぞれを利用するため、APIキーを環境変数に登録しましょう。
Cloudflare Turnstileは、テスト用のサイトキーとシークレットキーをドキュメントから取得できます。
Stripeの公開可能キーとシークレットキーは、Stripeダッシュボードから取得しましょう。
TURNSTILE_SITE_KEY = '1x00000000000000000000AA'
TURNSTILE_SECRET_KEY = '1x0000000000000000000000000000000AA'
STRIPE_PUBLISHABLE_KEY='pk_test_から始まる公開可能キー'
STRIPE_SECRET_KEY='sk_test_から始まるシークレットキー'
HonoとWranglerを利用したローカル開発では、.dev.vars
に設定した値を、次のような書き方で取得できます。
app.get('/hello', async c => {
console.log(c.env.TURNSTILE_SITE_KEY);
return c.json({ message: 'test' });
})
HTMLを作成しよう
続いて決済フォームを表示するコードを実装しましょう。
src/index.tsx
のコードを、次のように変更します。
import { Hono } from 'hono';
import { FC } from 'hono/jsx';
import { html } from 'hono/html';
type Bindings = {
TURNSTILE_SITE_KEY: string;
TURNSTILE_SECRET_KEY: string;
STRIPE_PUBLISHABLE_KEY: string;
STRIPE_SECRET_KEY: string;
};
const app = new Hono<{
Bindings: Bindings
}>();
const Top:FC<{
TURNSTILE_SITE_KEY: string;
STRIPE_PUBLISHABLE_KEY: string;
}> = ({TURNSTILE_SITE_KEY: siteKey, STRIPE_PUBLISHABLE_KEY: stripePublishableKey}) => {
return (
<html>
<head>
<title>決済フォームデモ</title>
<style>
{html`
html,
body {
height: 100%;
}
body {
display: flex;
align-items: center;
justify-content: center;
padding-top: 40px;
padding-bottom: 40px;
background-color: #fefefe;
}
form > * {
margin-bottom: 20px;
}
`}
</style>
</head>
<body>
<form id="payment-form">
<div id="payment-element"></div>
<div id="address-element"></div>
<div id="result"></div>
<button type="submit">Order</button>
</form>
{html`
<script src="https://js.stripe.com/v3/"></script>
<script>
const stripe = Stripe('${stripePublishableKey}');
const elementsAppearance = {
theme: 'stripe'
};
const options = {
mode: 'payment',
amount: 1000,
currency: 'jpy',
appearance: elementsAppearance
};
const elements = stripe.elements(options);
const paymentElement = elements.create('payment');
paymentElement.mount('#payment-element');
const addressElement = elements.create('address', {
mode: 'billing',
appearance: elementsAppearance,
});
addressElement.mount('#address-element');
</script>
`}
</body>
</html>
);
};
app.get('/', (c) => {
return c.html(<Top
STRIPE_PUBLISHABLE_KEY={c.env.STRIPE_PUBLISHABLE_KEY}
TURNSTILE_SITE_KEY={c.env.TURNSTILE_SITE_KEY}
/>)
});
export default app;
このコードでは、決済ページのHTMLを、Top
コンポーネントとしてJSXで定義しています。また、定義したJSXはc.html()
でレンダリングし、決済フォームの描画に必要なStripeの公開可能キーを環境変数から受け取っています。
Wranglerで決済フォームを表示する
実装した決済フォームの表示を確認しましょう。
npm run dev
を実行すると、Wrangler経由でアプリが立ち上がります。
$ npm run dev
[mf:inf] Updated and ready on http://0.0.0.0:8787
[mf:inf] - http://127.0.0.1:8787
[mf:inf] - http://192.168.86.21:8787
[mf:inf] - http://172.18.144.83:8787
http://127.0.0.1:8787
にアクセスすると、Stripeの決済フォームが表示されます。
Stripe Payment Intentで、決済処理を実行する
続いて決済処理を実装しましょう。
決済処理を行うために必要なPayment Intentを作成するので、Stripe SDKをインストールします。
% npm i stripe
続いてsrc/index.tsx
にPOST /payment-intent
APIを追加しましょう。
+import Stripe from 'stripe';
...
+app.post('/payment-intent', async c => {
+ const stripe = new Stripe(c.env.STRIPE_SECRET_KEY, {
+ apiVersion: '2023-08-16',
+ appInfo: {
+ name: 'qiita-example/cloudflare-turnstile-example'
+ }
+ });
+ const paymentIntent = await stripe.paymentIntents.create({
+ amount: 1000,
+ currency: 'jpy'
+ });
+ return c.json({
+ client_secret: paymentIntent.client_secret
+ });
+});
ここでは、1,000円の支払いを行うための、Payment Intentを作成し、クライアント用のシークレットをレスポンスとして返すAPIを追加しています。
このAPIを利用して、決済処理を行うコードをフロントエンド側に追加しましょう。
+ let submitButon = document.querySelector("button[type='submit']");
+ const resultElement = document.getElementById('result');
...
paymentElement.mount('#payment-element')
+ const paymentForm = document.getElementById('payment-form')
+ paymentForm.addEventListener('submit', async e => {
+ e.preventDefault();
+ if (submitButon) {
+ submitButon.setAttribute('disabled', true);
+ }
+ const { error: submitError } = await elements.submit();
+ if (submitError) {
+ console.log(submitError);
+ submitButon.removeAttribute('disabled');
+ return;
+ }
+ const response = await fetch('/payment-intent', {
+ method: "POST",
+ })
+ const { client_secret: clientSecret } = await response.json();
+ const { error: confirmationError } = await stripe.confirmPayment({
+ elements,
+ clientSecret,
+ confirmParams: {
+ return_url: 'http://localhost:8787'
+ }
+ });
+ submitButon.removeAttribute('disabled');
+ console.log(confirmationError);
+ resultElement.innerHTML = JSON.stringify(confirmationError, null, 2);
+ })
Stripeが提供するテスト用カード番号を入力することで、支払い処理の成功や、カードのエラー処理などが確認できます。
Cloudflare Turnstileで、botによるフォーム操作をブロックする
Cloudflare Turnstileを利用する場合、「Turnstileの検証に通過した場合のみ注文できるようにする」実装が行えます。
これによって、botによるPayment Intentを作成するAPI呼び出し数を減らすことができます。
src/index.tsx
に、Turnstile用のHTMLを追加しましょう。
<form id="payment-form">
<div id="payment-element"></div>
<div id="address-element"></div>
<div id="result"></div>
+ <div class="cf-turnstile" data-sitekey={`${siteKey}`}></div>
<button type="submit">Order</button>
</form>
{html`
+ <script src="https://challenges.cloudflare.com/turnstile/v0/api.js?onload=_turnstileCb" async defer></script>
<script src="https://js.stripe.com/v3/"></script>
続いて、Stripe Elementsの決済処理に、Turnstileを組み込みます。
- <button type="submit">Order</button>
+ <button type="submit" disabled>Order</button>
</form>
{html`
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?onload=_turnstileCb" async defer></script>
<script src="https://js.stripe.com/v3/"></script>
<script>
+ let turnstileToken = '';
let submitButon = document.querySelector("button[type='submit']");
+ function _turnstileCb() {
+ turnstile.render('.cf-turnstile', {
+ callback: function(token) {
+ turnstileToken = token;
+ submitButon.removeAttribute('disabled');
+ },
+ })
+ }
Turnstileの認証結果をturnstile.render
のcallback
で受け取り、成功した場合のみsubmit
ボタンのdisabled
属性を削除しています。
このため、Turnstileの検証に失敗した場合は、フォームのボタンを押すことができません。
フォームのsubmit処理を次のように変更することで、submit処理側でもブロックできます。
paymentForm.addEventListener('submit', async e => {
e.preventDefault();
+ if (!turnstileToken) return;
if (submitButon) {
Cloudflare Turnstileによるサーバー側の検証も実施する
Cloudflare Turnstileでは、フロントエンドでの認証結果を使って、サーバー側で検証を行うことができます。
POST /payment-intent
APIを編集して、こちらでも検証を行えるようにしましょう。
+import { HTTPException } from 'hono/http-exception';
...
app.post('/payment-intent', async c => {
+ const body = await c.req.json();
+ const ip = c.req.header('CF-Connecting-IP')
+ const formData = new FormData();
+ formData.append('secret', c.env.TURNSTILE_SECRET_KEY);
+ formData.append('response', body.turnstileToken);
+ formData.append('remoteip', ip || '');
+ const turnstileResult = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
+ body: formData,
+ method: 'POST',
+ });
+ const outcome = await turnstileResult.json<TurnstileResult>();
+ if (!outcome.success) {
+ throw new HTTPException(401, {
+ message: JSON.stringify(outcome)
+ });
+ }
const clientSecret = ''
return c.json({
client_secret: clientSecret
})
})
export default app
フロントエンドアプリのコードを変更し、クライアント側のTurnstileで取得したトークンをAPIに送信しましょう。
const response = await fetch('/payment-intent', {
method: "POST",
+ body: JSON.stringify({
+ turnstileToken,
+ })
})
環境変数のTURNSTILE_SECRET_KEY
を、「常に失敗するキー」に変更しましょう。
-TURNSTILE_SECRET_KEY = '1x0000000000000000000000000000000AA'
+TURNSTILE_SECRET_KEY = '2x0000000000000000000000000000000AA'
npm run dev
を実行し直して、決済フォームを操作すると、下の画像のようなエラーが発生します。
このようにして、Stripe側のチェックシステムに加えて、Cloudflareのbot対策機能を決済フローに組み込むことができます。
Turnstileの検証結果を、3DセキュアやStripe Radarの判定に利用する方法
「Turnstileの結果を元に決済をブロックする」代わりに、Stripe側で追加の確認を行うフローにすることもできます。
まず、フロントエンドアプリのブロック処理を削除しましょう。
- <button class="w-100 btn btn-lg btn-primary" type="submit" disabled>Sign in</button>
+ <button class="w-100 btn btn-lg btn-primary" type="submit" >Sign in</button>
...
paymentForm.addEventListener('submit', async e => {
e.preventDefault();
- if (!turnstileToken) return;
if (submitButon) {
submitButon.setAttribute('disabled', true);
}
続いてPOST /payment-intent
の処理を次のように変更します。
const outcome = await turnstileResult.json<TurnstileResult>();
- if (!outcome.success) {
- throw new HTTPException(401, {
- message: JSON.stringify(outcome)
- });
- }
const stripe = new Stripe(c.env.STRIPE_SECRET_KEY, {
...
const paymentIntent = await stripe.paymentIntents.create({
amount: 1000,
currency: 'jpy',
+ metadata: {
+ turnstile_result: outcome.success ? 'success' : 'failed',
+ },
+ payment_method_options: {
+ card: {
+ request_three_d_secure: outcome.success ? 'automatic' : 'any',
+ }
+ }
});
payment_method_options[card][request_three_d_secure]
をany
に変更することで、顧客に3Dセキュアによる追加認証を手動で要求することができます。
また、Stripe Radar for Teamsを利用されている場合は、ルールを作成することで、「レビューが必要な決済」としてマークすることもできます。
レビューリストに支払いを追加することで、Cloudflare Turnstileが「botなどからの決済である可能性が高い」と判断した支払いに対して手動でレビューを行うことができます。
また、outcome.challenge_ts
を利用して、「いつ認証が成功したか」をmetadataに含めることもできます。
const paymentIntent = await stripe.paymentIntents.create({
amount: 1000,
currency: 'jpy',
metadata: {
turnstile_result: outcome.success ? 'success' : 'failed',
+ turnstile_challenge_ts: outcome.challenge_ts,
}
});
これらのデータを活用して、オンライン決済をより安全に顧客へ提供することができます。
追加の学習リソース
StripeとCloudflare Turnstileを利用した不正決済・botアクセス対策については、以下の記事やガイドも併せてご覧ください。
また、「オンライン不正行為についてのレポート」を公開しています。
不正利用への対策や業種・国・地域ごとの傾向の違い、そしてStripeを利用した対策方法などについて紹介していますので、ぜひご覧ください。
関連記事