まだプログラマーですが何か?

プログラマーネタとアスリートネタ中心。たまに作成したウェブサービス関連の話も http://twitter.com/dotnsf

タグ:express

Tips 的な小ネタです。

Node.js + Express によるウェブアプリケーションコードの中で、何らかの URL への GET リクエスト(POST とかでもいいですが、処理内容は GET の時と同じなので GET で考えることにします)を受けて処理している時の、アクセス時のフル URL をサーバー側で知る方法です。 なお、ここでの「フル URL 」とは、プロトコル+ホスト名+(デフォルトと異なる場合は)ポート番号+アクセスパス+実行時のURLパラメータ のこととします。

この値はクライアント側の JavaScript を使えば windows.location オブジェクトを参照することで取得できます。ただこちらはあまり意味がないというか、アクセスしたユーザーは自分のブラウザのアドレス欄を見れば URL を確認できるのでわざわざ別途必要になるケースが珍しいはずです。このクライアントサイドでの話ではなく、サーバーサイドの処理内で知る方法、という意味です。

結論としては以下のようなコードで取得することが可能です:
//.  app.js
var express = require( 'express' ),
    app = express();

app.get( '/*', function( req, res ){
  res.contentType( 'text/plain; charset=utf-8' );
  var url = req.protocol + '://' + req.get( 'host' ) + req.originalUrl;
  res.write( url );
  res.end();
});

var port = process.env.PORT || 8080;
app.listen( port );
console.log( "server starting on " + port + " ..." );

まずルーティングのパス定義部分を '/*' としています。これによってルートパス以下のすべてのパスへの GET リクエストをこのハンドラが受け持つ、と宣言します。

肝心のフル URL ですが、このハンドラ実行時のパラメーター: req(リクエストオブジェクト)を使って、以下のように求めることができます:
 var url = req.protocol + '://' + req.get( 'host' ) + req.originalUrl;

req.protocol にはプロトコル("http" または "https")、req.get( 'host' ) でポート番号まで含めたアクセス時のホスト名、そして req.originalUrl にはアクセス時のフルパスが URL パラメータまで含めた形で取得できます。これらをつなぎ合わせることでアクセス時のフル URL が取得できるので、これをレスポンスで返す、という処理をしています。

試しに実行していくつかの URL パターンでアクセスしてみた所、以下のようにいずれも期待通りの結果になりました:
(http://localhost:8080/)
2021062301


(http://localhost:8080/abc/hello?x=100)
2021062302


(http://localhost:8080/abc/hello?x=100&y=200)
2021062303


1つの環境で複数のサーバー名を持って稼働するサーバーの場合に、「何というホスト名でアクセスされているのか」をサーバー側からも知ることができる、という情報でした。

なおこのアプリケーションのソースコードはこちらで公開しています:
https://github.com/dotnsf/access_url


どれだけ需要があるかわかりませんが、docker イメージ(dotnsf/access-url)の形で以下からも公開しています:
https://hub.docker.com/r/dotnsf/access_url


利用可能な Kubernetes クラスタ環境(と接続設定などが済んだ kubectl コマンド)があれば、以下の手順でコマンドを実行することで Deployment と(spec.type = "NodePort" の) Service を作成できます:
$ git clone https://github.com/dotnsf/access_url

$ cd access_url

$ kubectl -f yaml/app_deployment.yaml

IBM Cloud の無料版 IKS(IBM Kubernetes Services) で、上記コマンドを実行して作成したアプリケーションにアクセスした時の様子が以下になります。アクセスした URL が正しくサーバーサイドで取得できている様子が確認できます:
2021062401

 
最後に余談を。上述のクライアントサイド JavaScript による(window.location オブジェクトを用いた)取得方法との取得できる情報の違いについて補足します。

クライアントサイドで取得する場合、サーバーサイドで取得できない情報が1つ取得できます。それが「ハッシュ」と呼ばれる情報で例えば、
 http://xxx.xxx.xxx.xxx/abc/hello?x=1&y=2#here
という URL アドレスの最後の "#here" 部分の情報です。

この情報はクライアントサイドであれば window.location.search を参照することで取得することが可能ですが、サーバーサイドでは取得する方法がありません。ただハッシュ情報はクライアントサイドで(HTML 内の特定位置を参照するなど)利用するためのものであって、サーバーサイドで生成する情報としてはハッシュによる差異はありません。要はサーバーサイドでは意味のない情報であるためサーバー側では取得できなくなっている、ものと思われます。



まず、今回紹介するのは Node.js + Express で作った API を CORS 対応にする、という、これ自体はシンプルな内容なのですが、この話を考えるに至った経緯を最初にまとめておきます。


【背景】
普段からウェブアプリケーション・サービスを開発しています。詳しいアーキテクチャはともかく、ユーザーのアクセス先となるフロントエンドのアプリケーション・サーバーとバックエンド(データベースなど)があって、クラウドっぽくバックエンドには API サーバー経由でアクセスする、という形態を多く採用しています。

この形態を採用している時に限らない話ですが、「サービスの安定運用」を考えると「いかにフロントエンドを安定させるか」を考慮する必要があります。ネットワークやバックエンド含めたサービス全体のどこかに不具合が生じた場合であっても、ユーザーが最初にアクセスするフロントエンドが動いていれば「画面に障害発生メッセージを出す」ことができるようになります。逆にフロントエンドにアクセス過多を含めた不具合が発生してしまうと、「メッセージで利用者に不具合が発生していることを知らせる」ことすらもできなくなってしまいます。

このフロントエンドサーバーを安定稼働させるための技術として、最近は(docker などの)コンテナ技術であったり、(k8s や OpenShift などの)コンテナのオーケストレーション技術が流行っています。ここまでは「いわゆる一般論」的な話です。

一般論を理解した所で、技術者としての「一般論ではない話」も考えます。自分は業務でもプログラミングや作ったりサービスの運用を行ったりしていますが、業務外でも(つまり個人でも)プログラミングしたり、作ったサービスを公開して運用したりします。2つの異なる立場を持っているわけです(決して珍しくないと思ってます)。前者ではある程度の予算の中でクラウドのサービスを契約したりして、必要であればベンダーが提供するコンテナやコンテナ・オーケストレーションも使って構築することになります。 一方後者では、これらのインフラ構築部分も自腹になるわけです。まあ「これも授業代」と太っ腹に考える人は立派だと思いますが、コンテナ・オーケストレーションまで使おうとするとそれなりに懐も痛む価格だったりします。


要するに「個人開発者としての自分はケチ」なわけで(繰り返しますが、決して珍しくないと思ってますw)、「ケチはケチなりに知恵と作業でなんとかしたい」と考えるわけです。コンテナ技術やコンテナ・オーケストレーション技術を否定するつもりは全くありませんが、「より安価」に「フロントエンドを安定稼働」させる方法はないものか、と:
2021032001


#「コンテナ・オーケストレーションまで自分で構築すればいい」と考える人もいると思うので一応コメントを。技術的にはそのとおりなんですが、目的はあくまで「安価なフロントエンドの安定稼働」です。そう考えると例えば1ノードで構築した場合に目的を達成しているといえるか・・・ では複数ノードを構築する場合、今度は安価といえるのか・・・ となってしまうと考えました。


そんな自分にひらめいた1つの案が「フロントエンドを GitHub Pages にする」方法です。GitHub Pages は GitHub の無料アカウントがあれば使うことのできる「静的ウェブコンテンツの公開サービス」です。GitHub の一部として考えると、容量は(ほぼ)無制限で、アップロード直後にバックアップされ、世界中のウェブサービスの中でも指折りの安定稼働を誇っています。「フロントエンドを安定させたい」という目的だけを考えると、「かなり使える案」だと思っています:
2021032002
(↑フロントエンドを GitHub Pages にして、バックエンドを IBM Cloud にする場合)


もちろんこの方法は万能ではありません。まず GitHub Pages で公開できるコンテンツはウェブアプリケーションではなく「静的ページ」、つまり HTML ページに限られます(この時点で i18n などを考慮するアプリケーションページの公開は難しくなります)。表示データは REST API で取得すれば良いのですが、フロントエンドが静的ページである以上、通信は AJAX に限られてしまいます。その結果、API 側は(クロスオリジン通信をすることになるため)CORS を考慮した設計が必須となります:
2021032003


フロントエンドはウェブアプリケーションではないので、ウェブページをテンプレートから作る、といった便利な手法が使えず、AJAX を駆使したレンダリングを実装しないといけないことも不利な点となりますが、バックエンド側も考慮点の影響が大きな方法ではあると思っています。ただ「安価にフロントエンドを安定稼働」させる面ではイケそうな方法にも感じています。


・・・といった背景がありました。この前提だと REST API 部分も便利なサービスを有償契約して使うのではなく、安価なアプリケーション・サーバーを使って、自前で API サーバーを構築する必要があります(最悪、ここにトラブルがあってもフロントエンド側ではその旨を伝えることができる構成)。というわけで、無料のライトプランが使える IBM Cloud で「Node.js + Express で作った API を CORS 対応にする」ための方法を理解しておく必要がありました。


【Node.js + Express の REST API を CORS 対応する】
こちらはサンプルを用意しておきました:
https://github.com/dotnsf/express-cors


まず settings.js の exports.cors 配列変数内にクロスオリジン通信を許可するオリジン(ドメイン)を登録しておきます。デフォルトでは以下のようになっていて、'http://localhost:8080' と 'https://dotnsf.github.io' からの AJAX 通信を許可するよう設定しています。前者はローカルでの動作確認用ですが、実際に Github Pages で運用を始めた後は削除しておくべきです。後者は僕の Github Pages のドメインです。実際にみなさんが Github Pages でこの API を使う場合は自分自身の Github Pages ドメインに変更してください:
//. settings for CORS
exports.cors = [ 'http://localhost:8080', 'https://dotnsf.github.io' ];

本体とも言える app.js の内部は以下です:
//. app.js
var express = require( 'express' ),
    app = express();

var settings = require( './settings' );

app.use( express.static( __dirname + '/public' ) );

//. CORS
if( settings && settings.cors && settings.cors.length && settings.cors[0] ){
  var cors = require( 'cors' );
  var option = {
    origin: settings.cors,
    optionSuccessStatus: 200
  };
  app.use( cors( option ) );
}

app.get( '/ping', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );
  res.write( JSON.stringify( { status: 'OK' } ) );
  res.end();
});


var port = process.env.PORT || 8080;
app.listen( port );
console.log( "server starting on " + port + " ..." );

赤字以外の部分はごく普通の Node.js + Express を使った REST API アプリケーションです。この例では GET /ping という動作確認用のシンプルな API を1つだけ定義しています(サーバー側に障害等が起きていなければ、返り値は常に { status: 'OK' } という JSON です)。

肝心の CORS の対応をしているのが赤字部分です。settings.js の内容を確認し、exports.cors 配列に有効な値が1つでも含まれていると判断した場合は、cors ライブラリを使って設定されているオリジンを対象に CORS を有効にします。ちなみに settings.js の exports.cors が例えば null とか [](空配列)とかに設定されている場合は CORS の処理が行われないので、CORS の処理としてはデフォルトの「全てのクロスオリジンからの AJAX アクセスを許可しない(同一オリジンからの AJAX アクセスのみ許可する)」ことになります。

あとは必要に応じてベーシック認証を加えるとか、トークン認証を加えるとか(フロントエンドが静的コンテンツだとトークンの管理が面倒そうだけど)するなどして CORS 対応の REST API を構築することができます。ここは手順がわかってしまえば簡単そうですね。


【運用時】
こんな感じでデータベースへの読み書き検索などが REST API 化され、CORS 対応までできていれば Github Pages の HTML からも利用できるので、かなり安定したフロントエンドコンテンツを実現できそうです。この形で REST API が実現できていると、例えばウェブアプリを作るハンズオンでもあらかじめ用意した REST API を Github Pages から呼び出す形で実現できるので、フロントエンドの運用サーバーは無料で(しかもかなり安定したものを)用意できます。更にフロントエンド部分の開発時にはこんなフォルダ構成の Node.js + Express のアプリケーションにしておくと docs/ 以下の静的コンテンツを対象に作って、ローカルで動作確認もして、コードごと Github にあげて docs/ 以下を Github Pages で公開する、といった便利な開発・運用も可能になります:
//. app.js
var express = require( 'express' ),
    app = express();

app.use( express.static( __dirname + '/docs' ) );

var port = process.env.PORT || 8080;
app.listen( port );
console.log( "server starting on " + port + " ..." );
(↑メインファイル。静的コンテンツのフォルダを /docs に指定している以外はごく普通のシンプルな Express アプリ)


2021032004
(↑このプロジェクトを Github に上げる)


2021032005
(↑Github Pages の設定で /docs フォルダ以下を公開するよう指定する)


フロントエンドコンテンツは全て AJAX 対応させる必要があり、昨今の流行りとは違う形での実装は必要になります。それはそれで大変だし、普通のウェブアプリケーションで使える便利な技術も使えなくて不便な面も出てくるのですが、"Poorman's stable web contents" みたいに割り切って使うネタを準備しています。まだアイデア先行な面もあって実証実験が必要な段階だと思っていますが、運用していて気づくことがあったらまたこのページに追記します。


Node.js + Express の環境でウェブアプリケーションを開発していると、いくつかの方法で URL のパラメータを受け取る方法があります:
6dnng3pre04xxdebia1g


例えば、 "/xxx?name0=value0&name1=value1" というパス /xxx への GET アクセス時に URL パラメータである name0 及び name1 の値(それぞれ "value0" と "value1" に相当する部分)を取得するには以下のように req.query オブジェクトから取り出すという処理で実現できます:
var express = express();
var app = express();

app.get( '/xxx', function( req, res ){
  var name0 = req.query.name0;
  var name1 = req.query.name1;
    :
});

  :
  :

また、"/yyy/name2" というパスへの GET アクセス時における "name2" 部分を可変にしたい(例えば "/yyy/1" にアクセスした場合は name2 = "1" で、"yyy/2" にアクセスした場合は name2 = "2" として取り出したい)場合は、以下のように req.params オブジェクトから取り出すことで実現できます:
var express = express();
var app = express();

app.get( '/yyy/:name2', function( req, res ){
  var name2 = req.params.name2;
    :
});

  :
  :

ここまでは express の標準機能として実装されています。


で、今回挑戦したいのは "/zzz/name0/name1/name2/.../name9" というパスへの GET アクセス時における name0, name1, name2, ..., name9 の値を取り出すことです。この例ではパラメータが10個ですが、実際にはいくつ存在しているかわからないものとします(1個かもしれないし、10個以上かもしれない)。つまり "/zzz/" で始まるパスへの GET アクセス時に "/zzz" 以降のパス部分をまとめてパラメータ化して取得したい、という要望実現への挑戦です。

パラメータの個数が固定であれば(例えば3つであれば)、上述の "/yyy/:name2" の時の応用で、"/zzz/:name0/:name1/:name2/" のハンドリングを行って、req.params.name0, req.params.name1, req.params.name2 の値をそれぞれ取り出すことで実現できます。ただ今回はこのパラメータの数を可変にしたい場合の実現方法を考えたいのです。

その実現例の1つがこちらです:
var express = express();
var app = express();

app.use( function( req, res, next ){
  if( req.url.startsWith( '/zzz/' ) ){
    // '/zzz/' で始まるパスへのアクセスだった場合は、5文字目以降をパラメータとして取り出し、
// '/zzz?params=' の後ろに URL パラメータとしてくっつけてリダイレクトする var params = req.url.substr( 4 ); res.redirect( '/zzz?params=' + params ); }else{
// '/zzz/' で始まらないパスへのアクセスだった場合はそのまま処理する next(); } });
app.get( '/zzz', function( req, res ){ // params URL パラメータの値を取り出して、'/' で分割する var params = req.query.params.split( '/' );

// params[0] に "name0" が、params[1] に "name1" が、・・それぞれ格納されている : }); : :

可変階層のパスをハンドリングすることはできないので、該当部分を URL パラメータ(params)として処理するようにしています。そして /zzz/ で始まるパスへのアクセスがあった場合は app.use() による前処理として5文字目(2つ目の '/')以降を取り出して params パラメータの値に指定してリダイレクトするようにしています。こうすることで /zzz/name0/name1/name2/../name9 へのアクセスは /zzz?params=name0/name1/../name9 へアクセスするようリダイレクトされ、リダイレクトされた先で元のパラメータを残さず処理することができるようになります。

リダイレクトしての処理なので、ずるい(?)やり方ではあるんですが、一応これなら /zzz/name0/name1/name2/... へのアクセス全般を可変階層でも(リダイレクト先で)処理できそうです。


PayPay for Developers が公開され、アプリケーション開発者が自分のアプリケーション内に決済機能をもたせることが比較的容易にできるようになりました。これまでにも同様の機能はありましたが、PayPay アプリを利用した決済のため、クレジットカード情報など個人情報管理を意識することもなく、かつ日本円での決済ができるというメリット、加えて後述するようにその実装も非常にシンプルにできる点が画期的でした。

以下、実際に自分が Node.js で作成したサンプルを公開して紹介します。どの程度のシンプルさなのかも実際に見ていただきたいです。なお以下で紹介する内容は sandbox モードという、実際の決済は行わないテストモードで利用する想定で紹介しているので、安心して使ってみてください(アプリケーションはそのままで、加盟店登録を行って、モードを sandbox モードからプロダクションモードに変更することで本当の決済が行えるようになります)。

また API を細かく説明する前に、まずは実際に PayPay API を使ったサンプルアプリケーションを動かして決済を行い、その後でどのような実装になっているのかを説明します。

なお本稿は 2020/11/13 時点での動作を確認したアプリケーションに基づいて記載しています。


【準備】
サンプルを実際に動かすためにはいくつかの事前準備が必要です。まず何はともあれスマートフォンに PayPay アプリをインストールしておきましょう:
2020111301


次にローカル環境に Node.js をインストールしておきます。サンプルアプリケーションは Node.js で記述されているため、その実行環境としての Node.js を導入します。

そして PayPay for Developers に ID とサービス(実店舗に相当するアプリケーション)を登録する必要があります:
https://developer.paypay.ne.jp/account/signup

まずはメールアドレスとパスワードを指定して、ID を登録します:
2020111301


最初に指定したメールアドレスに確認のためのコードが送信されるので、メールを受信したらそこに書かれているコードを入力して「確認する」ボタンを押します。これでメールアドレスの存在を証明できました:
2020111301


次にサービスの内容を入力します。ここで入力した内容が決済時の画面などに表示されるものです。右上で skip することもできるようですが、ここでは登録する方法を紹介します:
2020111302


といってもここで指定するのはロゴの画像と、サービスの名称です。この例では Qdle という名前でサービスを登録しています(深い意味はありません、後で出てきます)。このあたりは適当に:
2020111303


次の画面では利用する決済手段を選ぶことができます。今回の例に限っては「ウェブペイメント」と「動的ユーザースキャン」なのですが、今後どういう使い方をするかわからないのでとりあえず全部チェックしました(最初からされています)。で「登録する」をクリック:
2020111304


無事にアカウントとサービスが PayPay for Developers に登録できました。「開始する」ボタンでダッシュボード画面に移動します:
2020111305


なお登録後はこちらのページから ID とパスワードでログインできます:
https://developer.paypay.ne.jp/account/signin

2020111302


ログイン直後のダッシュボード画面ではこのようなページが表示されます。本番モードではなく「テストモード」で稼働している点を確認してください(テストモード中は架空ユーザーでの決済になり、実際のお金の決済は行われません):
2020111302


画面下部に API キーシークレットが表示されています。また画面右上の DO と書かれた箇所をクリックすると MERCHANT ID を確認することができます。これら3つの値はこの後で使うことになるのでメモしておくか、コピー&ペーストできるようにしておきます:
2020111303


また後ほど sandbox モードで決済を行うのですが、その時に決済を行う架空ユーザー(3名)とそのパスワード、架空残高を画面下部の「テストユーザー」タブから確認することができます(パスワードの値はパスワード横のアイコンをクリックすることで見ることができます)。今はまだ使いませんが、この場所でユーザー情報を確認できることを覚えておきます:
2020111304


以上でサンプルアプリケーションを動かす準備はできました。


【ソースコードのダウンロードと編集】
次にサンプルソースコードをダウンロードします。git clone するか、ソースをまとめて zip ダウンロードして展開するなどしてソースコード一式を手元に用意します:
https://github.com/dotnsf/paypayapi

2020111305


展開されたファイルのうち、settings.js ファイルだけは動かす前に値の編集が必要です。テキストエディタでこのファイルを開き、以下の3行(テストモードでなく、本番モードで動かす場合は4行)を変更して保存します:
exports.apikey = '(API キーの値)';
exports.apisecret = '(シークレットの値)';
exports.merchantid = '(MERCHANT ID の値)';
exports.productionMode = false;   //. 本番モードの場合のみ true に変更

上3つは上述のダッシュボード画面で確認した値に変更します。また4行目はテストモードであれば false のままで、加盟店登録後に本番モードで稼働させる場合のみ true に変更します。

最後にこの状態で依存ライブラリをまとめてインストールします。コマンドラインやターミナルで paypayapi ソースコードフォルダに移動し、次のコマンドを実行します(実行完了までしばらく時間がかかります):
$ npm install

これでサンプルアプリケーションを動かすための準備ができました。

【アプリケーション起動】
まずはサンプルアプリケーションを動かします:
$ node app

そしてウェブブラウザで以下のアドレスを指定して開きます:
http://localhost:8080/

こんな感じのシンプルなページが表示されれば、アプリケーションの起動に成功です:
2020111306


画面には3つの機能しかなく、1つは支払額(円)を指定するフィールド、1つは支払い明細を記述するフィールド、もう1つは支払いを実行するボタンです。適当に入力してみます:

※下の例では支払額を 123 円、支払明細には「コーヒー代」と入力しました。
2020111307


この条件で PayPay による支払いを(架空ユーザーで)実行してみますが、テストモードで支払いを実行するために少し別の準備が必要になります。この状態のまま「支払い」ボタンを押さずに以下を続けます。

※ここで「支払い」ボタンを押して、以下の開発モードの切り替えに時間がかかってしまうと QR コードが無効になってしまい、スキャンできなくなってしまいます。その場合は改めてこの画面で「支払い」ボタンを押して QR コードを生成し直してください。


【PayPay 開発者モード】
支払いボタンを押す前にスマホ側の PayPay の準備をしておきます。スマホ側で PayPay を起動します。自分の(本物の)アカウントでログイン済みの場合はそのままでは架空ユーザーでの取引ができないので一旦ログアウトします。アカウントを選択し、アカウント情報画面の最下部からログアウトします:
2020111302

2020111303


改めて未ログイン状態で PayPay アプリを起動すると以下のようなログイン画面が表示されます。ここで PayPay アプリをテストモードに切り替えるため、画面左上の PayPay ロゴを7回タップします:
2020111304


途中からカウントダウンに・・・続けてロゴをタップします:
2020111305


すると「開発者モードでログイン」という選択肢が現れるので、これを選択します:
2020111306


開発者モードでのログイン画面が表示されます:
2020111307


ここでは自分のアカウントではなく、上述の PayPay for Developers ダッシュボード画面で確認したテストユーザーのいずれかの ID とパスワードを入力してログインします(ここでは下図一番上にいる 08075317593 ユーザーでログインしてみます):
2020111308


対象ユーザーのパスワード横にあるアイコンをクリックしてパスワードマスクを外すことで実際のパスワードを視認できるようになります。ユーザーネームとパスワードを入力して PayPay アプリに開発者モードでログインします:
2020111308


初回ログイン時のみ多要素認証が求められます。テストモードユーザーの SMS を受け取ることはできませんが、テストモードユーザーの認証コードは全て 1234 なので、1234 と入力します:
2020111309


開発者モードで、テストユーザーの ID でログインした時の画面です。上部に「開発者モードである」旨が表示されています:
2020111310



またこの状態で残高を確認してみます。ダッシュボードで確認した時と同じ残高(下図の場合は 99900 円)になっていることを確認します:
2020111311


これで PayPay をテストユーザーで使う準備ができました。普通の(本来の)モードに戻す場合はいったんログアウトし、再度正規のユーザーでログインしてください。


【アプリで生成した QR コードで決済】
PayPay アプリ側でテストユーザーの準備ができたので、改めて先程のアプリケーションに戻って、決済の続きを実行します。

支払額と明細を確認して「支払い」ボタンをクリックします:
2020111301


別ウィンドウが開き、その中に決済に関する情報と QR コードが表示されているはずです:
2020111302


この QR コードを開発者モードでログイン中の PayPay アプリを使ってスキャンします:
2020111301


すると(PayPay ユーザーにはおなじみの)「ペイペイッ」という音声が流れ、決済が完了してしまいます。普通に PayPay 使った時とまったく同じです:
2020111302


"Qdle" に支払った、という画面になっていますが、この "Qdle" は PayPay for Developers に最初に登録したサービス名です:
2020111303


残高を確認すると支払った分が引かれています。実決済ではないというだけで、本当に決済が実現してしまいました。。
2020111304


ダッシュボード側にも決済が反映されており、残高が更新されているはずです:
2020111303


開発者モードで実行されているため、実際に本当のお金の動きがあるわけではないのですが、このモードを変更した上で、実際の PayPay アプリで QR コードを読みとって決済すれば本当にお金が動く決済が実現します。

以下、このアプリケーションの中身(というか、このオペレーションに関わる部分)を抜粋して紹介しますが、いかにシンプルな作りになっているのかを確認できると思っています。


【ソースコード解説】
ではこの PayPay 決済アプリがどのようなソースコードで実現されているのかを解説します。といっても本当にシンプルなので、あまり解説するポイントもないのですが・・・

まずアプリ画面の HTML 部分です。本当にこれだけ、「支払い」ボタンの onClick イベントに反応して createQRCode() 関数を実行しているだけです:
<div class="container">
  <table class="table table-bordered">
    <tbody>
      <tr>
        <td>支払額(円)</td>
        <td><input type="text" id="amount" value="100"/></td>
      </tr>
      <tr>
        <td>明細</td>
        <td><input type="text" id="orderDescription" value="" placeholder="書籍代"/></td>
      </tr>
      <tr>
        <td> </td>
        <td><button class="btn btn-primary" id="orderBtn" onClick="createQRCode();">支払い</button></td>
      </tr>
    </tbody>
  </table>
</div>


次に「支払い」ボタンをクリックした時に実行されるハンドラ(createQRCode() 関数)の JavaScript がこちらです。支払額と明細の情報を送信して POST /paypay/qrcode の REST API を実行しているだけです。成功後の処理については後述します。ここまでは特に解説するほどの内容にはなっていません:
function createQRCode(){
  var _amount = $('#amount').val();
  var orderDescription = $('#orderDescription').val();
  
  var amount = ( _amount ? parseInt( _amount ) : 0 );
  orderDescription = ( orderDescription ? orderDescription : '' );
  if( amount ){
    $.ajax({
      type: 'post',
      url: '/paypay/qrcode',
      data: { amount: amount, orderDescription: orderDescription },
      success: function( result ){
        console.log( result );
          :
          :
      },
      error: function( e0, e1, e2 ){
        console.log( e0, e1, e2 );
      }
    });
  }else{
    alert( 'amount needed.' );
  }
}

次に REST API 側のコードを紹介します。REST API は paypay/paypay.js 内でまとめて実装していて、この中では最初に settings.js で指定された API キーやシークレット、MERCHANT ID の値を使ってPAYPAY オブジェクトの初期化をしています:
var settings = require( '../settings' );

var PAYPAY = require( '@paypayopa/paypayopa-sdk-node' );
PAYPAY.Configure({
  clientId: settings.apikey,
  clientSecret: settings.apisecret,
  merchantId: settings.merchantid,
  productionMode: settings.productionMode
});

そして POST /paypay/qrcode の中身がこちらです。PayPay SDK を使って PAYPAY.QRCodeCreate という関数を実行しています。実行時のパラメータ(payload)を指定していますが、決済額の情報は amount オブジェクトで指定しています( currency を "JPY" に指定していますが、現状はこの値しか使えないようです)。また merchantPaymentId をランダムに生成して指定していますが、ここで指定した値は後にキャンセルする際に必要になるので、アプリケーションによってはどこかで保持しておく必要があるかもしれません。また orderDescription に明細情報の文字列を入れておくことで決済時の画面にもコメントを表示させることが可能になります。

この PAYPAY.QRCodeCreate 関数は成功するとステータスコードが 201 になります。その値を見て成功・失敗を判断し、application/json 形式で結果を返しています:
router.post( '/qrcode', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );

  if( req.body.amount ){
    var payload = {
      merchantPaymentId: generateId( 'dotnsf-paypay' ),
      amount: { amount: req.body.amount, currency: "JPY" },
      codeType: "ORDER_QR",
      orderDescription: ( req.body.orderDescription ? req.body.orderDescription : 'orderDescription' ),
      isAuthorization: false,
      redirectUrl: "https://paypay.ne.jp/",
      redirectType: "WEB_LINK",
      userAgent: ( req.body.userAgent ? req.body.userAgent : 'My PayPay App/1.0' )
    };
    PAYPAY.QRCodeCreate( payload, function( response ){
      //console.log( response );
      if( response.STATUS && response.STATUS >= 200 && response.STATUS < 300 ){   //. 実際は 201
        res.write( JSON.stringify( { status: response.STATUS, body: JSON.parse( response.BODY ) } ) );
        res.end();
      }else{
        res.status( response.STATUS );
        res.write( JSON.stringify( { status: response.STATUS, body: JSON.parse( response.BODY ) } ) );
        res.end();
      }
    });
  }else{
    res.status( 400 );
    res.write( JSON.stringify( { status: 400, error: 'no amount info found.' } ) );
    res.end();
  }
});


ここも PayPay SDK を使っているだけで、それほど複雑な内容ではないと思っています。そして改めて1つ前の「支払い」ボタンをクリックした時のハンドラの成功後の処理がこちらです:
          :
          :
      success: function( result ){
        console.log( result );
        if( result && result.status && result.status == 201 && result.body && result.body.data ){
          var merchantPaymentId = result.body.data.merchantPaymentId; //. 支払いID(キャンセル時に必要)
          var codeId = result.body.data.codeId; //. QRコードID(QRコード削除時に必要)
          var url = result.body.data.url;  //. QRコードが表示されるページの URL

          if( url ){
            //. QRコードが表示されるページを別ウィンドウで開く
            window.open( url, 'PayPayWindow' );
          }
        }
      },
          :
          :


REST API の実行結果として返ってきたオブジェクト(result)を使い、result.body.data.url の値を取得します。この値が送信した条件で決済(より正確には決済して、自分が登録した店舗に決済額が入金される仕組みまで含めた処理を実行)するための QR コードが表示されるページの URL となっています。なので window.open() を使い、別ウィンドウで同ページを表示する、という処理を行っています。

別ウィンドウで表示されるページは上述しました。ここに表示される QR コードを PayPay アプリで読みとり、後は PayPay とそのアプリが面倒みてくれます。なのでサンプルアプリとしては何もしていません。

QR コード決済のエラー処理や例外対応については省略してしまっていますが、本質的にはたったこれだけのコードで自分のアプリケーションに日本円での PayPay 決済処理が実装できてしまう、という事実に驚いてしまいました。


【使ってみた感想】
なんといっても非常に簡単に実装できてしまうことに驚きでした。クレジットカード情報(の保管)を意識する必要もなく、日本円でのオンライン決済アプリのハードルがかなり下がったんじゃないかと感じました。これから決済アプリが流行る要因にもなりうると思っています。

このサンプリアプリケーションのソースコードを公開しているので、興味ある方は中を見ていただきたいです。実際にはキャンセル処理への対応ができるような REST API も実装済みではありますが、本サンプルアプリ内では使っていません。なので、本当にシンプルなコードだけでここまでできてしまい、かつアプリケーション側が意識すべきセキュリティ要件もかなり低く作れそうな印象を持っています。


【参考リンク】
Node.js 用 PayPay SDK
PayPay for Developers 公式ドキュメント
ダイナミック QR コード解説


【応用編】
なお、このブログエントリの続きはこちら。実運用を意識した内容の応用編です:
http://dotnsf.blog.jp/archives/1078272938.html

サブジェクトが少しわかりにくいと思ったので最初にやりたいことを補足しておきます。

ウェブサービスを公開する際に Basic 認証と呼ばれる認証機能を有効にすることがあります。アクセス時にユーザーIDとパスワードが聞かれ、正しい組み合わせを入力しないと先に進めなくなる、というものです。会員制サービスや、正式公開前のサービスを限られた人だけで使いたい場合、グーグル等の検索エンジンクローラーに見つからない状態で運用したい場合などによく使われます:
thumb_basic


今回やりたかったのは、この Basic 認証を例えば以下の条件で実現するような Node.js アプリケーションを作ることです:
・パス /hello 以下にアクセスした際に Basic 認証が必要
・/hello にアクセスするには URL パラメータ id が必要(つまり GET /hello だけではエラーとなり、GET /hello?id=XXX というフォーマットでアクセスする必要がある)
/hello?id=XXX の時と /hello?id=YYY の時とでは Basic 認証のユーザーIDやパスワードが異なる


最後のが今回の肝となる条件です。パラメータ id の値ごとに Basic 認証のユーザーIDやパスワードが変わり(データベース等に格納されているものを id をキーに取り出して比較するイメージ)、これを Node.js + Express 環境でどのように実現するか、というのが挑戦内容です。


やりたいことが明確になったところで、改めて Node.js + Express 環境で Basic 認証をかける方法をググってみると、basic-auth-connect モジュールを使う方法がメジャーな方法の1つとして見つかります。これは簡単にいうと以下のような感じで Basic 認証をかけるものことができるものです:
var express = require( 'express' ),
    basicAuth = require( 'basic-auth-connect' ),
    app = express();

app.all( '/hello*', basicAuth( function( user, pass ){
  return ( 'username' === user && 'password' === pass );
}));

  :
  :

app.get( '/hello', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );
  res.write( JSON.stringify( { status: true }, 2, null ) );
  res.end();
});

  :
  :

GET /hello リクエストに対しては単に { status: true } という JSON を返すだけの定義がされていますが、その前に Basic 認証を有効にする部分が記述されています。この例では(/hello に何らかの URL パラメータが付属する場合も含めた) /hello* というパスに GET リクエストが行われた場合に Basic 認証が必要になり、ユーザーID 'username' 、パスワード 'password' が入力された場合のみ true(認証成功)で実際の GET /hello の処理が行われ、それ以外の場合は false(認証失敗)という扱いとなって再度入力が求められたり、何度か間違えると認証エラー扱いとなる、というものです。とても便利で、よく使っています。


さて、今回は上述の条件で Basic 認証を有効にする必要があり、少し異なる処理が必要です。正しいユーザーIDとパスワードは URL パラメータ id によって変わるのですが、この URL パラメータは req オブジェクから取り出す必要があり、今の形のままでは(認証判断時に req オブジェクトが取得できないので)取得が難しそうです。自分もこの basic-auth-connect モジュールを使う前提で実装を考えていたので詰まってしまいました。。

結論としては basic-auth-connect モジュールを使うことを諦め、自分で認証判断してエラー時にエラーコードを返す、という地味な処理に切り替えて実装できました:
var express = require( 'express' ),
    //basicAuth = require( 'basic-auth-connect' ),  //. basic-auth-connect は使わない
    app = express();

//. パラメータ id 毎に必要なユーザーIDとパスワード(本当はデータベース等から取得するイメージ)
var db = {
  "000" : { user: 'a', pass: 'x' },
  "001" : { user: 'b', pass: 'y' },
  "002" : { user: 'c', pass: 'z' }
};

/* URL パラメータ毎に認証情報を変えたい */
app.use( function( req, res, next ){
  //. hello* へのリクエスト時かどうかを判断
  var originalUrl = req.originalUrl;
  if( originalUrl.startsWith( '/hello' ) ){
    //. URL パラメータ ID を取り出す
    var id = req.query.id;
    //. 指定された ID のユーザー ID とパスワードが存在しているかどうかを調べる
    if( db[id] ){
      //. ヘッダから入力されたユーザーIDとパスワードを取り出す
      var b64auth = ( req.headers.authorization || '' ).split( ' ' )[1] || '';
      var [ user, pass ] = Buffer.from( b64auth, 'base64' ).toString().split( ':' );

      //. 入力内容が正しい場合のみ next() を返して本来の処理へ
      if( db[id].user == user && db[id].pass == pass ){
        return next();
      }else{
        //. 入力内容が間違っていた場合は認証エラー扱いとする
        res.set( 'WWW-Authenticate', 'Basic realm="MyApp"' );
        res.status(401).send( 'Authentication required.' );
      }
    }else{
      //. 指定された ID が存在していなかった場合も認証エラー扱いとする
      res.set( 'WWW-Authenticate', 'Basic realm="MyApp"' );
      res.status(401).send( 'Authentication required.' );
    }
  }else{
    return next();
  }
});

  :
  :

app.get( '/hello', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );
  res.write( JSON.stringify( { status: true }, 2, null ) );
  res.end();
});

  :
  :


赤字部分が今回作成した処理です。req オブジェクトからリクエスト先のパスや Basic 認証で指定された情報を取り出して正しい情報かどうかを判断し、正しい場合は本来の処理へ、間違っていた場合は HTTP の認証エラー結果を返すような内容を記述しています。basic-auth-connect モジュールを使うとこのあたりの細かな記述をする必要がなかったのですが、自分で判断する場合はこのあたりも自分の責任範囲で用意する必要があります。

上述の例では URL パラメータ id は "000", "001", "002" のいずれかである必要があり、それぞれの場合の Basic 認証情報(ユーザーID : パスワード)はそれぞれ "a":"x", "b":"y", "c":"z" としています。この正しい組み合わせが指定された場合のみ GET /hello が実行されて結果が返される、という処理が実行されるようになります。


(参照)
https://stackoverflow.com/questions/23616371/basic-http-authentication-with-node-and-express-4


このページのトップヘ