有时用户希望当他们不在线时,服务器代表他们向 Facebook 发表帖子。例如,业务页面的所有者可能希望在某款产品的库存不多时发布公告,鼓励顾客在还有货时尽快购买。或者一个人可能希望他的时间表以随机的间隔发布消息。
可以编写一个服务器来实现此目的,但这么做并不容易。在这个 3 教程系列文章中,我将展示如何使用 IBM Bluemix 作为云提供商来实现此目的。本系列还会介绍 MEAN 堆栈所有 4 个组件的基本知识。为了演示此功能,我将展示如何构建一个应用程序,在随机的时间代表用户发表笑话。
获取代码
如果应用程序仅知道来自 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 字符串表示。结果与下面的内容类似:
点击查看代码清单
关闭 [x]
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
。要向用户给予问候,而不是在用户已登录时要求用户登录,可执行以下更改:
myApp.controller
调用改为: myApp.controller("facebookCtrl", function($scope) { // Status of Facebook communications $scope.fbStatus = ""; // Name of the connected person $scope.userName = ""; });
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); };
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); }); }
在 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
是空的,用户将看到登录按钮。但如果它有一个值,用户会看到一个问候语,其中包含用户的名称。
现在,用户信息可供浏览器使用。但是您需要将它存储在服务器上。这需要两个操作:
为此,最简单的方法是创建一个数据库,然后通过 REST 服务来访问数据库。在第 2 步到第 4 步中,您将创建该数据库中。在第 5 步和第 6 步中,您将创建和使用该 REST 服务。
Node.js 常用的 MongoDB 数据库可在 Bluemix 上以一个单独服务的形式来提供。
mongodb-usingfb
。单击 CREATE 。 "dependencies":{
"express":"4.12.x",
"cfenv":"1.0.x",
"mongodb":"*"
},
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
现在您已拥有数据库,下一步是从服务器应用程序连接该数据库。该服务器应用程序的源代码位于 app.js 文件中。
// 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"); });
要验证数据库连接,可将以下代码添加到 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
应始终可用。
可以为浏览器设计您自己的接口来读取和写入用户信息。但为什么要这么麻烦呢?对于这个问题,现在已有一个非常完美的标准: REST 。
"dependencies":{
"express":"4.12.x",
"cfenv":"1.0.x",
"mongodb":"*",
"body-parser":"*"
},
在 app.js 文件中现有的 app.get
调用上方输入以下小步骤(步骤 2 到 6)中代码。新调用被限制到路径 /rest/user 或它之下的路径,而且对于这些路径,调用应覆盖一般的处理函数。注释中已解释了代码的作用。
// 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');
// 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()}); });
// 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 });
// 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 });
$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(); });
用户登录时,浏览器不知道需要创建用户条目,还是更新用户条目。但是,因为更新操作拥有参数 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,我将在本系列的下一篇文章中解释这个难题:“ 让服务器代表用户执行操作。 ”