LoginSignup
50
38

More than 3 years have passed since last update.

Stripeでカード決済(3Dセキュア対応)

Posted at

はじめに

Stripeの記事は Qiita でも結構あり、〇番煎じ感満載ですが、3Dセキュア対応はなさそうだったので、記事にすることにしました。

なお、この記事では主に技術面(実装方法)についての話になるため、技術面以外(たとえば手数料など)は記載しません。また、私はクレジットカードの業務は未経験のため、この業界の専門用語(業界用語)はあまり分かりませんので、言葉が間違っている箇所があるかもしれませんが、ご了承ください。

言語は Ruby にしましたが、他の言語でのある程度分かるように、ライブラリを使用せず httpclient を使用しました。実際の開発では(ある言語なら)ライブラリを使ったほうが良いです。

Stripeとは

カード決済サービスです。(ここを読む人に、これ以上の説明いらないですよね)

カード決済で、よくありそうなユースケース

  1. 1回だけの支払い (Checkout)
  2. 初回はカード登録のみ、後に請求する (SetupIntents, PaymentMethod, PaymentIntents)
  3. 初回に支払いとカード登録の両方、後にも請求する (PaymentMethod, PaymentIntents)

(カッコ内は、使用する Stripe の API名)

1. は説明不要、2. はサブスクのような、入会時にカード登録だけして、その後月次請求するようなユースケース、3. はショッピングサイトのような、購入の決済時に、後の買い物のためにカード登録も行うようなユースケースです。おそらくこの3つのパターンのどれかに収まるのではないかと思います。また「後の請求」とは、定期/不定期/1回に限らず、カード登録とは別タイミングで請求する場合を指します。この記事では 2.3. のユースケースについて説明します。

Stripeでの処理の流れ

「2. 初回はカード登録のみ、後に請求する」場合

2021-01-08_161516.png

①アプリケーションが、カード入力フォームを返す前に SetupIntentを生成する
②顧客のリクエストに対し、③Stripe Element(Stripe.js)を使ってカード入力フォームを返す
④顧客はカード情報をStripeに送信する
⑤Stripeはカード情報から PaymentMethod を返し、⑥アプリケーションに送る
⑦アプリケーションはCustomerを作成し、それにPaymentMethodを付けることで、⑧Stripeがカード情報をCustomerに紐づける

「3. 初回に支払いとカード登録の両方、後にも請求する」場合

2021-01-13_173323.png

①アプリケーションが カード入力フォームを返す前に 空の Customerを作り、②支払いを表す PaymentIntentを生成する
③顧客のリクエストに対し、④Stripe Element(Stripe.js)を使ってカード入力フォームを返す
⑤顧客はカード情報をStripeに送信する
⑥Stripeがカード情報をCustomerに紐づける
⑦Stripeはカード情報から PaymentMethod を返す(アプリケーションはPaymentMethodは必須ではないので、サーバーに送らなくてもよい)

見ての通り、カード番号などセンシティブな情報はアプリケーションに送らず、トークン(PaymentMethod)という形でアプリケーションに送るため、アプリケーションはセンシティブな情報を保持せず、カード決済が行なえるという仕組みになっています。

3Dセキュアについて

3Dセキュアの認証レベル

日本では2020年現在、3Dセキュアのカードはほとんど無いようですが、今後徐々に普及していくかもしれません。Stripeも、3Dセキュアに対応していない旧API(Charge API)より、今回説明する新API(PaymentIntents API)を推奨しています。

顧客が3Dセキュアのカードを使用すると、カード情報入力後に認証が要求されます。ただし、すべての3Dセキュアのカードが認証必須かというと、そういうわけではなく、認証レベルに応じていろいろあるようです。

https://stripe.com/docs/sources/three-d-secure#check-requirement (英語)

認証レベルは4段階あります。

  • required(必須):3Dセキュアは必須であり、支払いを成功させるために、3Dセキュアのプロセスを完了させなければなりません。
  • recommanded(推奨):3Dセキュアは推奨されます。3Dセキュアのプロセスは必須ではありませんが、強く推奨し、変換レートへの影響を最小限にします。
  • optional(オプション):3Dセキュアはオプションです。3Dセキュアのプロセスは必須でも推奨でもありませんが、詐欺の可能性を減らすために実行することができます。
  • not_supported(サポートされない):このカードでは3Dセキュアはサポートされません。代わりに、通常のカードの支払いとして実行されます。

Stripeを使用すると、認証レベルに応じて認証要不要の判断や認証の画面はStripeが勝手にやってくれるため、実装面で認証レベルを意識することはほとんど無いです。しかし、業務的に意識する場合があるかもしれないので、頭の片隅に入れておきましょう。

認証の免除について

3Dセキュアの認証は、基本的に支払いの時に発生します。これは、「1. 1回だけの支払い」の場合は問題ありませんが、「2. 初回はカード登録のみ、後に請求する」や「3. 初回に支払いとカード登録の両方、後にも請求する」で後に請求するとき、顧客はオンラインにいないため、認証できません。そのため、認証を免除する仕組みがあります。

具体的な免除されるケースは、ここに下手に書くと誤解されそうなので、下記の公式のドキュメントを読んでください。ただし重要なことは、どのようなケースであっても認証の可否は銀行が判断し、その銀行が認証が必要と判断したら顧客の認証が求められるということです。つまり100%認証を免除する方法は無いということです。そのため、請求の際に認証が必要とされるケースを想定して、何かしらの業務的なフローが別途必要になる(たとえばメールで案内して、認証画面に誘導するなど)ということです。

https://stripe.com/ja-us/guides/strong-customer-authentication#段階認証-sca-の免除 (日本語)

ライブラリについて

Ruby、Python、PHP、Java、Node.js、Go、.NET があるようです。導入方法は各言語の標準的な方法みたいなので、公式のドキュメントを見てください。ここに無い言語は、StripeのAPIはすべてRESTfull APIなので、今回の記事のようにHttpClientなどで直接アクセスすれば可能です。

0. 準備する

ようやく本題に入れます。2.3.に共通して必要な準備の手順です。

0-1. アカウントを作成する

ここから、Stripeのアカウントを作成します。

0-2. API鍵を入手する

アカウントを作成してダッシュボードにログインしたら、左側メニューの「開発者」>「APIキー」から、公開鍵とシークレット鍵を取得しておきます。

※検証やテストでは、テスト用の鍵を使うこと

2020-12-16_094407.png

2. 初回はカード登録のみ、後に請求する

https://stripe.com/docs/payments/save-and-reuse (英語)

手順は、ほぼ上記URLに書かれている通りですが、若干手順を変えていて、Customerの生成をSetupIntent生成の前ではなく、カード情報入力後に行っています。先にCustomerを作ってしまうと、顧客がカード情報入力画面で閉じてしまうと、ゴミCustomerが残ってしまうためです。

  • 【サーバーサイド】SetupIntentを生成
  • 【クライアントサイド】カードの入力フォームを表示
  • 【クライアントサイド】(顧客がカード情報を入力後)PaymentMethodを生成
  • 【サーバーサイド】Customerを生成し、PaymentMethodを紐づけ
  • (オプション)【サーバーサイド】オーソリ(与信)をする
  • (後に)【サーバーサイド】請求する

2-1. 【サーバーサイド】共通(HttpClient生成)

https://stripe.com/docs/api/authentication (英語)

Stripe APIの認証はBasic認証です。Basic認証の username に Stripe のシークレット鍵、password は空を指定します。(Bearer認証も使えるみたいです)

require 'sinatra/base'
require 'httpclient'
require 'json'

class App < Sinatra::Base
    @@private_key = "<Stripeのシークレット鍵>"

    def create_http_client
        client = HttpClient.new
        client.set_auth('https://api.stripe.com', @@private_key, '')
        client
    end

2-2. 【サーバーサイド】SetupIntentを生成する

空の(パラメータなしで)SetupIntentを生成して、レスポンスにあるclient_secretを取得します。この値を画面のフォームに埋め込むため、クライアントサイドに渡します。

    get '/' do
        client = create_http_client
        response = client.post('https://api.stripe.com/v1/setup_intents')
        intents = JSON.parse(response.body)

        erb :index, :locals => {
            :client_secret => intents['client_secret'],
        }
    end

2-3. 【クライアントサイド】Stripe Elementを使えるようにする

JavaScriptをロードするだけです。なお、このJavaScriptをあらかじめダウンロードしてローカルのサーバーに配置する、ということもできますが、Stripeとしては非推奨なので、すなおに次のようにしておきましょう。

<script src="https://js.stripe.com/v3/"></script>

2-4. 【クライアントサイド】カードの入力フォームを表示する

Stripe Elementを使用するには、JavaScriptのソースコードの部分を見ての通り、Stripeの公開鍵からStripeオブジェクトを生成し、element() でelementインスタンスを生成した後、create() でElementを生成、その後 DOM Element に mount() するだけです。

フォームの部分は、まず <form>data-secret 属性に先ほどSetupIntent生成時に取得した client_secret の値を埋め込みます。次にカードの入力項目を表示するために、<div id="card-element">を用意しておきます。カード名義人の入力はオプションなので、無くても良いです。

form を2つに分けている理由は、カード情報を入力するテキストボックス内でEnterを押されると、submit イベントが発生してしまうからです。くわしくは、次の節で説明します。

<html>
<head>

...

<script type="text/javascript">
$(function() {
    // elementを生成し、mountする
    var stripe = Stripe('<Stripeの公開鍵>');
    var elements = stripe.elements();

    var cardElement = elements.create('card');
    cardElement.mount('#card-element');

    ...
});
</script>
</head>
<body>
<!-- エラー文言表示領域 -->
<div id='card-errors'></div>
<!-- 入力フォーム -->
<form id='setup-form' data-secret='<%= client_secret %>'>
    <div>カード名義人: <input type='text' id='card-name'></div>
    <div id='card-element'></div>
    <button id='card-button'>カード登録</button>
</form>
<form id='hidden-form' action='/' method='post'>
    <input type='hidden' name='payment-method' value=''>
</form>
</body>
</html>

2-5. 【クライアントサイドサイド】カード情報からPaymentMethodを生成する

confirmCardSetup() を呼び出すだけで、カードの検証と(検証が成功すれば)PaymentMethod が生成されます。もし3Dセキュアのカードが入力されて認証が必要な場合、Stripeが認証の画面を出します。取得したPaymentMethodを(実際はIDだけで良いが)サーバー側に送信します。

もし、カードの検証でエラーがあった場合は(3Dセキュアでの認証失敗も含む)、 confirmCardSetup() がエラーを返します。下のコード片では、Stripeの返すエラーメッセージをそのまま表示するようにしています。エラーメッセージはローカライズされているので、そのまま出力しても問題ないですが、一部マニアックなエラーは英語のままになっているものもあるようです。

イベントのハンドルは、ボタンの click イベントだけでなく、カード情報のテキストボックス内でEnterが押された用に form の submit イベントもハンドルするようにします。サーバー側に送信するために、form.submit() を呼び出しますが、id=setup-formsubmit() をしてしまうと、form の submit イベントをハンドリングしているため、無限ループになってしまいます。

<html>
<head>

<script type="text/javascript">
$(function() {
    ...

    function sendToken(event) {
        event.preventDefault();

        var clientSecret = "<%= client_secret %>";
        stripe.confirmCardSetup(
            clientSecret,
            {
                payment_method: {
                    card: cardNumberElement,
                    billing_details: {
                        name: $('#card-name').val()
                    },
                },
            },
        ).then(function(result) {
            if (!result.error) {
                // The setup has succeeded. Display a success message.
                var form = $('#hidden-form');
                form.find('input[name="payment-method"]').val(result.setupIntent.payment_method);
                form.submit();
            } else {
                // Display error.message in your UI.
                $('#card-errors').text(result.error.message);
            }
        });
    });

    $('#card-button').on('click', sendToken);
    $('#setup-form').on('submit', sendToken);
});
</script>

...

2-6. 【サーバーサイド】Customerを生成し、カード情報を紐づける

Customer を生成するときに、クライアントサイドで主特区した PaymentMethod の ID を指定することで、カード情報を紐づけられた Customer が生成できます。Customer(のID)は、後に請求するときに必要になるので、データベースなどに保存しておきましょう。


    post '/' do
        client = create_http_client

        response = client.post('https://api.stripe.com/v1/customers', {
            'payment_method': params['payment_method'],
            'email': 'test@example.com',
            'description': 'テストユーザー',
        })
        customer = JSON.parse(response.body)
        # 後の請求のために、customer['id'] を保存しておく

        ...
    end

2-7. (オプション)【サーバーサイド】オーソリ(与信)を行う

もし、オーソリ(与信)を行いたい場合は、CustomerとPaymentMethodからPaymentIntentを作ることでオーソリができます。caputure_methodmanual にすることで、請求(キャプチャー)ではなく、オーソリになります。

    post '/' do
        ... 

        response = client.post('https://api.stripe.com/v1/payment_intents', {
            'amount':    999, # 与信額
            'currency': 'jpy', # 与信する金額の通貨コード
            'customer': customer['id'],
            'payment_method': params['payment_method'],
            'off_session': true,
            'confirm': true,
            'capture_method': 'manual',
        })
        payment_intent = JSON.parse(response.body)

        ...
    end

2-8. 【サーバーサイド】(後に)請求する

実際に請求するときは、CustomerとPaymentMethodからPaymentIntentを生成することでできます。PaymentMethodは 2-5 の値でもいいのですが、CustomerからPaymentMethodの一覧を取得することでも、取得できます。

請求は、オーソリ(与信)とは逆に caputure_methodautomatic (デフォルトでこの値になっているため、下記のコード断片では省略している)にします。

    client = create_http_client

    # PaymentMethod一覧を取得
    response1 = client.get('https://api.stripe.com/v1/payment_methods', {
        'customer': <2-7. で生成したCustomerid>,
        'type': 'card',
    })
    payment_methods = JSON.parse(response1.body)
    payment_method = payment_methods[0] # PaymentMehtodは1つしかないはず

    # 請求する
    response2 = client.post('https://api.stripe.com/v1/payment_intents', {
        'amount':    1111, # 請求額
        'currency': 'jpy', # 請求する金額の通貨コード
        'customer': <2-7. で生成したCustomerid>,
        'payment_method': payment_method['id']
        'off_session': true,
        'confirm': true,
    })

3. 初回に支払いとカード登録の両方、後にも請求する

https://stripe.com/docs/payments/save-during-payment (英語)

手順は、上記URLに記載されている通りです。2. と違う点は、Customerを生成する前に先に支払いを完了してしまうと、後から Customer を紐づけできないため、先に(空の)Customerを生成しておく必要があることです。また支払いがあるため、カード情報の紐づけに SetupIntent を使うのではなく、支払いを表す PaymentIntent を使います。

  • 【サーバーサイド】空の Customer を生成
  • 【サーバーサイド】Customer から、支払いを表す PaymentIntent を生成
  • 【クライアントサイド】カードの入力フォームを表示
  • 【クライアントサイド】(顧客がカード情報を入力後)PaymentMethod を生成
  • (後に)【サーバーサイド】請求する

3-1. 【サーバーサイド】共通(HttpClient生成)

2-1. と同じで、Stripe APIの認証をBasic認証でやります。

require 'sinatra/base'
require 'httpclient'
require 'json'

class App < Sinatra::Base
    @@private_key = "<Stripeのシークレット鍵>"

    def create_http_client
        client = HttpClient.new
        client.set_auth('https://api.stripe.com', @@private_key, '')
        client
    end

3-2. 【サーバーサイド】空のCustomerを生成する

支払いを済ませた後では Customer を紐づけできないため、先に空の Customer を生成します。

    get '/' do
        client = create_http_client
        response1 = client.post('https://api.stripe.com/v1/customer')
        customer = JSON.parse(response1.body)

        ...

3-3. 【サーバーサイド】支払い用のPaymentIntentを生成する

SetupIntent の代わりに、PaymentIntentを生成します。その際、Customer のIDをパラメータで渡し、カード情報を紐づけられるようにしておきます。

        ...

        response2 = client.post('https://api.stripe.com/v1/payment_intents', {
            'amount': 1099, # 請求金額
            'currency': 'jpy', # 請求する通貨コード
            'customer': customer['id'],
        })
        intents = JSON.parse(response2.body)

        erb :index, :locals => {
            :client_secret => intents['client_secret'],
        }
    end

3-4. 【クライアントサイド】Stripe Elementを使えるようにする

2-3. と同様です。

<script src="https://js.stripe.com/v3/"></script>

3-5. 【クライアントサイド】カードの入力フォームを表示する

2-4. と同様です。

<html>
<head>

...

<script type="text/javascript">
$(function() {
    // elementを生成し、mountする
    var stripe = Stripe('<Stripeの公開鍵>');
    var elements = stripe.elements();

    var cardElement = elements.create('card');
    cardElement.mount('#card-element');

    ...
});
</script>
</head>
<body>
<form id='setup-form' data-secret='<%= client_secret %>'>
    <div>カード名義人: <input type='text' id='cardholder-name'></div>
    <div id='card-element'></div>
    <button id='card-button'>カード登録</button>
</form>
<form id='hidden-form' action='/' method='post'>
    <input type='hidden' name='payment-method' value=''>
</form>
</body>
</html>

3-6. 【クライアントサイドサイド】カード情報からPaymentMethodを生成する+請求する

2-5. とほぼ同様ですが、支払いがあるため、confirmCardSetup() ではなく、confirmCardPayment() を使います。処理が成功すると、ここでカード情報がCustomerに紐づけされます。なお、PaymentMethodをサーバーに送っていますが、サーバー側は特に使用しないため、送らなくてもよいです。

<html>
<head>

<script type="text/javascript">
$(function() {
    ...

    function sendToken(event) {
        event.preventDefault();

        var clientSecret = "<%= client_secret %>";
        stripe.confirmCardPayment(
            clientSecret,
            {
                payment_method: {
                    card: cardNumberElement,
                    billing_details: {
                        name: $('#cardholder-name').val()
                    },
                },
            },
        ).then(function(result) {
            if (!result.error) {
                // The setup has succeeded. Display a success message.
                var form = $('#hidden-form');
                form.find('input[name="payment-method"]').val(result.paymentIntent.payment_method);
                form.submit();
            } else {
                // Display error.message in your UI.
                $('#card-errors').text(result.error.message);
            }
        });
    });

    $('#card-button').on('click', sendToken);
    $('#setuo-form').on('submit', sendToken);
});

3-8. 【サーバーサイド】(後に)請求する

2-8. と同様です。

    client = create_http_client

    # PaymentMethod一覧を取得
    response1 = client.get('https://api.stripe.com/v1/payment_methods', {
        'customer': <3-2. で生成したCustomerid>,
        'type': 'card',
    })
    payment_methods = JSON.parse(response1.body)
    payment_method = payment_methods[0] # PaymentMehtodは1つしかないはず

    # 請求する
    response2 = client.post('https://api.stripe.com/v1/payment_intents', {
        'amount':    1111, # 請求する金額
        'currency': 'jpy', # 請求する金額の通貨コード
        'customer': <3-2. で生成したCustomerid>,
        'payment_method': payment_method['id']
        'off_session': true,
        'confirm': true,
    })

その他補足

Stripe Elementについて

Stripe Elementでできないこと

  • 有効期限を「年月」の順にする
  • 有効期限の入力をプルダウンにする
  • カード番号、有効期限、CVCに最初から値を入れておく

郵便番号の入力フィールドを常に非表示にする

Stripeが用意するカード情報の入力フォームには、郵便番号が現れたり消えたりしますが、これはカードの国籍(?)によって、Stripeが自動的に表示/非表示を切り替えています。日本のカードの番号を入力すれば郵便番号のフィールドは現れないのですが、番号が途中まで入力された状態だと、まだ国籍(?)が分からないせいなのか、郵便番号のフィールドが現れてしまいます。常に非表示にするには、Stripe Elementを生成するときに、hidePostalCode: true を指定します。

var cardElement = elements.create('card', {"hidePostalCode": true});

それ以外に指定できるオプションは、下記を参照してください。

https://stripe.com/docs/js/elements_object/create (英語)

テキストボックスを縦(というか任意の配置)にする

Stripeが生成する入力フォームは、カード番号、有効期限、CVCが一行に並ぶ配置で固定です。しかしこれらのフィールドは、それぞればらばらに生成することができます。これによって、縦や任意の配置にすることができます。confirmCardSetup() の引数には、カード番号のelementを渡すようにします。

$(function() {
    var stripe = Stripe('Stripeの公開鍵');
    var elements = stripe.elements();

    var cardNumberElement = elements.create('cardNumber');
    cardNumberElement.mount('#card-number-element');
    var cardExpiryElement = elements.create('cardExpiry');
    cardExpiryElement.mount('#card-expiry-element');
    var cardCvcElement = elements.create('cardCvc');
    cardCvcElement.mount('#card-cvc-element');

    function sendToken(event) {
        event.preventDefault();

        var clientSecret = "<%= client_secret %>";
        stripe.confirmCardSetup(
            clientSecret,
            {
                payment_method: {
                    card: cardNumberElement,
                    billing_details: {
                        name: $('#card-name').val()
                    },
                },
            },
        ).then(function(result) {
            ...
<form id='setup-form' data-secret='<%= client_secret %>'>
    <div>カード名義人: <input type='text' id='cardholder-name'></div>
    <div id='card-number-element'></div>
    <div id='card-expiry-element'></div>
    <div id='card-cvc-element'></div>
    <button id='card-button'>カード登録</button>
</form>

Stripeのサポート

https://support.stripe.com/questions/japan-faq (FAQページの右上、サインインしないと画面遷移しない)

無料のアカウントでも、サポートを使うことができるようです。電話とチャットは英語のみですが、メール(質問フォーム)は日本語で可能です。

「トピックの選択...」は、技術的な質問ならば「Payment API」を選択しておけばよさそうです。

テスト用のカード一覧

https://stripe.com/docs/testing#cards (英語)

英語が苦手な人のために、3Dセキュアのカードの一覧を載せておきます。

  • 通常
カード番号 説明
4000 0025 0000 3155 on-time paymentsで認証を要求します。しかしこのカードをset upし、その後のオフセッションでの支払いのために保存されたカードを使うなら、追加の認証は必要ありません。
4000 0027 6000 3184 どう set up されたかに関係なく、すべてのトランザクションで認証が要求されます。
4000 0082 6000 3178 on-time paymentsで認証を要求します。認証に成功するか、事前にset up していても、すべての支払いは insufficient_funds のコードで失敗します。
4000 0038 0000 0446 on-time paymentson-sessionで認証を要求します。しかしoff-session paymentでは、事前に set up されたかのように成功します。
4000 0535 6000 0011 どう set up されたかに関係なく、すべてのトランザクションで認証が要求されます。INR の支払いのみに使用できます。
カード番号 用途 説明
4000 0000 0000 3220 Required 支払いを成功させるために、3Dセキュアの2要素認証を完了しなければなりません。デフォルトでは、Raderのルールは、3Dセキュアの認証を要求します。
4000 0000 0000 3063 Required 支払いを成功させるために、3Dセキュアの認証を完了しなければなりません。デフォルトでは、Raderのルールは、3Dセキュアの認証を要求します。
4000 0084 0000 1629 Required 3Dセキュアの認証は要求されますが、認証後支払いは card_declined のコードで拒否されます。デフォルトでは、Raderのルールは、3Dセキュアの認証を要求します。
4000 0084 0000 1280 Required 3Dセキュアの認証は要求されますが、3Dセキュアを検索するリクエストは、実行中エラーで失敗します。支払いは、card_declined のコードで失敗します。 デフォルトでは、Raderのルールは、3Dセキュアの認証を要求します。
4000 0000 0000 3055 Supported 3Dセキュアの認証はまだ実行されますが、必須ではありません。デフォルトでは、Raderのルールは3Dセキュアの認証を要求しません。
4000 0000 0000 3097 Supported 3Dセキュアの認証はまだ実行されますが、必須ではありません。しかし、3Dセキュアの試行は、実行中エラーになります。デフォルトでは、Raderのルールは3Dセキュアの認証を要求しません。
4242 4242 4242 4242 Supported 3Dセキュアはサポートされますが、3Dセキュアに登録されていません。つまり、3DセキュアがRaderのルールで要求されても、顧客は追加の認証を行いません。デフォルトでは、Raderのルールは3Dセキュアの認証を要求しません。
3782 8224 6310 005 Not Supported 3Dセキュアはサポートされないし、実行もできません。支払いは、認証を行うことなく実行されます。
50
38
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
50
38