我开始用node.js,express和mongodb规划一个REST API。API为网站(公共区域和私人区域)提供数据,以后可能还会为移动应用程序提供数据。前端将使用AngularJS开发。
最近几天,我读了很多关于保护REST api的文章,但我没有找到一个最终的解决方案。就我所理解的是使用HTTPS来提供基本的安全性。但是我如何在用例中保护API:
只有网站/应用程序的访问者/用户被允许获取网站/应用程序公共区域的数据
仅允许经过身份验证和授权的用户获取私有区域的数据(并且仅允许用户授予权限的数据)
目前,我只考虑允许具有活动会话的用户使用API。为了授权用户,我将使用护照和许可,我需要为自己实现一些东西。都在HTTPS之上。
有人能提供一些最佳实践或经验吗?我的“架构”是否存在缺陷?
我也遇到过你描述的同样的问题。我正在建设的网站可以从手机和浏览器访问,所以我需要一个api,让用户注册,登录和做一些特定的任务。此外,我需要支持可伸缩性,相同的代码在不同的进程/机器上运行。
因为用户可以创建资源(也就是POST/PUT动作),所以你需要保护你的api。您可以使用oauth,也可以构建自己的解决方案,但请记住,如果密码很容易被发现,那么所有的解决方案都可能被破坏。其基本思想是使用用户名、密码和令牌(即apitoken)对用户进行身份验证。这个apitoken可以使用node-uuid生成,密码可以使用pbkdf2进行散列
Then, you need to save the session somewhere. If you save it in memory in a plain object, if you kill the server and reboot it again the session will be destroyed. Also, this is not scalable. If you use haproxy to load balance between machines or if you simply use workers, this session state will be stored in a single process so if the same user is redirected to another process/machine it will need to authenticate again. Therefore you need to store the session in a common place. This is typically done using redis.
当用户通过身份验证(用户名+密码+apitoken)时,为会话生成另一个令牌,即accesstoken。同样,使用node-uuid。向用户发送accesstoken和userid。userid (key)和accesstoken (value)存储在带有和过期时间的redis中,例如1h。
现在,每当用户使用其余api执行任何操作时,它都需要发送userid和accesstoken。
如果你允许用户使用其余api注册,你需要创建一个admin apitoken帐户,并将其存储在移动应用程序中(加密用户名+密码+apitoken),因为新用户注册时不会有apitoken。
web也使用这个api,但你不需要使用apitokens。您可以在redis存储中使用express,也可以使用上述相同的技术,但可以绕过apitoken检查,并在cookie中向用户返回userid+accesstoken。
如果您有私有区域,则在验证时将用户名与允许的用户进行比较。您还可以为用户应用角色。
简介:
另一种没有apitoken的方法是使用HTTPS,在授权头中发送用户名和密码,并在redis中缓存用户名。
我愿意将此代码作为提出的问题的结构性解决方案,根据(我希望如此)公认的答案。(你可以很容易地自定义它)。
// ------------------------------------------------------
// server.js
// .......................................................
// requires
var fs = require('fs');
var express = require('express');
var myBusinessLogic = require('../businessLogic/businessLogic.js');
// .......................................................
// security options
/*
1. Generate a self-signed certificate-key pair
openssl req -newkey rsa:2048 -new -nodes -x509 -days 3650 -keyout key.pem -out certificate.pem
2. Import them to a keystore (some programs use a keystore)
keytool -importcert -file certificate.pem -keystore my.keystore
*/
var securityOptions = {
key: fs.readFileSync('key.pem'),
cert: fs.readFileSync('certificate.pem'),
requestCert: true
};
// .......................................................
// create the secure server (HTTPS)
var app = express();
var secureServer = require('https').createServer(securityOptions, app);
// ------------------------------------------------------
// helper functions for auth
// .............................................
// true if req == GET /login
function isGETLogin (req) {
if (req.path != "/login") { return false; }
if ( req.method != "GET" ) { return false; }
return true;
} // ()
// .............................................
// your auth policy here:
// true if req does have permissions
// (you may check here permissions and roles
// allowed to access the REST action depending
// on the URI being accessed)
function reqHasPermission (req) {
// decode req.accessToken, extract
// supposed fields there: userId:roleId:expiryTime
// and check them
// for the moment we do a very rigorous check
if (req.headers.accessToken != "you-are-welcome") {
return false;
}
return true;
} // ()
// ------------------------------------------------------
// install a function to transparently perform the auth check
// of incoming request, BEFORE they are actually invoked
app.use (function(req, res, next) {
if (! isGETLogin (req) ) {
if (! reqHasPermission (req) ){
res.writeHead(401); // unauthorized
res.end();
return; // don't call next()
}
} else {
console.log (" * is a login request ");
}
next(); // continue processing the request
});
// ------------------------------------------------------
// copy everything in the req body to req.body
app.use (function(req, res, next) {
var data='';
req.setEncoding('utf8');
req.on('data', function(chunk) {
data += chunk;
});
req.on('end', function() {
req.body = data;
next();
});
});
// ------------------------------------------------------
// REST requests
// ------------------------------------------------------
// .......................................................
// authenticating method
// GET /login?user=xxx&password=yyy
app.get('/login', function(req, res){
var user = req.query.user;
var password = req.query.password;
// rigorous auth check of user-passwrod
if (user != "foobar" || password != "1234") {
res.writeHead(403); // forbidden
} else {
// OK: create an access token with fields user, role and expiry time, hash it
// and put it on a response header field
res.setHeader ('accessToken', "you-are-welcome");
res.writeHead(200);
}
res.end();
});
// .......................................................
// "regular" methods (just an example)
// newBook()
// PUT /book
app.put('/book', function (req,res){
var bookData = JSON.parse (req.body);
myBusinessLogic.newBook(bookData, function (err) {
if (err) {
res.writeHead(409);
res.end();
return;
}
// no error:
res.writeHead(200);
res.end();
});
});
// .......................................................
// "main()"
secureServer.listen (8081);
这个服务器可以用curl测试:
echo "---- first: do login "
curl -v "https://localhost:8081/login?user=foobar&password=1234" --cacert certificate.pem
# now, in a real case, you should copy the accessToken received before, in the following request
echo "---- new book"
curl -X POST -d '{"id": "12341324", "author": "Herman Melville", "title": "Moby-Dick"}' "https://localhost:8081/book" --cacert certificate.pem --header "accessToken: you-are-welcome"