AngularJS+Satellizer+Node.js+MongoDB->Instagram-14

Build an Instagram clone with AngularJS, Satellizer, Node.js and MongoDB

14.Instagram 认证的 Express 路由

这一节变得比较难一点,因为它所涵盖的范围很大。我会把它打散成多个代码片段来讲解,在这一节最后给出完整代码。

<!-- lang: js -->
app.post('/auth/instagram', function(req, res) {

});

这个请求 /auth/instagram 由 Satellizer 从弹窗拿到了授权码之后进行。这个处理很快,弹窗会在你能看清楚 query 参数之前把弹窗给关闭。

https://hackhands.com/wp-content/uploads/2014/10/Screenshot-2014-10-27-23.43.11.png

下面是 Satellizer 的实现代码,那个 defaults.url 就是我们在 client/app.js 中指定的 http://localhost:3000/auth/instagram

<!-- lang: js -->
oauth2.exchangeForToken = function(oauthData, userData) {
  var data = angular.extend({}, userData, {
    code: oauthData.code,
    clientId: defaults.clientId,
    redirectUri: defaults.redirectUri
  });

  return $http.post(defaults.url, data);
};

所以,现在你知道那些你在这个路由中看到的 code, clientIdredirectUri 是从哪里来的了吧:

<!-- lang: js -->
app.post('/auth/instagram', function(req, res) {
  var accessTokenUrl = 'https://api.instagram.com/oauth/access_token';

  var params = {
    client_id: req.body.clientId,
    redirect_uri: req.body.redirectUri,
    client_secret: config.clientSecret,
    code: req.body.code,
    grant_type: 'authorization_code'
  };

});

在拿到 authorization code 之后,下一步就是把它换成 access token,有了令牌才能获取用户的照片或者给用户点赞。

Instagram 当它发送 POST 请求到 https://api.instagram.com/oauth/access_token 的时候,需要上面的所有参数。

https://hackhands.com/wp-content/uploads/2014/11/Screenshot-2014-11-09-21.16.20.png

<!-- lang: js -->
app.post('/auth/instagram', function(req, res) {
  var accessTokenUrl = 'https://api.instagram.com/oauth/access_token';

  var params = {
    client_id: req.body.clientId,
    redirect_uri: req.body.redirectUri,
    client_secret: config.clientSecret,
    code: req.body.code,
    grant_type: 'authorization_code'
  };

 // Step 1. Exchange authorization code for access token.
 request.post({ url: accessTokenUrl, form: params, json: true }, function(e, r, body) {
   // Step 2a. Link user accounts.
   if (req.headers.authorization) {

   } else { // Step 2b. Create a new user account or return an existing one.

   }
 });
});

注意: 在实际生产环境中,类似 isAuthenticated 这样的方法,返回 truefalse 要光比返回一个响应要好。上面的代码里只是简单的检查一下 req.headers.authorization 有没有,而没有检查 token 是不是合法是不是正确格式。

在继续讨论之前,我们来简单过一下这个端点被访问时的场景,当然不包括极端情况,不要忘记本教材的主要目的:

  • 用户没有登录。
  • 是不是回头客?
    • YES: 返回令牌和用户对象。
    • NO: 用 Instagram 的个人资料创建一个新的用户。
  • 通过 email 登录的用户。
  • 是否有关联的 Instagram ID 的账号?
    • YES: 合并两个账号。Instagram 账号优先。删除 Email 账号。
    • NO: 把 Instagram 和当前的 email 连接起来。

让我们从第二种场景开始,当我们的用户没有登录的情况:

<!-- lang: js -->
app.post('/auth/instagram', function(req, res) {
  var accessTokenUrl = 'https://api.instagram.com/oauth/access_token';

  var params = {
    client_id: req.body.clientId,
    redirect_uri: req.body.redirectUri,
    client_secret: config.clientSecret,
    code: req.body.code,
    grant_type: 'authorization_code'
  };

  // Step 1. Exchange authorization code for access token.
  request.post({ url: accessTokenUrl, form: params, json: true }, function(e, r, body) {
    // Step 2a. Link user accounts.
    if (req.headers.authorization) {

    } else { // Step 2b. Create a new user account or return an existing one.
      User.findOne({ instagramId: body.user.id }, function(err, existingUser) {
        if (existingUser) {
          var token = createToken(existingUser);
          return res.send({ token: token, user: existingUser });
        }

        var user = new User({
          instagramId: body.user.id,
          username: body.user.username,
          fullName: body.user.full_name,
          picture: body.user.profile_picture,
          accessToken: body.access_token
        });

        user.save(function() {
          var token = createToken(user);
          res.send({ token: token, user: user });
        });
      });
    }
  });
});

就像我上面所解释的那样,首先检查用户是不是已经注册。我们显然不希望每次他们点注册的时候就创建一个新的账号。

接下来的代码片段是用户已经登录的场景,进入了 if (req.headers.authorization) 部分的时候,我必须把它分开来写,要不然就很难跟进了。

<!-- lang: js -->
User.findOne({ instagramId: body.user.id }, function(err, existingUser) {

  var token = req.headers.authorization.split(' ')[1];
  var payload = jwt.decode(token, config.tokenSecret);

  User.findById(payload.sub, '+password', function(err, localUser) {
    if (!localUser) {
      return res.status(400).send({ message: 'User not found.' });
    }

    // Merge two accounts.
    if (existingUser) {

      existingUser.email = localUser.email;
      existingUser.password = localUser.password;

      localUser.remove();

      existingUser.save(function() {
        var token = createToken(existingUser);
        return res.send({ token: token, user: existingUser });
      });

    } else {
      // Link current email account with the Instagram profile information.
      localUser.instagramId = body.user.id;
      localUser.username = body.user.username;
      localUser.fullName = body.user.full_name;
      localUser.picture = body.user.profile_picture;
      localUser.accessToken = body.access_token;

      localUser.save(function() {
        var token = createToken(localUser);
        res.send({ token: token, user: localUser });
      });

    }
  });
});

这不是我些过的那些漂亮代码,不过即使我用一些诸如 async.js waterfall 的方法来处理这些回调,也不会让代码看起来更简单。理想情况下,如果能把这条路由应该拆分成多个方法,就能降低代码行数,会让它更具有可读性。而如果你的关注的重点是测试,那你应该会很期望把这条路径拆开来。

我们首先要检查的是,我们有没有一个账户的 Instagram ID 和给出来的一样,不过我们一直到 第 12 行 才开始做这件事情。然后解码令牌,拿到包含了用户 id 的用户文档类,为了便于理解我们叫它 localUser。如果你用过 Passport 的 LocalStrategy,你一定会理解为什么我会叫它 localUser。 Ultimatelly, we need to differentiate two users somehow since we have two nested Mongoose queries on the same User model.

接下来,如果如果找到和给出的 Instagram ID 一样的账号的话,把 email 和 password 追加进去,用以避免两个账号。 Email 账号就删掉了,以免造成同一用户有多个账号。记住,email 字段有设置 unique: true,所以你不能有两个同样的 email 的 MongoDB 文档存在。

账号合并部分可变性就大了。如果你除了 email 和 password 字段意外还有别的需要合并的话,你必须自己去编辑。或者说你希望从另一种角度合并,用新账号覆盖老账号,删除老账号。外观上,他们可能看起来是一样的,但是他们会有不同的 ObjectID 。合并账号的做法有各种各样,这里讲的只是其中一种。

如果第一次用 Instagram 注册,我们从发送到 https://api.instagram.com/oauth/access_token 的 POST 请求中,给现存 email 账号可以拿一个新的 Instagram 用户信息。

注意: 许多 OAuth 供应商会让你访问一个指定的端点来获取用户的基本信息,比如说 Facebook, GitHub, Google 和其他许多供应商。而在 Instagram 中不是;这样挺好的,我就不用在我的路由里面再嵌入另外一个回调,要不然我就拿不到了。

下面是用 Instagram 认证路由的完整代码:

<!-- lang: js -->
app.post('/auth/instagram', function(req, res) {
  var accessTokenUrl = 'https://api.instagram.com/oauth/access_token';

  var params = {
    client_id: req.body.clientId,
    redirect_uri: req.body.redirectUri,
    client_secret: config.clientSecret,
    code: req.body.code,
    grant_type: 'authorization_code'
  };

  // Step 1. Exchange authorization code for access token.
  request.post({ url: accessTokenUrl, form: params, json: true }, function(error, response, body) {

    // Step 2a. Link user accounts.
    if (req.headers.authorization) {

      User.findOne({ instagramId: body.user.id }, function(err, existingUser) {

        var token = req.headers.authorization.split(' ')[1];
        var payload = jwt.decode(token, config.tokenSecret);

        User.findById(payload.sub, '+password', function(err, localUser) {
          if (!localUser) {
            return res.status(400).send({ message: 'User not found.' });
          }

          // Merge two accounts.
          if (existingUser) {

            existingUser.email = localUser.email;
            existingUser.password = localUser.password;

            localUser.remove();

            existingUser.save(function() {
              var token = createToken(existingUser);
              return res.send({ token: token, user: existingUser });
            });

          } else {
            // Link current email account with the Instagram profile information.
            localUser.instagramId = body.user.id;
            localUser.username = body.user.username;
            localUser.fullName = body.user.full_name;
            localUser.picture = body.user.profile_picture;
            localUser.accessToken = body.access_token;

            localUser.save(function() {
              var token = createToken(localUser);
              res.send({ token: token, user: localUser });
            });

          }
        });
      });
    } else {
      // Step 2b. Create a new user account or return an existing one.
      User.findOne({ instagramId: body.user.id }, function(err, existingUser) {
        if (existingUser) {
          var token = createToken(existingUser);
          return res.send({ token: token, user: existingUser });
        }

        var user = new User({
          instagramId: body.user.id,
          username: body.user.username,
          fullName: body.user.full_name,
          picture: body.user.profile_picture,
          accessToken: body.access_token
        });

        user.save(function() {
          var token = createToken(user);
          res.send({ token: token, user: user });
        });
      });
    }
  });
});

下面是我们到目前为止,在 server.js 中的代码的一个概要截图:

https://hackhands.com/wp-content/uploads/2014/11/Screenshot-2014-11-09-23.04.06.png

我们基本上已经完成后端代码了。只需要再添加另外三个路由,它们比上面这个要简单得多小得多。

你可能感兴趣的:(AngularJS,express,nodejs,node,node.js,OAuth,oauth2,OAuthn,Satellizer)