Meteor 中服务器端路由的身份验证
Authentication on Server side routes in Meteor
为服务器端路由验证用户的最佳方式(最安全和最简单)是什么?
Software/Versions
我正在使用最新的 Iron Router 1.* 和 Meteor 1.* 开始,我只使用 accounts-password.
参考代码
我有一个简单的服务器端路由,可以将 pdf 呈现到屏幕上:
both/routes.js
Router.route('/pdf-server', function() {
var filePath = process.env.PWD + "/server/.files/users/test.pdf";
console.log(filePath);
var fs = Npm.require('fs');
var data = fs.readFileSync(filePath);
this.response.write(data);
this.response.end();
}, {where: 'server'});
例如,我想做一些接近 this SO answer suggested 的事情:
在服务器上:
var Secrets = new Meteor.Collection("secrets");
Meteor.methods({
getSecretKey: function () {
if (!this.userId)
// check if the user has privileges
throw Meteor.Error(403);
return Secrets.insert({_id: Random.id(), user: this.userId});
},
});
然后在客户端代码中:
testController.events({
'click button[name=get-pdf]': function () {
Meteor.call("getSecretKey", function (error, response) {
if (error) throw error;
if (response)
Router.go('/pdf-server');
});
}
});
但即使我以某种方式让这个方法起作用,我仍然容易受到用户的攻击,只要输入像'/pdf-server'这样的URL,除非路由本身以某种方式检查了秘密collection对吗?
在Route中,我可以获取请求,并且不知何故获取了header信息?
Router.route('/pdf-server', function() {
var req = this.request;
var res = this.response;
}, {where: 'server'});
然后从客户端通过 HTTP header 传递令牌,然后在路由中检查来自 Collection?
的令牌是否正确
由于服务器端路由充当简单的 REST 端点,因此它们无权访问用户身份验证数据(例如,它们无法调用 Meteor.user()
)。因此,您需要设计一个替代的身份验证方案。实现此目的最直接的方法是使用某种形式的密钥交换,如 here and here.
所讨论
示例实现:
server/app.js
// whenever the user logs in, update her apiKey
Accounts.onLogin(function(info) {
// generate a new apiKey
var apiKey = Random.id();
// add the apiKey to the user's document
Meteor.users.update(info.user._id, {$set: {apiKey: apiKey}});
});
// auto-publish the current user's apiKey
Meteor.publish(null, function() {
return Meteor.users.find(this.userId, {fields: {apiKey: 1}});
});
lib/routes.js
// example route using the apiKey
Router.route('/secret/:apiKey', {name: 'secret', where: 'server'})
.get(function() {
// fetch the user with this key
// note you may want to add an index on apiKey so this is fast
var user = Meteor.users.findOne({apiKey: this.params.apiKey});
if (user) {
// we have authenticated the user - do something useful here
this.response.statusCode = 200;
return this.response.end('ok');
} else {
// the key is invalid or not provided so return an error
this.response.statusCode = 403;
return this.response.end('not allowed');
}
});
client/app.html
<template name="myTemplate">
{{#with currentUser}}
<a href="{{pathFor route='secret'}}">secret</a>
{{/with}}
</template>
备注
使 /secret
只能通过 HTTPS 访问。
虽然请求 /secret
的用户很可能当前已连接,但不能保证她是。用户可能已经登录,复制了她的密钥,关闭了选项卡,并在稍后的某个时间发起了请求。
这是一种简单的用户认证方式。如果服务器路由显示高价值数据(SSN、信用卡等),我会探索更复杂的机制(参见上面的链接)。
有关从服务器发送静态内容的详细信息,请参阅 this question。
除了使用 url 令牌作为其他答案外,您还可以使用 cookies:
添加一些允许您设置 cookie 并在服务器端读取它们的包:
meteor add mrt:cookies thepumpinglemma:cookies
然后你可以使用一些东西将 cookie 与你的登录状态同步
客户端
Tracker.autorun(function() {
//Update the cookie whenever they log in or out
Cookie.set("meteor_user_id", Meteor.userId());
Cookie.set("meteor_token", localStorage.getItem("Meteor.loginToken"));
});
服务器端
在服务器端你只需要检查这个cookie是否有效(使用铁路由器)
Router.route('/somepath/:fileid', function() {
//Check the values in the cookies
var cookies = new Cookies( this.request ),
userId = cookies.get("meteor_user_id") || "",
token = cookies.get("meteor_token") || "";
//Check a valid user with this token exists
var user = Meteor.users.findOne({
_id: userId,
'services.resume.loginTokens.hashedToken' : Accounts._hashLoginToken(token)
});
//If they're not logged in tell them
if(!user) return this.response.end("Not allowed");
//Theyre logged in!
this.response.end("You're logged in!");
}, {where:'server'});
我认为我有一个安全且简单的解决方案可以在 IronRouter.route() 中执行此操作。必须使用 header 中的有效用户 ID 和身份验证令牌发出请求。我从 Router.route() 中调用此函数,然后它让我可以访问 this.user,或者如果身份验证失败则以 401 响应:
// Verify the request is being made by an actively logged in user
// @context: IronRouter.Router.route()
authenticate = ->
// Get the auth info from header
userId = this.request.headers['x-user-id']
loginToken = this.request.headers['x-auth-token']
// Get the user from the database
if userId and loginToken
user = Meteor.users.findOne {'_id': userId, 'services.resume.loginTokens.token': loginToken}
// Return an error if the login token does not match any belonging to the user
if not user
respond.call this, {success: false, message: "You must be logged in to do this."}, 401
// Attach the user to the context so they can be accessed at this.user within route
this.user = user
// Respond to an HTTP request
// @context: IronRouter.Router.route()
respond = (body, statusCode=200, headers) ->
this.response.statusCode statusCode
this.response.setHeader 'Content-Type', 'text/json'
this.response.writeHead statusCode, headers
this.response.write JSON.stringify(body)
this.response.end()
客户这样说:
Meteor.startup ->
HTTP.get "http://yoursite.com/pdf-server",
headers:
'X-Auth-Token': Accounts._storedLoginToken()
'X-User-Id': Meteor.userId()
(error, result) -> // This callback triggered once http response received
console.log result
这段代码很大程度上受到了 RestStop 和 RestStop2 的启发。它是用于在 Meteor 0.9.0+(构建于 Iron Router 之上)中编写 REST API 的 meteor 包的一部分。您可以在此处查看完整的源代码:
由于 Meteor 不使用会话 cookie,客户端在向服务器路由发出 HTTP 请求时必须明确包含某种用户标识。
最简单的方法是在 URL 的查询字符串中传递 userId。显然,您还需要添加一个安全令牌,以证明用户确实是他们声称的那个人。可以通过 Meteor 方法获取此令牌。
Meteor 本身不提供这种机制,因此您需要一些自定义实现。我写了一个名为 mhagmajer:server-route
的 Meteor 包,它已经过全面测试。您可以在这里了解更多信息:https://blog.hagmajer.com/server-side-routing-with-authentication-in-meteor-6625ed832a94
我坚信使用 HTTP headers 是解决此问题的最佳方法,因为它们很简单,不需要弄乱 cookie 或开发新的身份验证方案。
我喜欢@kahmali 的回答,所以我写它是为了与 WebApp 和一个简单的 XMLHttpRequest 一起工作。这已经在 Meteor 1.6 上进行了测试。
客户端
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
// Skipping ahead to the upload logic
const xhr = new XMLHttpRequest();
const form = new FormData();
// Add files
files.forEach((file) => {
form.append(file.name,
// So BusBoy sees as file instead of field, use Blob
new Blob([file.data], { type: 'text/plain' })); // w/e your mime type is
});
// XHR progress, load, error, and readystatechange event listeners here
// Open Connection
xhr.open('POST', '/path/to/upload', true);
// Meteor authentication details (must happen *after* xhr.open)
xhr.setRequestHeader('X-Auth-Token', Accounts._storedLoginToken());
xhr.setRequestHeader('X-User-Id', Meteor.userId());
// Send
xhr.send(form);
服务器
import { Meteor } from 'meteor/meteor';
import { WebApp } from 'meteor/webapp';
import { Roles } from 'meteor/alanning:roles'; // optional
const BusBoy = require('connect-busboy');
const crypto = require('crypto'); // built-in Node library
WebApp.connectHandlers
.use(BusBoy())
.use('/path/to/upload', (req, res) => {
const user = req.headers['x-user-id'];
// We have to get a base64 digest of the sha256 hashed login token
// I'm not sure when Meteor changed to hashed tokens, but this is
// one of the major differences from @kahmali's answer
const hash = crypto.createHash('sha256');
hash.update(req.headers['x-auth-token']);
// Authentication (is user logged-in)
if (!Meteor.users.findOne({
_id: user,
'services.resume.loginTokens.hashedToken': hash.digest('base64'),
})) {
// User not logged in; 401 Unauthorized
res.writeHead(401);
res.end();
return;
}
// Authorization
if (!Roles.userIsInRole(user, 'whatever')) {
// User is not authorized; 403 Forbidden
res.writeHead(403);
res.end();
return;
}
if (req.busboy) {
// Handle file upload
res.writeHead(201); // eventually
res.end();
} else {
// Something went wrong
res.writeHead(500); // server error
res.end();
}
});
我希望这对某人有所帮助!
为服务器端路由验证用户的最佳方式(最安全和最简单)是什么?
Software/Versions
我正在使用最新的 Iron Router 1.* 和 Meteor 1.* 开始,我只使用 accounts-password.
参考代码
我有一个简单的服务器端路由,可以将 pdf 呈现到屏幕上:
both/routes.js
Router.route('/pdf-server', function() {
var filePath = process.env.PWD + "/server/.files/users/test.pdf";
console.log(filePath);
var fs = Npm.require('fs');
var data = fs.readFileSync(filePath);
this.response.write(data);
this.response.end();
}, {where: 'server'});
例如,我想做一些接近 this SO answer suggested 的事情:
在服务器上:
var Secrets = new Meteor.Collection("secrets");
Meteor.methods({
getSecretKey: function () {
if (!this.userId)
// check if the user has privileges
throw Meteor.Error(403);
return Secrets.insert({_id: Random.id(), user: this.userId});
},
});
然后在客户端代码中:
testController.events({
'click button[name=get-pdf]': function () {
Meteor.call("getSecretKey", function (error, response) {
if (error) throw error;
if (response)
Router.go('/pdf-server');
});
}
});
但即使我以某种方式让这个方法起作用,我仍然容易受到用户的攻击,只要输入像'/pdf-server'这样的URL,除非路由本身以某种方式检查了秘密collection对吗?
在Route中,我可以获取请求,并且不知何故获取了header信息?
Router.route('/pdf-server', function() {
var req = this.request;
var res = this.response;
}, {where: 'server'});
然后从客户端通过 HTTP header 传递令牌,然后在路由中检查来自 Collection?
的令牌是否正确由于服务器端路由充当简单的 REST 端点,因此它们无权访问用户身份验证数据(例如,它们无法调用 Meteor.user()
)。因此,您需要设计一个替代的身份验证方案。实现此目的最直接的方法是使用某种形式的密钥交换,如 here and here.
示例实现:
server/app.js
// whenever the user logs in, update her apiKey
Accounts.onLogin(function(info) {
// generate a new apiKey
var apiKey = Random.id();
// add the apiKey to the user's document
Meteor.users.update(info.user._id, {$set: {apiKey: apiKey}});
});
// auto-publish the current user's apiKey
Meteor.publish(null, function() {
return Meteor.users.find(this.userId, {fields: {apiKey: 1}});
});
lib/routes.js
// example route using the apiKey
Router.route('/secret/:apiKey', {name: 'secret', where: 'server'})
.get(function() {
// fetch the user with this key
// note you may want to add an index on apiKey so this is fast
var user = Meteor.users.findOne({apiKey: this.params.apiKey});
if (user) {
// we have authenticated the user - do something useful here
this.response.statusCode = 200;
return this.response.end('ok');
} else {
// the key is invalid or not provided so return an error
this.response.statusCode = 403;
return this.response.end('not allowed');
}
});
client/app.html
<template name="myTemplate">
{{#with currentUser}}
<a href="{{pathFor route='secret'}}">secret</a>
{{/with}}
</template>
备注
使
/secret
只能通过 HTTPS 访问。虽然请求
/secret
的用户很可能当前已连接,但不能保证她是。用户可能已经登录,复制了她的密钥,关闭了选项卡,并在稍后的某个时间发起了请求。这是一种简单的用户认证方式。如果服务器路由显示高价值数据(SSN、信用卡等),我会探索更复杂的机制(参见上面的链接)。
有关从服务器发送静态内容的详细信息,请参阅 this question。
除了使用 url 令牌作为其他答案外,您还可以使用 cookies:
添加一些允许您设置 cookie 并在服务器端读取它们的包:
meteor add mrt:cookies thepumpinglemma:cookies
然后你可以使用一些东西将 cookie 与你的登录状态同步
客户端
Tracker.autorun(function() {
//Update the cookie whenever they log in or out
Cookie.set("meteor_user_id", Meteor.userId());
Cookie.set("meteor_token", localStorage.getItem("Meteor.loginToken"));
});
服务器端
在服务器端你只需要检查这个cookie是否有效(使用铁路由器)
Router.route('/somepath/:fileid', function() {
//Check the values in the cookies
var cookies = new Cookies( this.request ),
userId = cookies.get("meteor_user_id") || "",
token = cookies.get("meteor_token") || "";
//Check a valid user with this token exists
var user = Meteor.users.findOne({
_id: userId,
'services.resume.loginTokens.hashedToken' : Accounts._hashLoginToken(token)
});
//If they're not logged in tell them
if(!user) return this.response.end("Not allowed");
//Theyre logged in!
this.response.end("You're logged in!");
}, {where:'server'});
我认为我有一个安全且简单的解决方案可以在 IronRouter.route() 中执行此操作。必须使用 header 中的有效用户 ID 和身份验证令牌发出请求。我从 Router.route() 中调用此函数,然后它让我可以访问 this.user,或者如果身份验证失败则以 401 响应:
// Verify the request is being made by an actively logged in user
// @context: IronRouter.Router.route()
authenticate = ->
// Get the auth info from header
userId = this.request.headers['x-user-id']
loginToken = this.request.headers['x-auth-token']
// Get the user from the database
if userId and loginToken
user = Meteor.users.findOne {'_id': userId, 'services.resume.loginTokens.token': loginToken}
// Return an error if the login token does not match any belonging to the user
if not user
respond.call this, {success: false, message: "You must be logged in to do this."}, 401
// Attach the user to the context so they can be accessed at this.user within route
this.user = user
// Respond to an HTTP request
// @context: IronRouter.Router.route()
respond = (body, statusCode=200, headers) ->
this.response.statusCode statusCode
this.response.setHeader 'Content-Type', 'text/json'
this.response.writeHead statusCode, headers
this.response.write JSON.stringify(body)
this.response.end()
客户这样说:
Meteor.startup ->
HTTP.get "http://yoursite.com/pdf-server",
headers:
'X-Auth-Token': Accounts._storedLoginToken()
'X-User-Id': Meteor.userId()
(error, result) -> // This callback triggered once http response received
console.log result
这段代码很大程度上受到了 RestStop 和 RestStop2 的启发。它是用于在 Meteor 0.9.0+(构建于 Iron Router 之上)中编写 REST API 的 meteor 包的一部分。您可以在此处查看完整的源代码:
由于 Meteor 不使用会话 cookie,客户端在向服务器路由发出 HTTP 请求时必须明确包含某种用户标识。
最简单的方法是在 URL 的查询字符串中传递 userId。显然,您还需要添加一个安全令牌,以证明用户确实是他们声称的那个人。可以通过 Meteor 方法获取此令牌。
Meteor 本身不提供这种机制,因此您需要一些自定义实现。我写了一个名为 mhagmajer:server-route
的 Meteor 包,它已经过全面测试。您可以在这里了解更多信息:https://blog.hagmajer.com/server-side-routing-with-authentication-in-meteor-6625ed832a94
我坚信使用 HTTP headers 是解决此问题的最佳方法,因为它们很简单,不需要弄乱 cookie 或开发新的身份验证方案。
我喜欢@kahmali 的回答,所以我写它是为了与 WebApp 和一个简单的 XMLHttpRequest 一起工作。这已经在 Meteor 1.6 上进行了测试。
客户端
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
// Skipping ahead to the upload logic
const xhr = new XMLHttpRequest();
const form = new FormData();
// Add files
files.forEach((file) => {
form.append(file.name,
// So BusBoy sees as file instead of field, use Blob
new Blob([file.data], { type: 'text/plain' })); // w/e your mime type is
});
// XHR progress, load, error, and readystatechange event listeners here
// Open Connection
xhr.open('POST', '/path/to/upload', true);
// Meteor authentication details (must happen *after* xhr.open)
xhr.setRequestHeader('X-Auth-Token', Accounts._storedLoginToken());
xhr.setRequestHeader('X-User-Id', Meteor.userId());
// Send
xhr.send(form);
服务器
import { Meteor } from 'meteor/meteor';
import { WebApp } from 'meteor/webapp';
import { Roles } from 'meteor/alanning:roles'; // optional
const BusBoy = require('connect-busboy');
const crypto = require('crypto'); // built-in Node library
WebApp.connectHandlers
.use(BusBoy())
.use('/path/to/upload', (req, res) => {
const user = req.headers['x-user-id'];
// We have to get a base64 digest of the sha256 hashed login token
// I'm not sure when Meteor changed to hashed tokens, but this is
// one of the major differences from @kahmali's answer
const hash = crypto.createHash('sha256');
hash.update(req.headers['x-auth-token']);
// Authentication (is user logged-in)
if (!Meteor.users.findOne({
_id: user,
'services.resume.loginTokens.hashedToken': hash.digest('base64'),
})) {
// User not logged in; 401 Unauthorized
res.writeHead(401);
res.end();
return;
}
// Authorization
if (!Roles.userIsInRole(user, 'whatever')) {
// User is not authorized; 403 Forbidden
res.writeHead(403);
res.end();
return;
}
if (req.busboy) {
// Handle file upload
res.writeHead(201); // eventually
res.end();
} else {
// Something went wrong
res.writeHead(500); // server error
res.end();
}
});
我希望这对某人有所帮助!