Omiseサービスを用いたWebシステムで、セキュアな機構を実現するためにしたこと

**この記事は社内ブログを外部向けに書き直したものです**

多くのWebシステムでは、クライアントのクレジットカードを取り扱うことがよくあります。ここで最も大切な事は、金銭関連のトラブルを発生しないようにすることです。
外部サービスであるOmiseを利用することにより、クレジットカード情報の漏洩を防止できますが、それでも以下のような問題が発生する可能性が残っています。

- 必要金額以上の支払い
- 払い戻しの失敗
- 必要金額以下での不正なサービス利用

ここでは、これらの発生を防止するための**アーキテクチャ的な**工夫について提案します。

自社WebシステムはMVCアーキテクチャに乗っ取っており、このうち)ControllerではRepositoryを通してデータを取得しています。もちろん、問題が発生しないためにはこれら全ての部分での精査が必要ですが、ここではこのうちModel, Controller, Repositoryにてセキュアな機構を実現するための方法について説明します。

# 二つのRepository
まず、ControllerからOmiseサービスにアクセスする時は、Omise-PHPライブラリのAPIを直接使うのではなく、OmiseSafeRepositoryとOmiseUnSafeRepositoryを使うようにします。

## OmiseSafeRepository
OmiseSafeRepositoryは、Omiseサービスに対する非破壊的なデータの操作(情報の読み取りなど)を提供します。

```php: 
class OmiseSafeRepository{
    //OmiseSafeRepositoryのメソッドの例
    function getCard($cardId){;}
    function getChargeList(){;}
    function getCustomer($customerId){;}
    ...
}
```
尚、OmiseSafeRepositoryインスタンスは`OmiseSafeRepository::getInstance()`で取得できます。

## OmiseUnSafeRepository
OmiseUnSafeRepositoryは、Omiseサービスに対する破壊的なデータの操作(課金、データの変更、アカウントの削除など)を提供します。

```php
class OmiseUnSafeRepository{
    //OmiseUnSafeRepositoryのメソッドの例
    function changePlan($customerId, $newPlanId){;}
    function charge($customerId, $price){;}
    ...
}
```

OmiseSafeRepositoryと同じくSingletonパターンを用いていますが、`getInstance()`メソッドにはアクセスできないようになっています。
OmiseUnSafeRepositoryのインスタンスを取得するには、後述するSecurityクラスを用いる必要があります。

# Securityクラス
Controllerにて、破壊的な処理を行うOmiseUnSafeRepositoryに簡単にアクセスできないようにし、また破壊的変更を加える前に各種のチェック(必要金額以上の支払いが発生していないか、不正なアクセスでないか、等)を行うことを強制させるため、以下のようにしてOmiseUnSafeRepositoryのインスタンスにアクセスできるようにします。

```php
//Controllerにて

Security::check($context, function($repository){
    //セキュリティーチェックにパスした時に行われる処理
    //ここでのみ$repository(OmiseUnSafeRepositoryのインスタンス)にアクセスできる
    ...
});
```

`$context`は現在のコンテキストを表すオブジェクトで、`Security::check`メソッドはこれを元に各種のチェックを行い、問題がなければ第二引数の無名関数を実行します。

これらの機構により、簡単に破壊的変更はできなくなったように見えます。しかし、Omise-PHPライブラリの提供するモデルは破壊的変更ができるようになっています。もし各Repositoryのメソッドの返り値がこれらのモデルだと、これらの機構を作った意味がなくなってしまいます。そこで、後述するClean Model機構を取り入れます。

# Clean Model
Omise-PHPライブラリで提供されているモデルクラスから、破壊的処理を取り除いたモデルクラスを用意します。(フィールドなどはそのままコピー)
ここでは、この破壊的処理を取り除いたモデルをClean Modelと呼びます。
各Repository内では、Omise-PHPライブラリのモデルインスタンスのフイールドを、全て
Clean Modelのフィールドのコピーし、このClean Modelをメソッドの返り値として利用するようにします。これにより、Modelを介した破壊的変更ができないようにします。

##  フィールドのコピーの手間

尚、ここでOmiseAPIのモデルの各フィールドを、いちいちClean Modelにコピーするのは手間がかかり、メンテナンスコストも上がってしまうので、OmiseObjectFillHelperトレイトを用いて、LaravelのModel::fill()のように簡単にフィールドコピーができるようにしました。

```php

function testFill(){
    $omiseObject = OmiseAccount::retrieve();

    $account = new Account();
    $account->fill($omiseObject);

    $this->assertEquals("mail@adress", $account->email);
}

class Account{
    use OmiseObjectFillHelper; //fillメソッドを自動で埋め込む

    public $id;
    public $email;
    private $created;
}
```

# よりセキュアにする為には
以上の提案は、あくまでもModel, Controller, Repositoryにおけるアーキテクチャ的な工夫にすぎず、セキュアな機構を提供するには他にも、

- Viewに対するテストなどの精査
- Omise自体の細い仕様の熟知
 
等が必要不可欠です。このうち、Omise自体の細い仕様については、例えば以下のようなものがあります。

- テスト環境、本番環境での動作の違い
- 課金許容範囲額(100 ~ 600000円)
- 最低振込額(261円以上)
- 各課金に対する払い戻し制限(テスト環境だと5回)

Omise-PHPライブラリは、こういった細かい仕様のサポートはしてくれないので、Omiseのドキュメントをよく読んでこれらに対応する処理を記述する必要があります。

# まとめ
セキュアな機構を実現するアーキテクチャ的なルール

- OmiseAPIには直接アクセスしない
- ModelはOmiseAPIで提供されているものは使わない
- 非破壊的なデータの操作(情報の読み取りなど)を行うにはOmiseSafeRepositoryを使う
- 破壊的なデータの操作(課金、データの変更、アカウントの削除など)を行うにはOmiseUnSafeRepositoryを使う
- OmiseUnSafeRepositoryを使うにはSecurityクラスを用いる
- OmiseObjectFillHelperを使うとClean Modelの実装が楽になる