目次


Bluemix と MEAN スタックを使用して自動投稿 Facebook アプリケーションを作成する: パート 2 ユーザー情報をサーバーに保管する

Comments

Facebook ユーザーは、自分がログインしていないときでも、自分の代わりにサーバーから Facebook に投稿させたいと思うことはあるものです。例えば、ビジネス・ページのオーナーが、顧客の購買心をあおるために、特定の製品が限られた数量になった時点で、その旨を通知する投稿を送信するという場合です。あるいは単純に、ユーザーが自分のタイムラインにランダムな間隔でメッセージを投稿したいという場合もあります。

こうしたことを実現するためのサーバーを作成するのは可能ですが、簡単ではありません。3 つのチュートリアルからなるこのシリーズでは、IBM Bluemix をクラウド・プロバイダーとして使用して、サーバーから Facebook に投稿させる方法を紹介します。また、シリーズを通して、MEAN スタックを構成する 4 つすべてのコンポーネントの基本も抑えます。具体的な機能をデモンストレーションするために、ランダムなタイミングでユーザーに代わってジョークを投稿するアプリケーションを作成する方法を紹介します。

  • パート 1 では、Facebook をログイン・ソース兼認証メカニズムとして使用する方法を説明します。
  • パート 2 (このチュートリアル) では、Facebook から取得したユーザー情報を保管するように MongoDB を構成する方法を説明します。
  • パート 3 では、Facebook REST API を使用して、サーバーにユーザーの代理を務めさせる方法を説明します。

アプリケーションを作成するために必要となるもの

ステップ 1. Facebook からユーザー情報を読み取る

ユーザーが認証済みであっても、Facebook で認証済みのいずれかのユーザーであるということしか、アプリケーションが認識していないのであれば、意味がありません。認証を利用するには、ユーザー情報にアクセスできる必要があります。入手できるユーザー情報を確認するには、facebook.js ファイル内の loggedOn 関数に以下のコードを追加します。

// Download the user information.
// Show the response in the status.
FB.api('/me', function(response) {
	setFacebookStatus("User information:" +
		JSON.stringify(response));
});

JSON.stringify 関数は、オブジェクトを JSON ストリングの表現に変換します。その結果は、以下のコードのようになります。

User information:{"id":"10204118527785551","email":"ori@simple-tech.com","first_name":"Ori","gender":"male","last_name":"Pomerantz","link":"https://www.facebook.com/app_scoped_user_id/10204118527785551/","locale":"en_US","name":"Ori Pomerantz","timezone":-5,"updated_time":"2015-01-27T02:52:51+0000","verified":true}

ユーザーの名前は request.name です。すでにユーザーがログインしている場合にログインを要求することなくユーザーを丁重に迎えるようにするには、以下の変更を加えます。

  1. datamodel.js ファイルで、myApp.controller を呼び出している箇所を以下のように変更します。
    myApp.controller("facebookCtrl", function($scope) {
    	// Status of Facebook communications
    	$scope.fbStatus = ";
    	
    	// Name of the connected person
    	$scope.userName = ";
    });
  2. 変更を加えなければならないスコープ変数は、fbStatus だけではありません。スコープ変数に変更を加える作業を容易にするために、setFacebookStatus 関数を以下のコードで置き換えます。
    // This function sets the a scope variable to a value.
    // It is useful to have this function so that the rest of
    // the JavaScript code would be able do this without relying
    // on Angular
    var setScopeVar = function(variable, value) {
    	var scope = angular.element($("#facebookCtrl")).scope();
    	
    	// scope.$apply takes a function because of re-entrancy.
    	// The browser may not be able to handle changes in the
    	// scope variable immediately, in which case the function
    	// will be executed later.
    	scope.$apply(function() {
    		scope[variable] = value;
    	});	
    };
    
    
    var setFacebookStatus = function(status) {
    	setScopeVar("fbStatus", status);
    };
  3. facebook.js ファイルで、loggedOn 関数を以下のように変更します。
    //This function is called when we KNOW the user is logged on.
    function loggedOn() {
    	setFacebookStatus("You're in");
    	
    	FB.api('/me', function(response) {
    		setScopeVar("userName", response.name);
    	});
    }
  4. index.html ファイルで、ログインを求めるコードを以下のように変更します。
    <fb:login-button scope="public_profile,email"
    	onlogin="checkLoginState();" ng-if="userName == ''">
    Login
    </fb:login-button>
    <div ng-if="userName != ''">
    Hello {{userName}}
    </div>

    <fb:login-button> タグと <div> タグの内部では、ng-if 属性を使用していることに注意してください。この属性を使用することで、属性の値として指定された条件が真になる場合にのみ、特定のタグとそのタグ内のコンテンツが表示されるように指定することができます。つまり、userName の値が空の場合には、ユーザーにログイン・ボタンが表示されることになりますが、値が設定されている場合には、ユーザーの名前を使用した歓迎メッセージが表示されます。

    現時点で、ユーザー情報はブラウザーに表示されるようになりましたが、その情報はサーバーに保管しなければなりません。それには、次の 2 つのアクションが必要です。

    1. リクエストをユーザー情報と併せてブラウザーからサーバーに送信します。
    2. そのユーザー情報をデータベースに保管します。

以上の作業を行う最も簡単な方法は、データベースを作成してから、REST サービスを使用して、データベースに保管されたユーザー情報にアクセスできるようにすることです。この後、ステップ 2 からステップ 4 では、データベースを作成します。続くステップ 5ステップ 6 では、REST サービスを作成して使用します。

ステップ 2. MongoDB データベースを構成する

Node.js で通常使用するデータベースは MongoDB です。Bluemix では、MongoDB を個別のサービスとして利用できるようになっています。

  1. Bluemix ダッシュボードにログインします。
  2. 「ADD A SERVICE OR API (サービスまたは API の追加)」をクリックします。
  3. 「Web and Application (Web とアプリケーション)」リストで、「mongodb」をクリックします。
  4. 使用しているアプリケーション・スペースを選択し (複数のスペースにアクセスできる場合)、アプリケーションを選択します。コードをそのままカット・アンド・ペーストできるように、サービス名にはこのチュートリアルで使用する「mongodb-usingfb」を使用することをお勧めします。「CREATE (作成)」をクリックします。
  5. 再ステージングするよう求められたら、「RESTAGE (再ステージ)」をクリックします。
  6. package.json ファイルで、依存関係を以下のように変更して、アプリケーションに MongoDB が必要であることを指定します。新しいテキストは、太字で表示してあります。

    "dependencies": {
           "express": "4.12.x",
           "cfenv": "1.0.x",
           "mongodb": "*"
    },
  7. manifest.yml ファイルで、依存関係を以下のように変更して、アプリケーションで mongodb-usingfb を使用することを指定します。新しいテキストは、太字で表示してあります。
    ---
    applications:
       - disk_quota: 1024M
       host: fb-bluemix2
       name: fb-bluemix2
       path: .
       domain: mybluemix.net
       instances: 1
       memory: 256M
       env: {
       }
       services:
          mongodb-usingfb:
             label: mongodb
             version: '2.4'
             plan: '100'
             provider: core

ステップ 3. MongoDB データベースに接続する

データベースが用意できたので、次は、サーバー・アプリケーションからこのデータベースに接続します。該当するサーバー・アプリケーションのソースは、app.js ファイルにあります。

  1. Cloud Foundry からアプリケーション環境を取得するためのコード行の下に、データベースに接続するための以下のコードを追加します。
    // Find the MongoDB service from the application
    // environment
    var dbInfo = appEnv.getService(/mongodb/);
    
    // If there is no MongoDB service, exit
    if (dbInfo == undefined) {
    	console.log("No MongoDB to use, I am useless without it.");
    	process.exit(-1);
    }
    
    // The variable used to actually connect to the database. It starts
    // as null until gives a usable value in the connect function.
    var userCollection = null;
    
    // Connect to the database. dbInfo.credentials.url contains the user name
    // and password required to connect to the database.
    require('mongodb').connect(dbInfo.credentials.url, function(err, conn) {
    	if (err) {
    		console.log("Cannot connect to database " + dbInfo.credentials.url);
    		console.log(err.stack);
    		process.exit(-2);
    	}
    	
    	console.log("Database OK");
    	
    	// Set the actual variable used to communicate with the database
    	userCollection = conn.collection("users");
    });
  2. アプリケーションに戻ります。変更後のアプリケーションを Bluemix 上で実行するために送信する際には、「Save to manifest file (マニフェスト・ファイルに保存)」を選択してから、サービスを選択するパネルが表示されるまで「Next (次へ)」をクリックします。
  3. サービスを選択するパネルが表示されたら、「mongodb-usingfb」を選択して「Finish (完了)」をクリックします。コンソールに成功を示すメッセージが表示されるのを確認します。

ステップ 4. データベース接続をテストする

データベース接続を確認するには、app.js ファイルに以下のコードを追加します。このコードは、データベースへのデータの挿入と、データベースからのデータの読み取りの両方を試行します。

// Insert data into the collection. If there is an after
// function, call it afterwards
var insertData = function(data, after) {
	
	// If the userCollection is not available yet,
	// wait a second and try again.
	if (userCollection == null) {
		setTimeout(function() {insertData(data, after);}, 1000);
		return ;
	}

	// Insert the data
	userCollection.insert(data, {safe: true}, function(err) {
		if (err) {   // Log errors
			console.log("Insertion error");
			console.log("Data:" + JSON.stringify(data));
			console.log("Stack:");
			console.log(err.stack);
		} else       // If no error, call after();
			if (after != null)
				after();
	});
}

// Read data in the collection, run the perEntry function on
// each entry.
var readData = function(filter, perEntry) {
	// If the userCollection is not available yet,
	// wait a second and try again.
	if (userCollection == null) {
		setTimeout(function() {readData(filter, perEntry);}, 1000);
		return ;
	}		
	// If we're successful, run perEntry on each entry. If not, log
	// that fact.
	userCollection.find(filter, {}, function(err, cursor) {
		if (err) {
			console.log("Search error");
			console.log("Filter:" + JSON.stringify(filter));
			console.log("Stack:");
			console.log(err.stack);			
		} else
			cursor.toArray(function(err, items) {
				for (i=0; i < items.length; i++)
					perEntry(items[i]);
				
			});   // End of cursor.toArray		
	});   // End of userCollection.find	
};    // End of readData

insertData({name: "jack", id: 25}, null);
readData({}, function(entry) {
	console.log("Entry:" + JSON.stringify(entry));
});

このコードで行っているように、userCollection 変数を定期的にポーリングするのは非効率ですが、ここで Node.js のイベント・インフラストラクチャーの代わりにこの方法を使用している唯一の理由は、アプリケーションの起動時にだけポーリングを行うためです。起動後は、userCollection は常に使用可能になります。

ステップ 5. サーバー・サイドに REST サーバーを追加する

ユーザー情報の読み取り/書き込みを行うために、ブラウザーのインターフェースを独自に考案することは可能ですが、わざわざそうする必要があるでしょうか?すでに、そのための申し分のない標準があります。それが、REST です。

  1. package.json ファイルで、依存関係を以下のように変更して、アプリケーションに body-parser パッケージが必要であることを指定します。新しいテキストは、太字で表示されています。このパッケージを使用して、HTTP リクエストの本体を構文解析します。これは、REST リクエストによってエンティティーを作成または更新するために必要なプロセスです。

    "dependencies": {
           "express": "4.12.x",
           "cfenv": "1.0.x",
           "mongodb": "*",
           "body-parser": "*"
    },

    app.js ファイル内で app.get を呼び出している箇所の上に、以降のサブステップ (サブステップ 2 から 6) で記載するコードを入力します。新しい呼び出しは、すべてパス /rest/user またはその配下のパスに対して行うように制限されます。これらのパスに対する呼び出しでは、汎用ハンドラーに変更を加える必要があります。コードの説明は、コメントとしてコードに含めてあります。

  2. 以下のコードを入力します。このコードには、REST インターフェースに必要な定義が含まれています。
    // The CRUD functions (Create, Read, Update, Delete) are
    // implemented under /rest/user
    var restUserPath = "/rest/user";
    
    // The body-parser is necessary for creating new entities
    // or updating existing ones. In both cases, the entity
    // attributes appear as JSON in the HTTP request body.
    var bodyParser = require('body-parser');
  3. 以下のコードを追加します。このコードには、POST リクエストを処理して新規ユーザーを作成する方法が示されています。REST では通常、POST リクエストは作成される新規エンティティーの ID を返しますが、この例では Facebook ID を使用しているため、その必要はありません。
    // Create is implemented in REST as HTTP Post, without the
    // ID (usually the client won't know the ID in advance,
    // although in this case it does).
    app.post(restUserPath, bodyParser.json(), function(req, res) {
    	var userData = req.body; 	// bodyParser.json() takes care
    								// of parsing the request
    	console.log("Trying to add user: " + JSON.stringify(userData));
    	
    	// After inserting the data, call res.send() to send an
    	// empty response to the client, which is interpreted as
    	// "operation successful".
    	//
    	// This is demonstration code. In production code you
    	// need to add more intelligent error handling than
    	// pretending they never happen.
    	insertData(userData, function() { res.send()});
    });
  4. 以下のコードを追加します。このコードは、すべてのユーザーに関する情報のリクエストを処理するコードです。
    // GETting restUserPath gives a list of users with their
    // full information. In production code you would limit
    // the query size.
    app.get(restUserPath, function(req, res) {
    	userCollection.find({}, // Empty filter for all users,
    						{}, // No options
    						function(err, cursor) {
    		if (err) {
    			console.log("Search error in getting the whole list");
    			console.log("Stack:");
    			console.log(err.stack);
    
    			// Respond to avoid getting the request forwarded to the
    			// next handler.
    			res.send();
    			
    		} else
    			cursor.toArray(function(err, items) {
    				// Send the item array.
    				res.send(items);
    			});   // End of cursor.toArray		
    	});   // End of userCollection.find		
    });
  5. 以下のコードを追加します。このコードは、特定のユーザーに関する情報のリクエストを処理するためのコードです。ここで初めて URL の一部にユーザー ID を含めます。
    // GETting restUserPath/<id> gives all the information
    // about the user with that id. The :id means that the
    // string that matches it will be available in the
    // request as req.params.id.
    app.get(restUserPath + "/:id", function(req, res) {
    	userCollection.find({"id": req.params.id},  
    						{}, // No options
    						function(err, cursor) {
    		if (err) {
    			console.log("Search error in getting a single item");
    			console.log("Stack:");
    			console.log(err.stack);	
    
    			// Respond to avoid getting the request forwarded to the
    			// next handler.
    			res.send();
    		
    		} else
    			cursor.toArray(function(err, items) {
    				res.send(items[0]);
    			});   // End of cursor.toArray		
    	});   // End of userCollection.find		
    });
  6. 以下のコードを追加します。このコードは、更新や削除を行うためのコードです。MongoDB では、ドキュメント内のフィールドを変更することができます (MondoDB でのドキュメントとは、大雑把に言えば、連想配列を意味します)。それには、$set パラメーター名を使用し、フィールドとそれぞれの新しい値からなる連想配列をパラメーター値として設定します。
    // PUT is used to update existing entries.
    app.put(restUserPath + "/:id", bodyParser.json(), function(req, res) {
    	// In a MongoDB update, you can use the command $set followed
    	// by an associative array of all the fields you wish to set and
    	// their new values.
    	userCollection.update({"id": req.params.id}, {$set: req.body},
    		{upsert: true});
    	
    	res.send();
    });
    
    
    
    // DELETE, logically enough, deletes a user
    app.delete(restUserPath + "/:id", function(req, res) {
    	userCollection.remove({"id": req.params.id});
    	
    	res.send();
    });

ステップ 6. クライアント・サイド (ブラウザー) に REST クライアント呼び出しを追加する

ブラウザーは、ユーザーがログインした時点では、ユーザー・エントリーを作成する必要があるのか、それともユーザー・エントリーを更新する必要があるのかを認識していません。ただし、更新操作にはパラメーター upsert: true が設定されるので、その場合には必ずエントリーを更新するようにします。このパラメーターが存在しない場合は、エントリーを作成します。

この情報を送信するために、facebook.js ファイルを編集し、loggedOn に変更を加えて新しい関数 putUserInfo を以下のように追加します。

var loggedOn = function() {
	setFacebookStatus("You're in");
	
	FB.api('/me', function(response) {
		setScopeVar("userName", response.name);

		// Only send the information we want to store
		putUserInfo({id: response.id,
			name: response.name,
			email: response.email
		});
	});
}

// This function PUTs the user information to the server
var putUserInfo = function(userInfo) {
	// The URL. A relative URL so we don't have to
	// figure out the host we came from.
	var url = "rest/user/" + userInfo.id;
	
	// $ is a variable that holds jQuery functions.
	// AJAX is asynchronous Javascript and XML,
	// which is used to communicate with servers
	$.ajax({
		
		// The HTTP verb we use
		type: "PUT",
		
		// Use JSON (rather than XML)
		contentType: "application/json; charset=utf-8",
		url: url,
		data: JSON.stringify(userInfo),
		
		// Function called in case this is successful
		success: function(msg) {
			;  // If we wanted to report success
		},
		
		// Function called in case this fails
		error: function(msg) {
			alert("Problem with user information:" + msg);			
		}
	});
}

セキュリティーに関する考慮事項

現時点でこのアプリケーションを攻撃しようとすると、ユーザーの Facebook ID さえわかれば、ユーザーの情報を読み取って変更できてしまいます。幸い、Facebook が提供するユーザー ID は、このアプリケーションに固有のものですが、ID が不正に使用されないようにするには、ID を隠す必要があります。それには、app.js ファイル内の最初の app.get 関数を以下のコードに変更して、2 番目の app.get 関数 (ユーザー ID を必要とする関数) をコメントアウトします。

// GETting restUserPath gives a list of users with their
// information. In production code you would limit
// the query size.
app.get(restUserPath, function(req, res) {
	userCollection.find({}, // Empty filter for all users,
						{}, // No options
						function(err, cursor) {
		if (err) {
			console.log("Search error in getting the whole list");
			console.log("Stack:");
			console.log(err.stack);		
			
			// Respond to avoid getting the request forwarded to the
			// next handler.
			res.send();
		} else
			cursor.toArray(function(err, items) {
				// items array, but limited to the
				// information we are willing to send
				var censored = new Array(items.length);
				
				// Only send the users' names
				for (var i=0; i<items.length; i++)
					censored[i] = {
						name: items[i].name
					};
				
				// Send the censored array.
				res.send(censored);
			});   // End of cursor.toArray		
	});   // End of userCollection.find		
});

Facebook から提供されるユーザー ID は、Facebook とサーバーが所有する共有シークレットです。ただし、ユーザー ID は、ブラウザー・アプリケーションを認証するエンティティーであるため、Facebook からブラウザーに送信しなければなりません。ユーザー ID がサーバーからブラウザーに送信されるとなると、認証が行われないにも関わらず、攻撃者がユーザー ID を入手する手段が出てきてしまうはずです。攻撃者はクライアント・サイドのコードを入手できるため、クライアントとサーバーとの間のプロトコルの内容を模倣できてしまう、ということを忘れないでください。

まとめ

これで、ユーザー情報はサーバーに保管されるようになりました。このアプリケーションに唯一欠けているのは、実際にユーザー情報を使用して Facebook を制御する方法です。このシリーズの次回のチュートリアル「サーバーにユーザーの代理を務めさせる」で、その方法を説明します。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Cloud computing, Web development
ArticleID=1012675
ArticleTitle=Bluemix と MEAN スタックを使用して自動投稿 Facebook アプリケーションを作成する: パート 2 ユーザー情報をサーバーに保管する
publish-date=08202015