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

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

タグ:nodejs

Node.js のアプリケーションで、以下のような処理を実装してみました:
 - 認証(ログイン)用の API には誰でもアクセスできる
 - 認証 API では ID とパスワードを与えて認証し、正しいユーザーにはトークンを発行する
 - 認証以外の主な API はこの発行されたトークンを使ってアクセスした時だけ実行を許可する
 - API 呼び出し時にトークンがなかったり、正しくなかった場合は実行せずにエラー


この仕組を実現するために JSON Web Tokens (以下 "JWT")を使いました:
https://jwt.io/

2017081400


JWT はオープンかつ Node.js ではスタンダードなトークンベースの認証ライブラリです。以下で紹介するサンプルでは Web フレームワークである Express や、POST データを扱う body-parser も合わせて使うので、まとめてインストールしておきます:
$ npm install express body-parser jsonwebtoken

これらのライブラリを使って以下のようなアプリケーションを作成します:
//. app.js

var express = require( 'express' );
var app = express();
var bodyParser = require( 'body-parser' );
var jwt = require( 'jsonwebtoken' );

//. アプリケーションサーバーの稼働ポート番号
var port = process.env.PORT || 8080

//. 任意のシークレット文字列を登録
app.set( 'superSecret', 'welovenodejs' );  //. 任意の文字列

app.use( bodyParser.urlencoded( { extended: false } ) );
app.use( bodyParser.json() );

//. ユーザー情報(本来は DB などに格納された情報を使う)
var users = [
  { name: 'user0', password: 'pass0', admin: true },
  { name: 'user1', password: 'pass1', admin: false },
  { name: 'user2', password: 'pass2', admin: false },
  { name: 'user3', password: 'pass3', admin: false }
];

//. ドキュメントルートへの GET は許可
app.get( '/', function( req, res ){
  res.send( 'Hello. The API is at http://localhost:' + port + '/api' );
});

//. API ROUTES
var apiRoutes = express.Router();

//. トークンなしでアクセスを許可する API を先に定義する

//. POST(http://localhost:8080/api/authenticate)
apiRoutes.post( '/authenticate', function( req, res ){
  for( var i = 0; i < users.length; i ++ ){
    if( users[i].name == req.body.name && users[i].password == req.body.password ){
      //. 認証したユーザーの情報を使ってトークンを生成
      var token= jwt.sign( users[i], app.get( 'superSecret' ), {
        expiresIn: '24h'
      });
      res.json( { success: true, message: 'Authentication successfully finished.', token: token } );
      return;
    }
  }

  res.json( { success: false, message: 'Authentication failed.' } );
  return;
});

//. ここより上で定義した API には認証フィルタはかけていない(そのまま使える) //. 認証フィルタ apiRoutes.use( function( req, res, next ){
//. ポスト本体、URLパラメータ、HTTPヘッダいずれかにトークンがセットされているか調べる var token = req.body.token || req.query.token || req.headers['x-access-token']; if( !token ){
//. トークンが設定されていなかった場合は無条件に 403 エラー return res.status(403).send( { success: false, message: 'No token provided.' } ); } //. 設定されていたトークンの値の正当性を確認 jwt.verify( token, app.get( 'superSecret' ), function( err, decoded ){ if( err ){ //. 正当な値ではなかった場合はエラーメッセージを返す return res.json( { success: false, message: 'Invalid token.' } ); } //. 正当な値が設定されていた場合は処理を続ける req.decoded = decoded; next(); }); }); //. 以下はトークンがないと使えない API //. GET(http://localhost:8080/api/) apiRoutes.get( '/', function( req, res ){ res.json( { message: 'Welcome to API routing.' } ); }); //. GET(http://localhost:8080/api/users) apiRoutes.get( '/users', function( req, res ){ res.json( users ); }); //. /api 以下に API をルーティング app.use( '/api', apiRoutes ); app.listen( port ); console.log( 'server started http://localhost:' + port + '/' );

肝になるのはルーティングに認証フィルタを定義している箇所です。ここよりも前(上)で定義した内容には認証フィルタは有効にならないので、認証なしで使える API となります(つまり GET / と POST /authenticate は認証していなくても使えます)。

この POST /authenticate API でポストデータ user, password を受取り、その値が変数 users の中で定義されているいずれかの組み合わせと一致していれば 24H 有効なトークンが発行されます(これを以下の API 実行時にパラメータ指定します)。

一方、ここよりも後(下)で定義する内容には認証フィルタが有効になり、GET /api と GET /api/users は上記方法で取得したトークンがパラメータに設定されていないと正しく処理されない API となります。


実際にこのアプリケーションを実行し($ node app)、curl コマンドで挙動を確認してみましょう。まずは問題なく実行できるはずの GET / を実行します:
$ curl -XGET 'http://localhost:8080/'
Hello. The API is at http://localhost:8080/api

問題なく実行できました。次は認証フィルタをかけた GET /api を実行してみます:
$ curl -XGET 'http://localhost:8080/api'
{"success":false,"message":"No token provided."}

API を実行した結果、「トークンがない」時のエラーメッセージが表示されました。ここも期待通りに動いています(試しませんが、ユーザー一覧を取得する GET /api/users も同様のエラーになります)。

では POST /authenticate で認証してトークンを取得してみますが最初はわざとパスワードを間違えてみます:
$ curl -XPOST -H 'Content-Type:application/json' 'http://localhost:8080/authenticate' -d '{"name":"user1","password":"pass0"}'
{"success":false,"message":"Authentication failed."}

先程同様にエラーになりましたが、エラーメッセージが「認証失敗」に変わりました。では改めて正しいパスワードを指定して実行してみます:
$ curl -XPOST -H 'Content-Type:application/json' 'http://localhost:8080/authenticate' -d '{"name":"user1","password":"pass1"}'
{"success":true,"message":"Authentication successfully finished.","token":"XXXXXX...XXXXXX"}

今度は認証が成功しました。レスポンスにも "token" が含まれています。最後にここで返ってきた token の値を指定して、先程アクセスできなかった GET /api を実行してみます:
$ curl -XGET 'http://localhost:8080/api?token=XXXXXX...XXXXXX'
{"message":"Welcome to API routing."}

今度は API が実行できました。同様にして GET /api/users も実行できるはずです:
$ curl -XGET 'http://localhost:8080/api/users?token=XXXXXX...XXXXXX'
[{"name":"user0","password":"pass0","admin":true},{"name":"user1","password":"pass1","admin":false},{"name":"user2","password":"pass2","admin":false},{"name":"user3","password":"pass3","admin":false}]


こんな感じで API の実行可否をトークンで制御できるようになりました。

今回の例ではソースコード内に静的に用意されたユーザー一覧を使って認証を行いましたが、実際の運用ではデータベース内に定義されたテーブル情報などを使うことになると思います。ただ JWT の基本的な考え方はこの1つのソースファイルだけで実現できているので、応用しやすいと思っています。

 

Node.js でのスクレイピングに挑戦したことがなかったのでやってみました。

一般的にスクレイピングでは http でコンテンツ文字列を取得して、正規表現などを用いて取得したコンテンツ文字列から指定のパターンに合致する箇所のテキストを取得します。今回はその部分を手でガリガリ、、ではなく、この cheerio-httpcli モジュールを使ってスクレイピングに挑戦しました:
https://www.npmjs.com/package/cheerio-httpcli

2017072600


まずはこの cheerio-httpcli モジュールを npm でインストールします:
$ npm install cheerio-httpcli

Node.js のコード内で cheerio-httpcli モジュールを利用する場合は以下のような記述を行います:
var client = require( 'cheerio-httpcli' );
 :

var url = 'https://www.google.com/finance/converter?a=1&from=EUR&to=JPY';
client.fetch( url, {}, function( err, $, res ){
  $('.bld').each( function(){
    text = $(this).text;
       :
  });
});


実はこれだけでテキストのフェッチとスクレイピングの両方が実現できてしまっています。URL を指定してその HTML テキストをフェッチします。その結果は $ に入るので、フェッチの結果は jQuery の DOM のように扱うことができます。上例では取得した HTML テキスト内において bld クラスを適用した要素内のテキスト(<xxx class="bld">XXXXX</xxx> となっている箇所の XXXXX 部分)を取得して、変数 text に代入しています。

上記のサンプルで紹介している URL: https://www.google.com/finance/converter?a=1&from=EUR&to=JPY はグーグルの通貨変換ページです(このパラメータの場合、ユーロ(EUR)から日本円(JPY)への変換レートを取得します)。この URL にアクセスすると、以下のような HTML が返って来ます:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
  :
  :
</div>
 
<div id=currency_converter_result>1 EUR = <span class=bld>130.0360 JPY</span>
<input type=submit value="Convert">
</div>
<input type=hidden name=meta value=ei=E1N4WYisKN6O0QS8v4OoBA>
</form>
</body>
</div>
</html>

このようにレート変換した結果は上記赤字部分になるので、ここを取得できればよいことになります。したがって上述のように bld クラスを指定した箇所のテキスト値をスクレイピングすれば(上記の場合であれば "130.0360 JPY" という文字列が)変数 text に代入できる、ということになります。

jQuery スタイルで使えるのがとっても便利!


このブログでも何度か Node.js のネタを扱ってますが、非同期処理に悩まされることが多いので、自分の理解の意味でもまとめておくことにしました。

まず理解の大前提として、Node.js はシングルスレッドで動作するため、いわゆる並列処理はできない仕様になっています。そして非同期に処理を実行することができます。これによって並列処理ができなくても、何かの時間のかかる処理があった場合にその終了を待たずに次の処理に進むことができることを意味しています。 ただ、この辺りがややこしく難解になっていることも事実です。

例えば REST API などの HTTP リクエストを行って、その結果が得られたら、その得られた結果の JSON オブジェクトの値を使って処理をする、などというよくあるケースでもこの問題に直面します。

まずは何が難解なのかを紹介します。REST API だとローカル環境で気軽に試せないので、わざと実行に時間のかかる関数を用意して説明します。

例えばこのような処理を考えてみます:
// test.js

// わざと1秒かけてから、パラメータの2倍の数値を出力する関数
function func1( x ){
  setTimeout( function(){
    console.log( 2 * x );
  }, 1000 );
}

// 初期値を設定して出力
var n0 = 10;
console.log( 'n0 = ' + n0 );

// 初期値を上記関数のパラメータに入れて実行
func1( n0 );

この中では func1 という関数を定義しています。setTimeout を使ってわざと1秒(1000ミリ秒)待ってから、パラメータの値の2倍を画面に出力する、という関数です。 この関数を n0 (=10 に設定)という変数をパラメータにして実行する、というものです。なので "20" という結果が出力されることを期待しています。

この内容を test.js というファイルに保存して、実行してみます(青字部分が出力結果):
$ node test
n0 = 10
20

期待通りに "20" と出力されました。とりあえずここまでは成功です。


さて問題はここからです。上記例では関数 func1 の中で console.log が実行されて出力までを行いました。これを func1 からは与えた数値を2倍した結果を受け取るようにして、func1 の外側で出力するように変更してみます。深く考えずにやるとこんな感じでしょか?
// test.js(注 正しく動きません)

// わざと1秒かけてから、パラメータの2倍の数値を出力する関数
function func1( x ){
  setTimeout( function(){
    console.log( 2 * x );
  }, 1000 );
}

// わざと1秒かけてから、パラメータの2倍の数値を戻す関数
function func2( x ){
  setTimeout( function(){
    return ( 2 * x );
  }, 1000 );
}

// 初期値を設定して出力
var n0 = 10;
console.log( 'n0 = ' + n0 );

// 初期値を上記関数のパラメータに入れて実行し、戻り値を出力
var n1 = func2( n0 );
console.log( 'n1 = ' + n1 );

func1 をほぼコピペして func2 という関数を作りました。console.log の代わりに return にして、値を返すようにしています。呼び出し元からはこの func2 を実行して、得られた結果を出力するようにしています。

これを先程と同様に実行するとこうなります:
$ node test
n0 = 10
n1 = undefined

n0(10)の値の2倍の "20" という結果を期待していたのですが、"undefined" と表示されてしまいます(正確に書くと、上記の2行はすぐに表示されますが、更に1秒くらい経過してから終了します)。この理由は最初に書いたように func2 は非同期に実行されているので、return の行が実行されるまでには1秒かかります。しかしその前に(return の行が実行される前に)関数そのものの処理は終了してしまいます。つまり値が戻る前に(戻っていない値を受け取ることになっている)n1 という変数を出力しているので "undefined" になっているのでした。

このように、期待通りに動かなかった理由は明白なのですが、ではどうすればこの関数が期待通りに動く(1秒後に与えられた結果を戻り値として戻し、受け取った側がその値を出力する)ようにできるでしょうか?これが今日紹介する大きなテーマです。


結論を先に紹介すると、ここで Promise オブジェクトを使って関数を修正し、受け取った側もその変更に合わせて一部書き直す必要があります。具体的には以下のように修正します:
// test.js

// わざと1秒かけてから、パラメータの2倍の数値を出力する関数
function func1( x ){
  setTimeout( function(){
    console.log( 2 * x );
  }, 1000 );
}

// わざと1秒かけてから、パラメータの2倍の数値を戻す関数
function func2( x ){
  return new Promise( function( resolve ){
    setTimeout( function(){
      resolve( 2 * x );
    }, 1000 );
  });
}

// 初期値を設定して出力
var n0 = 10;
console.log( 'n0 = ' + n0 );

// 初期値を上記関数のパラメータに入れて実行し、戻り値を出力
func2( n0 ).then( function( n1 ){
  console.log( 'n1 = ' + n1 );
});

まず関数 func2 側は、Promise オブジェクトを新規に作成します。Promise オブジェクトは処理が成功した場合の関数をパラメータに指定します。上図だと
function( resolve ){
  setTimeout( function(){
    resolve( 2 * x );
  }, 1000 );
}

という関数がパラメータに指定されているので、成功するとこの関数が実行されます(今回は使っていませんが、第二パラメータを指定した場合は失敗時に実行する処理を指定したことになります)。この処理の中で1秒待って、指定したパラメータを2倍して resolve とする、ということになります。

そしてこの関数 func2 を呼び出す側も少し変更が必要になります。 func2() 関数の実行結果をそのまま変数として受け取るのではなく、成功した場合(今回の例だと1秒待って2倍になった値が返された場合)の処理を .then() 内に渡して処理することになります。この then 内の処理で計算結果(resolve で処理された内容)を n1 という変数で受け取って console.log で表示する、という内容にしています。

こうして修正した test.js を実行すると、以下のような結果になります(実際には n0 = 10 がすぐに表示され、1秒くらい待ってから n1 = 20 の行が表示されて終了します):
$ node test
n0 = 10
n1 = 20

Node.js の関数内で非同期処理を実行して、その非同期処理の終了を待って値を受け取るような関数を作る場合は、Promise オブジェクトを使って上記のように記述します、という紹介でした。非同期実行に慣れていないと、この辺りで戸惑うことが多いと感じたので、まとめておきました。


IBM Bluemix からも提供されている IBM の DBaaS サービスである dashDB に Node.js からアクセスする方法を紹介します。実際には dashDB だけでなく、DB2 のサービスやオンプレミスデータベースへも同様に応用できますが、今回は Bluemix 上の DB2/dashDB 関連サービスを例に紹介します:
2017063002


dashDB は行指向/列指向型のテーブルをどちらも作成することができるリレーショナル・データベースのサービスですが、そのデータベースシステムとしての実体は IBM DB2 です。というわけで、このライブラリを使ってアクセスします:
https://www.npmjs.com/package/ibm_db

2017063001


まず以下のコマンドを実行して ibm_db をインストールします(このコマンドだけで DB2 ODBC Driver ごとインストールされます):
$ npm install ibm_db


そして以下のようなコードを用意して dashDB にアクセスします:

(settings.js)
exports.db_host = 'dashdb-entry-yp-XXXXXXXX.services.dal.bluemix.net';
exports.db_port = 50000;
exports.db_name = 'BLUDB';
exports.db_username = 'dashNNNN';
exports.db_password = 'PASSWORD';

(sample.js)
var ibm_db = require( 'ibm_db' );
var settings = require( './settings' );

var db_str = "DATABASE=" + settings.db_name
  + ";HOSTNAME=" + settings.db_host
  + ";UID=" + settings.db_username
  + ";PWD=" + settings.db_password
  + ";PORT=" + settings.db_port
  + ";PROTOCOL=TCPIP";
var sql = "select OBJECTID, NAME from SAMPLES.GEO_CUSTOMER limit 10";

ibm_db.open( db_str, function( err, conn ){
  if( err ) return console.log( err );

  conn.query( sql, function( err, data ){
    if( err ) console.log( err );
    else console.log( data );

    conn.close( function(){
      console.log( 'done.' );
    });
  });
});

settings.js の中身はユーザー名やパスワードといった dashDB に接続するためのサービス資格情報です。IBM Bluemix の画面から取得できる値を使って、実際の値で書き換えて使ってください:

2017063003


アプリケーションの実体は sample.js です。今回の例ではシンプルに接続して、サンプルデータとして GEO_CUSTOMER テーブルから OBJECTID と NAME の値を 10 件だけ取得する、という SQL (青字部分)を実行しました。また settings.js で定義した情報を取り出して接続文字列(赤字部分)を生成しています。

node コマンドで sample.js を実行して、以下のような結果が表示されれば成功です:
$ node sample.js
[ { OBJECTID: 1322, NAME: 'Kami Labarbera' },
  { OBJECTID: 1323, NAME: 'Johnathon Tunney' },
     :
  { OBJECTID: 1587, NAME: 'Althea Alcazar' } ]
done.







 

Node.js の処理内で unzip を実現する方法を紹介します。アップロードなどで zip ファイルを受取って、それをダイナミックに展開して特定のファイルを取り出す、といった仕組みを Node.js で実現する場合に必要な実装の例です。

この仕組みを実現するために、node-unzip という便利なライブラリがあるので、これを使うことにします:
https://www.npmjs.com/package/unzip

2017070601


fs ライブラリと併用して、こんな感じで使います(zip ファイル内の全ファイルを展開する例):
var fs = require( 'fs' );
var unzip = require( 'unzip' );

  :
  :

fs.createReadStream( './uploads/archive.zip' )
    .pipe( unzip.Extract( { path: './tmp/' } ) );

特定のファイルだけ(以下の例では拡張子が ".xml" のものだけ)を展開する場合は以下のようにします:
var fs = require( 'fs' );
var unzip = require( 'unzip' );

  :
  :

fs.createReadStream( './uploads/archive.zip' )
    .pipe( unzip.Parse() )
    .on( 'entry', function( entry ){
      var filename = entry.path;  //. ファイル名
      var type = entry.type;  //. 'Directory' または 'File'
      var size = entry.size;   //. ファイルサイズ

      if( filename.toLowerCase().endsWith( ".xml" ) ){
//. ".xml" で終わるファイル名だった場合のみ展開 entry.pipe( fs.createWriteStream( './tmp/' + filename ) ); }else{ entry.autodrain(); } });

そもそもの元ファイルが zip 圧縮されていたり、大量のファイルデータをアップロードして登録したい場合などは、目的のファイルを zip して、1回でまとめてアップロードできると便利なのですが、この方法であれば受け取った zip を展開して・・・という処理が実現できます。


このページのトップヘ