payjpの3Dセキュアをphpで実装する
案外資料が転がっていなくて思わぬ苦戦を強いられたので、備忘も兼ねて書き留めておくことにします。
Node.jsとかなら公式ドキュメントにあるのでそれも参考に。
準備するもの
composer install
- pay.jp
- firebase/php-jwt
- こちらは下記のコマンド実行で入る最新バージョンで問題ない筈
$ composer require firebase/php-jwt
鍵
- pay.jpの鍵
- livemode用の秘密鍵
- sk_live_***
- livemode用の公開鍵
- pk_live_***
- testmode用の秘密鍵
- sk_test_***
- testmode用の公開鍵
- pk_test_***
- livemode用の秘密鍵
実装要綱
今回はフロント側にチェックアウトを実装し、それをformでバックエンドに控えるphpに渡してphp側で支払い処理などを実装する形とします。
そのため、pay.jpに記載されているドキュメントになぞらえると「支払い時の3Dセキュア」を導入する形になります。
フロント側
今回はsmartyを使ったのでその書き方になっている箇所があります。
<form method="post" action="credit.php">
<script
type="text/javascript"
src="https://checkout.pay.jp/"
class="payjp-button"
data-key="{$smarty.const.PJ_PK}"
data-partial="false">
</script>
<input type="hidden" name="order_id" value="">
</form>
- data-keyには公開鍵を指定します、ここでの指定方法は一例です
- data-partialにはfalseを指定します、falseでないとsubmitが走らないので
- その他注文番号や金額など必要な値があればinput hiddenに持たせておきます
バックエンド側 リダイレクト前
今回は前述のとおりバックエンド側はphpです。
支払い処理用のcredit.phpと、3Dセキュア認証後の支払い完了処理用のcredit2.phpを作成します。(名称は簡略化しています)
<?php
// ログインチェック、Payjpなど外部ライブラリの読み込み、
// 注文番号や金額の取得など必要な処理(issetでの確認なども必要に応じて実装)
// 元の画面のURLを設定
$back_url = 'https://xxx.jp';
// トークンの取得
$token = $_POST['payjp-token'];
try {
// 支払いの作成
Payjp\Payjp::setApiKey(PJ_SK);
$result = Payjp\Charge::create(array(
"card" => $token,
"amount" => $total,
"currency" => "jpy",
"capture" => false,
"expiry_days" => 60,
'three_d_secure' => true,
));
// 作成に失敗した場合は元のURLに戻す
if(!$result->id) {
header("Location:".$back_url);
exit;
}
// 支払いIDをセッションに保持しておく
$_SESSION['credit_id'] = $result->id;
// jwtを生成
$payload = array(
'url' => HTTPS_PATH . "credit2.php?order_id=" . $order_id
);
$jwt = Firebase\JWT\JWT::encode($payload, PJ_SK, 'HS256');
// 3Dセキュアの認証画面にリダイレクト
header('Location: https://api.pay.jp/v1/tds/' . $result->id . '/start?publickey=' . PJ_PK . '&back_url=' . $jwt);
exit;
// エラー
} catch(\Payjp\Error\Card $e) {
// 必要な処理
} catch (\Payjp\Error\InvalidRequest $e) {
// 必要な処理
} catch (\Payjp\Error\Authentication $e) {
// 必要な処理
} catch (\Payjp\Error\ApiConnection $e) {
// 必要な処理
} catch (\Payjp\Error\Base $e) {
// 必要な処理
} catch (Exception $e) {
// 必要な処理
}
?>
- 公式ドキュメントはこちら
-
Payjp\Payjp::setApiKey
に鍵情報を渡すことで鍵を設定できます -
Payjp\Charge::create
で支払い情報を作成できます-
token
にはチェックアウトで作成されたトークンを設定します($_POSTで取得) -
amount
には金額が入ります - 日本円の場合
currency
はjpyで固定です -
three_d_secure
が今回の肝です、trueにしておかないと3Dセキュアの認証画面に飛びません
-
- 作成結果からidを取得することで支払いIDが取得できます
- credit2.phpで使うためセッションに保持しておきます
- 3Dセキュアの認証画面から戻ってくるURLは
back_url
パラメータで指定しますが、jwtの形式でフォーマットしてあげる必要があります- keyとして
url
を定義し、valueにURLを設定します- このとき日本語が含まれる場合はURLエンコードしてあげる必要があります
- 外部ライブラリのJWTを使用し、encodeの引数として、URL、秘密鍵、HS256を指定します
- keyとして
- header関数で指定のURLに飛ばすことで3Dセキュア認証画面に遷移させます
- エラーハンドリングは公式のAPIリファレンスも適宜参照してください
3Dセキュア
3Dセキュアの画面ではクレジットカードに紐づくメールアドレスにワンタイムトークンが送られるなどされますが、
秘密鍵および公開鍵においてtestのものを使用することで、テストモードで動作を確認することができます。
payjp側が、テストカードを用意していますのでこれを有難く使わせてもらいます。
テスト用の鍵を設定した状態で支払いを試行すると、
デバッグモードっぽい3Dセキュア画面が現れます。
ラジオボタンの動作は下記の通りです。
- 認証成功
- 認証が成功した場合の挙動
- アテンプト(attempted)
- 3Dセキュアをカード側が未設定の場合の挙動
- 完全認証かどうかを判定する場合に使う
- 認証失敗
- 異なる値を入力した場合の挙動
- エラー
- ほかの何らかの原因でエラーとなった場合の挙動
この値は three_d_secure_status
に保持されますので、完全認証を確認する場合は3Dセキュアからリダイレクトするphpにおいてこれを確認する処理を挟みます。
バックエンド側 リダイレクト後
リダイレクト前のphpでback_urlに指定したURLが呼び出されます。
<?php
// ログインチェック、セッションに保持したIDから注文データを取得するなど必要な処理を実施しておく
try {
Payjp\Payjp::setApiKey(PJ_SK);
$charge = Payjp\Charge::retrieve($_SESSION['credit_id']);
if ($charge->three_d_secure_status === 'attempted') {
// 認証が完全でない場合は決済を完了させずに戻す
header("Location:".$back_url."&back=1&error=2");
exit;
}
$charge->tdsFinish(); // 処理完了
} catch ...(割愛) {
}
// 注文者・管理者などにメールを送信する、DBの値を更新しておく、など必要な処理を実施する
header("Location:".$back_url);
exit;
?>
- 公式ドキュメントはこちら
-
Payjp\Charge::retrieve
で支払い状態を確認できます- 引数にはセッションに格納しておいた支払いIDを使います
- 完全認証かどうかを確認する必要がある場合は
three_d_secure_status
の値を確認します
-
tdsFinish()
を実行することで一連の3Dセキュアおよび支払い処理が完了します- 3Dセキュアで認証失敗或いはエラーがあった場合は例外が発生します
- これが実行されない限りは支払いが確定しません
- 処理完了後にメール送信処理などがあればそれを実行し、header関数でサンキューページや元の画面に遷移させるなどして完了です
これで一連の3Dセキュア導入対応は終わりです。
公式ドキュメントにあるコードサンプル等も適宜参照してください。