【问题标题】:Authentication on Server side routes in MeteorMeteor 中服务器端路由的身份验证
【发布时间】:2015-02-28 07:26:09
【问题描述】:

对服务器端路由的用户进行身份验证的最佳方式(最安全和最简单)是什么?

软件/版本

我正在使用最新的 Iron Router 1.* 和 Meteor 1.* 开始,我只使用帐户密码。

参考代码

我有一个简单的服务器端路由,可以将 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 的攻击,除非路由本身以某种方式检查了 Secrets 集合对吗?

在Route中,我可以获取请求,并以某种方式获取header信息?

Router.route('/pdf-server', function() {
  var req = this.request;
  var res = this.response;
}, {where: 'server'});

并从客户端通过 HTTP 标头传递一个令牌,然后在路由中检查该令牌是否来自 Collection?

【问题讨论】:

    标签: node.js authentication meteor routing iron-router


    【解决方案1】:

    除了使用 url 令牌作为其他答案之外,您还可以使用 cookie:

    添加一些允许您设置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'});
    

    【讨论】:

    • 是否有暂时的cookies(为了增加安全性)?例如,cookie 有效的 30 分钟窗口,之后客户端需要获取新的 cookie 才能访问文件?
    • @Aaron 它们包含与 DDP 线路上可见的相同数据,因此它们与登录令牌(在 db 中散列)一样安全。尽管如此,Cookies.set 需要一个可选的第三个参数,该参数可以包含一个 expires 键,Cookies.set(key, val, { domain: 'example.com', path: '/', expires: new Date(new Date().getTime() + 36000000) }) 会将其设置为在 10 小时内过期,例如路径 /
    • 我在 cookies.request 中试用了这段代码,没有“meteor_user_id”或“meteor_token”。由于无法在此处发布,因此我将在我的问题中发布屏幕截图。
    • @Aaron 如果你执行 console.log(cookies),它会显示为空,你必须使用 .get。如果 cookie 包含在其中,您可能需要检查 Chrome 开发控制台以查看发送到页面的标头
    • @Aaron 如果您使用path:'/' 匹配路线,则没有真正的优势。通常,弱点是 csrf(例如,如果您在 x.meteor.com 上托管,meteor.com 可以读取,那么 y.meteor.com 也可以) - 当流星允许在子域上免费托管时,子域和 cookie 是一个问题,但如果您正确设置了域,这不是问题。
    【解决方案2】:

    我认为我有一个安全且简单的解决方案可以在 IronRouter.route() 中执行此操作。请求必须在标头中使用有效的用户 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 的流星包的一部分。你可以在这里查看完整的源代码:

    https://github.com/krose72205/meteor-restivus

    【讨论】:

    • 感谢您的精彩回答!调用此代码的客户端会是什么样子?此外,如果我已登录,并采用服务器端路由来呈现 pdf,例如 /pdf-server,我会自动在标题中传递我的 loginToken 还是应该做一些特殊的事情来获得客户端标头中的令牌?
    • @Aaron:我刚刚用一些客户端代码更新了示例,用于添加身份验证标头。我还编辑了来自服务器的响应以包含一个标头,您需要在服务器端实现 CORS 合规性(以接受来自浏览器的请求)。我将更新 Restivus 包以包含它。我不确定 Meteor 是否会自动在标题中添加任何内容。我四处寻找,但找不到任何关于它的东西。对不起。
    • 抱歉,您无需担心 CORS 合规性,因为您是在同一个域中发出请求的。我将从示例中删除它以防止任何混淆。
    【解决方案3】:

    由于服务器端路由充当简单的 REST 端点,它们无权访问用户身份验证数据(例如,它们无法调用 Meteor.user())。因此,您需要设计一种替代的身份验证方案。实现此目的最直接的方法是使用某种形式的密钥交换,如 herehere 所述。

    示例实现:

    服务器/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

    【讨论】:

    • 这个方案唯一的问题是你每次都要查询数据库。
    【解决方案4】:

    我真的相信使用 HTTP 标头是解决此问题的最佳方法,因为它们很简单,不需要搞乱 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();
        }
      });
    

    我希望这对某人有帮助!

    【讨论】:

      【解决方案5】:

      由于 Meteor 不使用会话 cookie,客户端在向服务器路由发出 HTTP 请求时必须明确包含某种用户标识。

      最简单的方法是在 URL 的查询字符串中传递 userId。显然,您还需要添加一个安全令牌,以证明用户确实是他们所声称的身份。可以通过 Meteor 方法获取此令牌。

      Meteor 本身不提供这种机制,因此您需要一些自定义实现。我写了一个名为 mhagmajer:server-route 的 Meteor 包,它经过了彻底的测试。你可以在这里了解更多信息:https://blog.hagmajer.com/server-side-routing-with-authentication-in-meteor-6625ed832a94

      【讨论】:

        猜你喜欢
        • 2021-06-20
        • 1970-01-01
        • 1970-01-01
        • 2018-02-26
        • 2011-11-04
        • 2016-06-01
        • 2020-04-22
        • 2012-08-28
        • 1970-01-01
        相关资源
        最近更新 更多