💳

fincode API を使ってみた

2024/09/16に公開

これは何?

fincode APIを使ってアプリケーションを開発する機会があったので、

  • どのようにコードに組み込んだのか
  • 使った感想

をまとめておく。

そもそも fincode って何?

fincodeとは、GMOが提供するオンライン決済サービスです

と言っても色々な機能があり全てに言及するのは面倒なので、開発者目線でざっくりまとめると、
「決済やカード情報保存と言ったデリケートで面倒臭い実装をサービスとして提供してくれている」
という感じ。
GUIで操作できるテスト環境も提供してくれるので、APIの動作確認も割とお手軽にできる。

詳しくは公式ドキュメントを見てね。

どういう時に使うの?

「Web/モバイルアプリケーションでユーザーに決済の機能を提供したいが、大変すぎるので作りたくない・・」
という需要が9割じゃないだろうか。というか今回がそれだった。

どんな風に使うの?

  • 公式チュートリアルがあるので、テスト環境の新規ユーザ登録の部分や、実際にAPIを動かすといった部分は記述しない。
  • fincode APIのソフトウェアアーキテクチャへの組み込み、APIキーの取り扱いについて記述する。
  • 言語は Go 1.22.1 を使用する。

ソフトウェアアーキテクチャへの組み込み

今回はざっくりレイヤードアーキテクチャを採用したので、それに fincode API をどうやって組み込んだのかを記述する。
※レイヤードアーキテクチャの原典を完全に理解している訳ではないです。

最初に、今回開発したアプリケーションのディレクトリ構造を掲載する。(一部省略)

- app
 - controller
 - domain
 - usecase
 - infra
 ...
 ...

おそらくどこかで一度は見たことあるようなありふれた構造のはず。

今回は domain に interface を定義して、実装は infra に置くという構造にした。

- app
 - domain
  - gateway (fincode の interface 置き場)
 ...
 - infra
  - fincode (fincode の実装置き場)
 ...
 ...

DB 接続を行うとき、 domain に repository interface を定義して、repository の実装は infra に置くという構造にすることが多いと思うが、今回はその構造に習った。
fincode API も DB も、アプリケーションから見て外部接続であり、データアクセスと同じように扱うのが非常にしっくり来たからである。

例えば「決済を行う」処理であれば、以下のような形になる。

domain/gateway/payment.go
type IPaymentGateway interface {
	PayWithCard() (response, error)
}
infra/fincode/payment.go
type PaymentGateway struct {}

func (g *PaymentGateway) PayWithCard() (response, error) {}
usecase/payment.go
type PaymentUsecase struct {
	paymentGateway     gateway.IPaymentGateway
}

func (u *PaymentUsecase) Exec() (response, error) {
 ...
 ...
 response, err := u.paymentGateway.PayWithCard()
 if err != nil {
    return nil, err
 }
 ...
 ...
}

API キーの取り扱い

fincode API では認証のためにヘッダーに API キーをセットする必要がある。

この API キーは fincode に登録されたプラットフォームごとに発行されている。プラットフォーム A で決済を行う場合には、fincode 側で発行されたプラットフォーム A のシークレット API キーをヘッダーにセットして、決済 API を実行する。
この API キーが外部に漏れると、決済などのクリティカルなアクションが意図せず実行されてしまう恐れがあるので、慎重に扱う必要がある。

今回のアプリケーションでは、数十〜数百のプラットフォームを扱う必要があったが、
API キーを全てAWS Secrets Manager に格納しておくという方法だと料金が高くなってしまう。そのため API キーを AES で暗号化・復号化し、その共通鍵を1つだけ AWS Secrets Manager に格納することにした。
プラットフォーム新規登録時には API キーを共通鍵で暗号化してからDBに保存し、
fincode API の実行時にはDB から暗号化済みの API キーを読み出し、共通鍵で復号してヘッダーにセットする。

暗号化・復号化の手段として、 crypt/cipher の CFBモードを使用した。CBCと違って平文のパディングが不要なため、多少実装が楽できる。

暗号化・復号化の実装は Example をほとんどそのまま流用し、以下のような形になった。(デリケートな処理なので、念のため一部を書き換えています)

encrypt.go
func encrypt() []byte {
	plaintext := []byte(os.Getenv("API_SECRET_KEY"))
	keyText := os.Getenv("KEY_TEXT")

	block, err := aes.NewCipher([]byte(keyText))
	if err != nil {
		panic(err)
	}

	ciphertext := make([]byte, aes.BlockSize+len(plaintext))
	iv := ciphertext[:aes.BlockSize]
	if _, err := io.ReadFull(rand.Reader, iv); err != nil {
		panic(err)
	}

	stream := cipher.NewCFBEncrypter(block, iv)
	stream.XORKeyStream(ciphertext[aes.BlockSize:], plaintext)

	return ciphertext
}

domain/platform.go
type Platform struct {
    ...
    secretKey  []byte
    ...
}

func (p *Platform)  Decrypt() string {
	block, err := aes.NewCipher([]byte(os.Getenv("KEY_TEXT")))
	if err != nil {
		return ""
	}

	iv := p.secretKey[:aes.BlockSize]
	secretKey := p.secretKey[aes.BlockSize:]
	decrypted := make([]byte, len(secretKey))

	stream := cipher.NewCFBDecrypter(block, iv)
	stream.XORKeyStream(decrypted, secretKey)

	return string(decrypted)
}

今回は domain で定義した構造体をあらゆる層で使い回しているので、fincode API を呼び出すときには infra まで引き回した platform 構造体の Decrypt() をコールし、暗号化された SecretKey を復号化する。

infra/fincode/payment.go
type PaymentGateway struct {}

func (g *PaymentGateway) PayWithCard() (response, error) {
  ...
  request.Header.Set("Authorization", "Bearer "+platform.Decrypt())
  ...
}

感想

ドキュメントが整備されており、テスト環境も提供されているので開発は割と快適だった。
また、シークレット API キーの取り扱いに対処した際に、AES についての知識が得られたのは良かった。
Web アプリケーションで0から決済の仕組みを作るのはとても困難なので、今後も決済機能を組み込んだアプリケーションを作る場合にはお世話になるかもしれない。そう思うと、ここで fincode に触れられたのは貴重な経験になりそう。

Discussion