記事概要
Ruby on RailsにPAY.JPを実装する方法について、まとめる
前提
- Macを使用している
- Ruby on Railsでアプリケーションを作成している
- ライブラリ「pry-rails」をインストール済みである
- PAY.JPのアカウントを作成済みである
- PAY.JPが提供する「テストモード」を使用する
サンプルアプリ(GitHub)
PAY.JPとは
クレジットカード決済代行サービスの一つであり、多くの決済代行サービスの中でも、シンプルなAPIで開発をしやすいことが特徴。
処理の仕組み
PAY.JPとの間に発生する様々な決済情報の送受信は、PAY.JPがオープンAPIとして提供している仕組みを利用する。そのため、アプリケーション側で特別な対策をしなくても、セキュリティ的に担保された処理を行うことができる
処理イメージ
トークンは一度だけ有効となるものなので、同じトークンで別の決済を行うことはできない。
開発環境
PAY.JPが提供する「テストモード」を使用すると、実際のクレジットカード請求や、手数料の発生はない
payjp.js
クライアントサイドからPAY.JPのAPIを使用するために読み込むJavaScriptのライブラリ。この中に、安全にクレジットカード情報をトークン化するための処理などが含まれている
手順①(Turbo機能をオフ)
PAY.JPが存在するページに遷移するリンクに対して、data: { turbo: false }
を追記
<%= link_to "購入画面に進む", data: { turbo: false },class:"item-red-btn"%>
手順②(クレジットカード情報のトークン化)
-
クライアントサイドでPAY.JPのAPIを使用するために必要なJavaScriptを読み込むため、
app/views/layouts/application.html.erb
のhead要素内に、payjp.jsのライブラリを記述する<!DOCTYPE html> <html> <head> <!-- 中略 --> <!-- payjp.jsのライブラリを追加 --> <script type="text/javascript" src="https://js.pay.jp/v2/pay.js"></script> <!-- 中略 --> </head> <body> <%= yield %> </body> </html>
-
app/javascript
配下に、実際にトークン化を行うJavaScriptファイルを作成する -
上記で作成したファイルを読み込むための設定を行う
config/importmap.rb# 最終行に追加 pin "card", to: "card.js"
app/javascript/application.js// 最終行に追加 import "card"
-
JavaScriptファイルが正しく読み込まれることを確認するため、下記を記述する
card.jsconst pay = () => { console.log("カード情報トークン化のためのJavaScript"); }; window.addEventListener('turbo:load', pay); window.addEventListener("turbo:render", pay);
-
フォーム送信時にイベントが発火するように、下記を記述する
card.jsconst pay = () => { const form = document.getElementById('charge-form') form.addEventListener("submit", (e) => { console.log("フォーム送信時にイベント発火") e.preventDefault(); }); }; window.addEventListener('turbo:load', pay); window.addEventListener("turbo:render", pay);
-
PAY.JPにログイン後、ページの左側の「API」をクリックすると公開鍵を確認できる。「テスト公開鍵」をコピーし、公開鍵を取得する
-
公開鍵の情報を、JavaScriptのファイルに設定する
※鍵情報を公開しない。このコードの状態でGitHubにPushするなどしてしまうと、コードに含まれる鍵情報が公開されてしまい、不正請求などの被害にあうリスクがあるcard.jsconst pay = () => { // PAY.JPテスト公開鍵 const payjp = Payjp('pk_test_***********************') const form = document.getElementById('charge-form') form.addEventListener("submit", (e) => { console.log("フォーム送信時にイベント発火") e.preventDefault(); }); }; window.addEventListener('turbo:load', pay); window.addEventListener("turbo:render", pay);
-
フォームに、クレジットカード情報の入力項目を作成する
<%= form_with model: @order, id: 'charge-form', class: 'card-form',local: true do |f| %> <%= render 'layouts/error_messages', model: @order %> <div class='form-wrap'> <%= f.label :price, "金額" %> <%= f.text_field :price, class:"price", placeholder:"例)2000" %> </div> <!-- クレジットカード情報入力欄 --> <div class='form-wrap'> <p>カード番号</p> <div id="number-form" class="input-default"></div> </div> <div class='form-wrap'> <p>期限</p> <div id="expiry-form" class="input-default"></div> </div> <div class='form-wrap'> <p>カード背面4桁もしくは3桁の番号</p> <div id="cvc-form" class="input-default"></div> </div> <%= f.submit "購入", class:"button", id: "button" %> <% end %>
-
フォームに、クレジットカード情報の入力フォームを作成する
card.jsconst pay = () => { // PAY.JPテスト公開鍵 const payjp = Payjp('pk_test_***********************') // elementsインスタンスを作成 const elements = payjp.elements(); // クレジットカードの入力フォーム作成 const numberElement = elements.create('cardNumber'); const expiryElement = elements.create('cardExpiry'); const cvcElement = elements.create('cardCvc'); // id属性で指定した要素とelementインスタンスが情報を持つフォームとを置き換える numberElement.mount('#number-form'); expiryElement.mount('#expiry-form'); cvcElement.mount('#cvc-form'); const form = document.getElementById('charge-form') form.addEventListener("submit", (e) => { console.log("フォーム送信時にイベント発火") e.preventDefault(); }); }; window.addEventListener('turbo:load', pay); window.addEventListener("turbo:render", pay);
elements.create()
で指定できるタイプは下記。指定可能なtype 説明 card カード番号入力欄、有効期限入力欄、CVC入力欄の順に横に並んだフォーム cardNumber カード番号入力欄 cardExpiry 有効期限入力欄 cardCvc CVC入力欄 -
フォームに入力されたカードの情報を取得してトークン化するため、下記を記述する
card.jsconst pay = () => { // PAY.JPテスト公開鍵 const payjp = Payjp('pk_test_***********************') // elementsインスタンスを作成 const elements = payjp.elements(); // クレジットカードの入力フォーム作成 const numberElement = elements.create('cardNumber'); const expiryElement = elements.create('cardExpiry'); const cvcElement = elements.create('cardCvc'); // id属性で指定した要素とelementインスタンスが情報を持つフォームとを置き換える numberElement.mount('#number-form'); expiryElement.mount('#expiry-form'); cvcElement.mount('#cvc-form'); const form = document.getElementById('charge-form') form.addEventListener("submit", (e) => { // カード情報のトークンを取得する payjp.createToken(numberElement).then(function (response) { if (response.error) { } else { const token = response.id; console.log(token) } }); e.preventDefault(); }); }; window.addEventListener('turbo:load', pay); window.addEventListener("turbo:render", pay);
-
テストカードの情報を使用し、クレジットカード情報送信時にトークンが生成されることを、ブラウザで確認する
テストカードの情報は下記。詳細は、こちらを参照
項目 値 テストカード番号 4242424242424242(16桁) CVC 123 有効期限 登録時より未来
手順③(サーバーサイドへのトークン送付)
- トークンの値をフォームに含めるため、下記を記述する
card.js
const pay = () => { // PAY.JPテスト公開鍵 const payjp = Payjp('pk_test_***********************') // elementsインスタンスを作成 const elements = payjp.elements(); // クレジットカードの入力フォーム作成 const numberElement = elements.create('cardNumber'); const expiryElement = elements.create('cardExpiry'); const cvcElement = elements.create('cardCvc'); // id属性で指定した要素とelementインスタンスが情報を持つフォームとを置き換える numberElement.mount('#number-form'); expiryElement.mount('#expiry-form'); cvcElement.mount('#cvc-form'); const form = document.getElementById('charge-form') form.addEventListener("submit", (e) => { // カード情報のトークンを取得する payjp.createToken(numberElement).then(function (response) { if (response.error) { } else { const token = response.id; // トークンの値をHTMLのinput要素に埋め込み、フォームに追加する const renderDom = document.getElementById("charge-form"); const tokenObj = `<input value=${token} name='token'>`; renderDom.insertAdjacentHTML("beforeend", tokenObj); debugger; //ここで停止 } }); e.preventDefault(); }); }; window.addEventListener('turbo:load', pay); window.addEventListener("turbo:render", pay);
- ブラウザ上にトークンの値が現れ、フォームの中に追加されたことを確認する
- トークンの値を非表示にするため、下記を記述する
card.js
const pay = () => { // PAY.JPテスト公開鍵 const payjp = Payjp('pk_test_***********************') // elementsインスタンスを作成 const elements = payjp.elements(); // クレジットカードの入力フォーム作成 const numberElement = elements.create('cardNumber'); const expiryElement = elements.create('cardExpiry'); const cvcElement = elements.create('cardCvc'); // id属性で指定した要素とelementインスタンスが情報を持つフォームとを置き換える numberElement.mount('#number-form'); expiryElement.mount('#expiry-form'); cvcElement.mount('#cvc-form'); const form = document.getElementById('charge-form') form.addEventListener("submit", (e) => { // カード情報のトークンを取得する payjp.createToken(numberElement).then(function (response) { if (response.error) { } else { const token = response.id; // トークンの値をHTMLのinput要素に埋め込み、フォームに追加する const renderDom = document.getElementById("charge-form"); // トークンの値を非表示にする const tokenObj = `<input value=${token} name='token' type="hidden">`; renderDom.insertAdjacentHTML("beforeend", tokenObj); } }); e.preventDefault(); }); }; window.addEventListener('turbo:load', pay); window.addEventListener("turbo:render", pay);
- ブラウザにはトークンの値が表示されず、フォームにはトークンの値が追加されていることを確認する
- フォームに存在するクレジットカードの各情報を削除するため、下記を記述する
card.js
const pay = () => { // PAY.JPテスト公開鍵 const payjp = Payjp('pk_test_***********************') // elementsインスタンスを作成 const elements = payjp.elements(); // クレジットカードの入力フォーム作成 const numberElement = elements.create('cardNumber'); const expiryElement = elements.create('cardExpiry'); const cvcElement = elements.create('cardCvc'); // id属性で指定した要素とelementインスタンスが情報を持つフォームとを置き換える numberElement.mount('#number-form'); expiryElement.mount('#expiry-form'); cvcElement.mount('#cvc-form'); const form = document.getElementById('charge-form') form.addEventListener("submit", (e) => { // カード情報のトークンを取得する payjp.createToken(numberElement).then(function (response) { if (response.error) { } else { const token = response.id; // トークンの値をHTMLのinput要素に埋め込み、フォームに追加する const renderDom = document.getElementById("charge-form"); // トークンの値を非表示にする const tokenObj = `<input value=${token} name='token' type="hidden">`; renderDom.insertAdjacentHTML("beforeend", tokenObj); } // フォームに存在するクレジットカードの各情報を削除する numberElement.clear(); expiryElement.clear(); cvcElement.clear(); }); e.preventDefault(); }); }; window.addEventListener('turbo:load', pay); window.addEventListener("turbo:render", pay);
- サーバーサイドへフォームの情報を送信するため、下記を記述する
card.js
const pay = () => { // PAY.JPテスト公開鍵 const payjp = Payjp('pk_test_***********************') // elementsインスタンスを作成 const elements = payjp.elements(); // クレジットカードの入力フォーム作成 const numberElement = elements.create('cardNumber'); const expiryElement = elements.create('cardExpiry'); const cvcElement = elements.create('cardCvc'); // id属性で指定した要素とelementインスタンスが情報を持つフォームとを置き換える numberElement.mount('#number-form'); expiryElement.mount('#expiry-form'); cvcElement.mount('#cvc-form'); const form = document.getElementById('charge-form') form.addEventListener("submit", (e) => { // カード情報のトークンを取得する payjp.createToken(numberElement).then(function (response) { if (response.error) { } else { const token = response.id; // トークンの値をHTMLのinput要素に埋め込み、フォームに追加する const renderDom = document.getElementById("charge-form"); // トークンの値を非表示にする const tokenObj = `<input value=${token} name='token' type="hidden">`; renderDom.insertAdjacentHTML("beforeend", tokenObj); } // フォームに存在するクレジットカードの各情報を削除する numberElement.clear(); expiryElement.clear(); cvcElement.clear(); // サーバーサイドへフォームの情報を送信する document.getElementById("charge-form").submit(); }); // 通常のRuby on Railsにおけるフォーム送信処理はキャンセル e.preventDefault(); }); }; window.addEventListener('turbo:load', pay); window.addEventListener("turbo:render", pay);
手順④(決済機能のサーバーサイド実装)
- paramsを確認するため、
binding.pry
を追記するorders_controller.rbclass OrdersController < ApplicationController def index @order = Order.new end def create binding.pry # ここで処理停止 @order = Order.new(order_params) if @order.valid? @order.save return redirect_to root_path else render 'index', status: :unprocessable_entity end end private def order_params params.require(:order).permit(:price) end end
- ブラウザでフォームの送信を行い、paramsの中身を確認する
[1] pry(#<OrdersController>)> params => #<ActionController::Parameters {"authenticity_token"=>"M8O8hcgWQ4zJBLuh9OWEs0ZMXrMjUC9BW9Irmq2JcGr2RVr7wPxbsfeEZQOCRTM05keVG60y_-tWcfYLspEVSA", "order"=>{"price"=>"2000"}, "token"=>"tok_867ae3a9e4d8280018fcdd3a849a", "controller"=>"orders", "action"=>"create"} permitted: false> # priceの情報はorderというキーのバリューとして、ハッシュ形式で含まれている [2] pry(#<OrdersController>)> order_params[:price] => "2000" # tokenの情報はorderというキーのバリューとしては含まれていない [3] pry(#<OrdersController>)> order_params[:token] => nil
- mergeメソッドを用いてtokenの値を結合するため、下記を記述する
orders_controller.rb
# 中略 private def order_params # ストロングパラメーターに、tokenの値を結合 params.require(:order).permit(:price).merge(token: params[:token]) end # 中略
- ブラウザでフォームの再送信を行い、paramsの中身を確認する
[1] pry(#<OrdersController>)> params => #<ActionController::Parameters {"authenticity_token"=>"cDAcCU6XcG05HwlMQ3-fNv1wsiMCGWXdKYzIw_1TmT-1tvp3Rn1oUAef1-413yixXXt5i4x7tXckLxVS4kv8HQ", "order"=>{"price"=>"1500"}, "token"=>"tok_6d460ccc66a0708edfcc71b0ddaf", "controller"=>"orders", "action"=>"create"} permitted: false> # priceの情報はorderというキーのバリューとして、ハッシュ形式で含まれている [2] pry(#<OrdersController>)> order_params[:price] => "1500" # tokenの情報もorderというキーのバリューとして、ハッシュ形式で含まれている [3] pry(#<OrdersController>)> order_params[:token] => "tok_6d460ccc66a0708edfcc71b0ddaf"
-
binding.pry
の記述を削除する - 「Orderモデル(Orderクラス)」にtokenという属性が存在しないため、エラーが発生する
- Orderモデルでtokenの値を取り扱えるようにする
order.rb
class Order < ApplicationRecord attr_accessor :token # tokenの値をモデルで取り扱えるようにする validates :price, presence: true, numericality: {only_integer: true} end
- Gemの
payjp
を導入する
導入方法は、こちらを参照 - PAY.JPにログイン後、ページの左側の「API」をクリックすると秘密鍵を確認できる。「テスト秘密鍵」をコピーし、秘密鍵を取得する
- 秘密鍵の情報を、コントローラーのファイルに設定する
※鍵情報を公開しない。このコードの状態でGitHubにPushするなどしてしまうと、コードに含まれる鍵情報が公開されてしまい、不正請求などの被害にあうリスクがあるorders_controller.rb# 中略 def create @order = Order.new(order_params) if @order.valid? # バリデーションを正常に通過した時のみに、決済処理を実行 # PAY.JPテスト秘密鍵 Payjp.api_key = "sk_test_***********" Payjp::Charge.create( amount: order_params[:price], # 決済金額 card: order_params[:token], # トークン化されたカード情報 currency: 'jpy' # 通貨の種類(日本円) ) @order.save return redirect_to root_path else render 'index', status: :unprocessable_entity end end # 中略
- サーバーを再起動する
- ブラウザでフォームの再送信を行い、下記を確認する
- 金額がordersテーブルに保存されるか
- PAY.JP上で決済の記録が残るか
PAY.JPにログインし、「売上」の項目を選択することで閲覧可能
- リファクタリングする
orders_controller.rb
class OrdersController < ApplicationController def index @order = Order.new end def create @order = Order.new(order_params) if @order.valid? # バリデーションを正常に通過した時のみに、決済処理を実行 pay_item @order.save return redirect_to root_path else render 'index', status: :unprocessable_entity end end private def order_params # ストロングパラメーターに、tokenの値を結合 params.require(:order).permit(:price).merge(token: params[:token]) end # メソッドに決済処理を切り出す def pay_item # PAY.JPテスト秘密鍵 Payjp.api_key = "sk_test_***********" Payjp::Charge.create( amount: order_params[:price], # 決済金額 card: order_params[:token], # トークン化されたカード情報 currency: 'jpy' # 通貨の種類(日本円) ) end end
- ブラウザでフォームの再送信を行い、下記を確認する
- 金額がordersテーブルに保存されるか
- PAY.JP上で決済の記録が残るか
PAY.JPにログインし、「売上」の項目を選択することで閲覧可能
- tokenが空では保存できないというバリデーションを設定するため、下記を記述する
本来token
をOrderモデルのバリデーションとして記載できないが、attr_accessor :token
を記述したことで、バリデーションを設定できるorder.rbclass Order < ApplicationRecord attr_accessor :token # tokenの値をモデルで取り扱えるようにする validates :price, presence: true, numericality: {only_integer: true} validates :token, presence: true # tokenが空では保存できない end
- クレジット情報が不足している状態で、ブラウザのフォーム送信を行い、正常にエラーが表示されることを確認する
ここまで実装してきた機能のイメージ
手順⑤(環境変数の設定)
APIの鍵情報は外部に流出しないように工夫する必要がある
開発環境
鍵情報は環境変数として設定し、自身のPCだけに保持する
- ターミナルで以下のコマンドを実行し、環境変数を設定する
※現在の.zshrcの内容を削除せず、末尾に上記の設定を追記。既存の設定を削除してしまうとPCが正常に動作しなくなる危険性がある# zshrcファイルを開く % vim ~/.zshrc # iを押してインサートモードに移行し、下記を追記する。 export PAYJP_SECRET_KEY='sk_test_*************' export PAYJP_PUBLIC_KEY='pk_test_*************' # 編集が終わったらescキーを押してから:wqと入力して保存して終了
- 下記コマンドを実行し、設定を反映する
% source ~/.zshrc
- 下記コマンドで、コンソールを起動する
% rails c
- コンソールで、環境変数が設定されていることを確認する
[1] pry(main)> ENV["PAYJP_SECRET_KEY"] => "sk_test_*************" [2] pry(main)> ENV["PAYJP_PUBLIC_KEY"] => "pk_test_*************"
- サーバーサイドで環境変数を読み込むため、下記のように編集する
orders_controller.rb
# 中略 def pay_item # PAY.JPテスト秘密鍵 Payjp.api_key = ENV["PAYJP_SECRET_KEY"] # 直接秘密鍵を指定していたところを、環境変数に置き換え Payjp::Charge.create( amount: order_params[:price], # 決済金額 card: order_params[:token], # トークン化されたカード情報 currency: 'jpy' # 通貨の種類(日本円) ) end # 中略
- 「gon」というGemを導入する
導入方法は、こちらを参照
※ブラウザの開発者ツールで内容を見ることができてしまうため、本来は秘密情報を扱うためのものではない。公開鍵は厳密には秘匿する必要がないため、今回はgonを利用する - ビューで「gon」を使用できるようにするため、下記を追記する
※
<!-- gonを使用するために追記 --> <%= include_gon %> <!-- 省略 -->
include_gon
は、必ず表示しようとするビューに記述する必要がある。ビューが1つしかない場合は問題にならないが、複数のビューで使用する場合は全ビューに記述を行うか、application.html.erb
に記述する必要がある - RailsからJavaScriptへ公開鍵を渡すため、下記を記述する
orders_controller.rb
# 中略 def index gon.public_key = ENV["PAYJP_PUBLIC_KEY"] # RailsからJavaScriptへ公開鍵を渡す @order = Order.new end # 中略
gon.(変数名) = (代入する値)
と記述することで、JavaScriptで変数を使用可能 - JavaScriptで公開鍵を読み込む
card.js
const pay = () => { // gon.public_keyを変数publicKeyに代入 const publicKey = gon.public_key // PAY.JPテスト公開鍵 const payjp = Payjp(publicKey) // 中略
- サーバーを再起動する
- ブラウザで問題なく作動することを確認する
本番環境(Render)
はじめてデプロイするアプリに環境変数を設定
こちらを参照
デプロイ済みのアプリに環境変数を設定
- Renderにサインインし、ダッシュボードを開く
- 環境変数を設定したいアプリをクリックし、詳細ページを開く
- サイドバーにある「Environment」をクリックし、環境変数の設定欄を表示する
-
Add Environment Variables
をクリックし、下記2つを追加する- PAYJP_PUBLIC_KEY:公開鍵
- PAYJP_SECRET_KEY:秘密鍵
-
Value
に各値を入力する -
Save Changes
をクリックし、保存する - サーバーを再起動する
手順⑥(不備があった場合の処理を修正)
現状
修正手順
- render時にgonを使用するための設定を行う
orders_controller.rb
# 中略 def create @order = Order.new(order_params) if @order.valid? # バリデーションを正常に通過した時のみに、決済処理を実行 pay_item @order.save return redirect_to root_path else gon.public_key = ENV["PAYJP_PUBLIC_KEY"] # render時の環境変数読み込み render 'index', status: :unprocessable_entity end end # 中略
- renderが実行された場合でもJavaScriptのコードが実行されるように記述する必要があるが、記述済み。
card.js
// 省略 window.addEventListener("turbo:render", pay);
修正後
Ruby on Railsまとめ