内容


使用 Bluemix 和 MEAN 堆栈构建自助发表 Facebook 信息的应用程序,第 2 部分:将用户信息存储在服务器上

Comments

有时用户希望当他们不在线时,服务器代表他们向 Facebook 发表帖子。例如,业务页面的所有者可能希望在某款产品的库存不多时发布公告,鼓励顾客在还有货时尽快购买。或者一个人可能希望他的时间表以随机的间隔发布消息。

可以编写一个服务器来实现此目的,但这么做并不容易。在这个 3 教程系列文章中,我将展示如何使用 IBM Bluemix 作为云提供商来实现此目的。本系列还会介绍 MEAN 堆栈所有 4 个组件的基本知识。为了演示此功能,我将展示如何构建一个应用程序,在随机的时间代表用户发表笑话。

  • 第 1 部分 介绍如何使用 Facebook 作为登录来源和身份验证机制。
  • 第 2 部分(本教程)介绍如何配置 MongoDB 来存储从 Facebook 获取的用户信息。
  • 第 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 是空的,用户将看到登录按钮。但如果它有一个值,用户会看到一个问候语,其中包含用户的名称。

    现在,用户信息可供浏览器使用。但是您需要将它存储在服务器上。这需要两个操作:

    1. 从浏览器向服务器发送一条包含该信息的请求。
    2. 将信息存储在一个数据库中。

为此,最简单的方法是创建一个数据库,然后通过 REST 服务来访问数据库。在 第 2 步第 4 步 中,您将创建该数据库中。在 第 5 步第 6 步 中,您将创建和使用该 REST 服务。

第 2 步. 配置一个 MongoDB 数据库

Node.js 常用的 MongoDB 数据库可在 Bluemix 上以一个单独服务的形式来提供。

  1. 登录 Bluemix 仪表板。
  2. 单击 ADD A SERVICE OR API 磁贴。
  3. 在 Web and Application 列表中,单击 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 请求会返回所创建的新条目的标识符,但在本例中,我使用 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. 添加以下代码,处理对一个特定用户信息的请求。这是您第一次包含用户 ID 作为 URL 的一部分。
    // 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 允许您修改文档中的字段(在 MongoDB 中,大体来讲这表示一个关联数组),方法是用 $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 被滥用,您需要隐藏它。为此,将 app.js 文件中的第一个 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 和服务器都拥有它。但是,需要将它从 Facebook 传输到浏览器,因为要使用该条目向浏览器应用程序执行验证。如果它来自服务器,攻击者就能通过某种途径获取它,尽管攻击者没有被验证,情况也是如此。记住,客户端代码可供攻击者使用,所以他们可在客户端和服务器之间的协议中模仿任何角色。

结束语

您现在已将用户信息存储在服务器上。这个谜题中剩下的唯一部分就是如何实际使用该信息来控制 Facebook,我将在本系列的下一篇文章中解释这个难题:“让服务器代表用户执行操作。


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Cloud computing, Web development
ArticleID=1010819
ArticleTitle=使用 Bluemix 和 MEAN 堆栈构建自助发表 Facebook 信息的应用程序,第 2 部分:将用户信息存储在服务器上
publish-date=07132015