GMO-PGのリンクタイプPlusでセキュアなカード決済を実現する

# 概要

[GMO-PG リンクタイプPlusで決済に利用したカード情報を「決済後会員登録」によって自動でGMOに登録する](https://qiita.com/You_name_is_YU/items/a114c015bdc61ba0ccae)の記事にあるように、
一度利用したクレジットカード情報を登録しておくことで、２回目以降の決済では、
わざわざカード情報を入力しなくても、決済処理を行うことが可能となります。

ただ、ここで問題になるのが登録済みのカード情報がある場合、**ログインユーザーが誰であっても**決済手続きが完了できてしまう。ということです。

そこで、GMO-PGでは[3Dセキュア（本人認証サービス）](https://www.gmo-pg.com/sep/customer-3dsecure/)というものを用意しています。

この記事ではこの3Dセキュアを利用していきます。

# テスト環境で動作確認するための3Dセキュア対応クレジットカード情報

3DS1.0対応のカード情報
https://faq.gmo-pg.com/service/detail.aspx?id=1681&a=102&isCrawler=1

3DS2.0対応のカード情報
https://faq.gmo-pg.com/service/detail.aspx?id=2379&a=102&isCrawler=1

# サンプルコード

```php
   // GMO決済画面へのURLを生成して返却します。
    public function get_secure_payment_url($flg_conf_member = false)
    {
        // 決済URL取得API
        $url = 'https://pt01.mul-pay.jp/payment/GetLinkplusUrlPayment.json';

        // GMOの決済で指定するオーダーIDは一意でなければいけない（注文ID先頭4桁を会員IDにした。）
        $orderId = str_pad($this->user['user_id'], 4, '0', STR_PAD_LEFT) . 'OR' . date('YmdHis');

        // json形式のパラメータを生成するための配列パラメータ定義
        $arr_param = array(
            'geturlparam'=> array(
                'ShopID'=> SHOP_ID,
                'ShopPass'=> SHOP_PASS,
                'TemplateNo'=> '1'
            ),
            'configid'=> 'test01',
            'transaction'=> array(
                'OrderID'=> $orderId,
                'Amount'=> 10000,
                'Tax'=> 1000,
                'PayMethods'=> ['credit']
            ),
            'credit'=> array(
                'JobCd'=> 'CAPTURE',
                'Method'=> '1',
                'TdFlag'=> '2', // 3Dセキュア認証を契約に従って実施
                'Tds2Type'=> '1'
            )
        );
        // 3Dセキュア認証テスト用カード
        // 3DS1.0用　＝　https://faq.gmo-pg.com/service/detail.aspx?id=1681&a=102&isCrawler=1
        // 3DS2.0用　＝　https://faq.gmo-pg.com/service/detail.aspx?id=2379&a=102&isCrawler=1

        // GMO会員IDのパラメータを設定するか判別してパラメータに追加
        $arr_member_id = array(
            'credit'=> array(
                'MemberID'=> $this->user['user_id']  // GMO会員IDの指定
            )
        );

        if ($flg_conf_member) {
            if ($this->gmo_exists_member($this->user['user_id'])) {
                $arr_param = array_merge_recursive($arr_param, $arr_member_id);
            }
        } else {
            // $flg_conf_member=falseの場合は無受験に会員IDを指定する。
            $arr_param = array_merge_recursive($arr_param, $arr_member_id);
        }

        // 配列→json変換
        $param = json_encode($arr_param);
        return $this->get_gmo_linkurl($url, $param);
    }

    // プロトコルタイプのAPIを利用し、キー型のパラメータ指定方法によるAPI実行結果から、URL情報だけを抽出して返却します。
    private function get_gmo_linkurl($url, $param) {
        // リクエストコネクションの設定
        $curl = curl_init();
        curl_setopt($curl, CURLOPT_POST, true);
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($curl, CURLOPT_HTTPHEADER, array('Content-Type: application/json; charset=utf-8'));
        curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'POST');
        curl_setopt($curl, CURLOPT_POSTFIELDS, $param);
        curl_setopt($curl, CURLOPT_URL, $url);

        // リクエスト送信
        $response = curl_exec($curl);
        $curlinfo = curl_getinfo($curl);
        curl_close($curl);

        $resJson = json_decode($response, true);

        // LinkUrlが取得できなければエラーとして扱う
        if (!array_key_exists('LinkUrl', $resJson)) {
            $http_stscd = $curlinfo['http_code'];
            
            log_message('error', print_r($response ,true));

            // GMO エラーコードが返却されていればセットする
            $gmo_errcd = '';
            parse_str($response, $data);
            if(array_key_exists('ErrCode', $data)) {
                $gmo_errcd = $data['ErrCode'];
            }

            $errmsg = <<< EOD
            $url . ' API実行が失敗しました。 : '
            'HTTPステータスコード ： ' . $http_stscd
            'GMOエラーコード ： ' . $gmo_errcd
            EOD;
            
            throw new Exception($errmsg);
        }

        // URL取得APIの実行結果からリンク情報を取得し返却
        return $resJson['LinkUrl'];
    }
```

# 動作検証

クレジットカード情報を入力する際、テスト用のカード番号を入力する。

![スクリーンショット 2020-10-06 8.06.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246556/b143712c-d64d-c215-7f43-5022f2e4f43f.png)

通常通り決済を完了する。

![スクリーンショット 2020-10-06 8.06.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246556/2d95a518-0e46-e526-cb12-2ae96185b1dc.png)

3Dセキュアの認証画面が表示され認証が求められる。

![スクリーンショット 2020-10-06 8.06.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246556/202e09a6-9e1e-c874-e4fb-da4adaa74cd1.png)

正常に認証が通ると決済が完了する。

![スクリーンショット 2020-10-06 8.06.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246556/04d4c022-5b30-5971-918e-46f0b385baaa.png)


上記は3DS1.0の場合のテスト環境での動作です。
3DS2.0の場合は、本来パスワード入力などが求められるはずなのですが、テスト環境では入力画面が表示される動きはしないようです。

![スクリーンショット 2020-10-06 8.09.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/246556/cc0845ac-740a-365f-e399-228e22dc8979.png)

ちなみに、新規でカード情報を入力した場合のみではなく、登録済みのカード情報を利用した決済の場合であっても、同じ認証画面が表示され、認証情報を入力しないと決済が完了しないように動作させることが可能です。

# 最後に

3DS1.0/3DS2.0を導入したとして、ユーザーは自身が保持しているクレジットカードの3DS用の認証情報を把握しているものだろうか。。。