Build an Instagram clone with AngularJS, Satellizer, Node.js and MongoDB
这一节变得比较难一点,因为它所涵盖的范围很大。我会把它打散成多个代码片段来讲解,在这一节最后给出完整代码。
<!-- lang: js -->
app.post('/auth/instagram', function(req, res) {
});
这个请求 /auth/instagram 由 Satellizer 从弹窗拿到了授权码之后进行。这个处理很快,弹窗会在你能看清楚 query 参数之前把弹窗给关闭。
下面是 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, clientId 和 redirectUri 是从哪里来的了吧:
<!-- 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 的时候,需要上面的所有参数。
<!-- 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 这样的方法,返回 true 或 false 要光比返回一个响应要好。上面的代码里只是简单的检查一下 req.headers.authorization 有没有,而没有检查 token 是不是合法是不是正确格式。
在继续讨论之前,我们来简单过一下这个端点被访问时的场景,当然不包括极端情况,不要忘记本教材的主要目的:
让我们从第二种场景开始,当我们的用户没有登录的情况:
<!-- 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 中的代码的一个概要截图:
我们基本上已经完成后端代码了。只需要再添加另外三个路由,它们比上面这个要简单得多小得多。