转载

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

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

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

  • 第 1 部分 介绍如何使用 Facebook 作为登录来源和身份验证机制。
  • 第 2 部分(本教程)介绍如何配置 MongoDB 来存储从 Facebook 获取的用户信息。
  • 第 3 部分 介绍如何使用 Facebook REST API 让服务器充当用户。

构建您的应用程序所需的准备工作

  • 一个 Bluemix 帐户。
  • HTML 和 JavaScript 的知识。
  • 一个 Facebook 帐户。
  • 一个可将 Node.js 应用程序上传到 Bluemix 的开发环境,比如 Eclipse。要使用 Eclipse,请参阅 “ 使用 IBM Eclipse Tools for Bluemix 部署应用程序 ”。

获取代码

第 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 字符串表示。结果与下面的内容类似:

点击查看代码清单

关闭 [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 。要向用户给予问候,而不是在用户已登录时要求用户登录,可执行以下更改:

  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,我将在本系列的下一篇文章中解释这个难题:“ 让服务器代表用户执行操作。 ”

正文到此结束
Loading...