八、使用允许和拒绝规则的安全性

在前一章中,我们创建了 admin 用户并准备了editPost模板。 在本章中,我们将使用这个模板来创建和编辑文章。

为了使在数据库中插入和更新文档成为可能,我们需要设置约束,以便不是每个人都可以更改数据库。 在 Meteor 中,这是通过允许和拒绝规则完成的。 这些函数将在将文档插入数据库之前检查文档。

在本章中,你将涵盖以下主题:

  • 添加和更新帖子
  • 使用允许和拒绝规则来控制数据库的更新
  • 使用服务器上的方法以获得更大的灵活性
  • Using method stubs to enhance user experience

    注释

    如果你已经直接读了这一章,并想要遵循这些例子, 下载前一章的代码示例从这本书的 web 页面在 https://www.packtpub.com/books/content/support/17713从 GitHub 库 https://github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter7

    这些代码示例还将包含所有样式文件,因此我们不必担心在过程中添加 CSS 代码。

添加一个函数来生成鼻涕虫

为了从我们的文章标题中生成的蛞蝓,我们将使用underscore-string库,它带有一个简单的slugify()函数。 幸运的是,Meteor 软件包服务器上已经存在这个库的包装包。 为了添加它,我们在my-meteor-blog文件夹的终端中运行以下命令:

$ meteor add wizonesolutions:underscore-string

这将扩展 Meteor 默认使用的underscore,与额外的字符串函数,如_.slugify(),从字符串生成一个 slug。

创建新岗位

现在我们可以为每个创建的页面生成蛞蝓,我们可以继续将保存过程添加到editPost模板中。

为此,我们需要为我们的editPost模板创建一个 JavaScript 文件,将一个名为editPost.js的文件保存到my-meteor-blog/client/templates文件夹中。 在这个文件中,我们将为模板的Save按钮添加一个事件:

Template.editPost.events({
  'submit form': function(e, template){
    e.preventDefault();
    console.log('Post saved');
  }
});

现在,如果我们转到/create-post路线,并点击Save Post按钮,浏览器的控制台就会出现Post saved日志。

保存邮件

为了保存帖子,我们将简单地获取表单的内容并将其存储在数据库中。 稍后,我们将重定向到新创建的文章页面。 为此,我们用以下几行代码扩展 click 事件:

Template.editPost.events({
  'submit form': function(e, tmpl){
    e.preventDefault();
    var form = e.target,
        user = Meteor.user();

我们获取当前用户,以便以后添加他作为文章的作者。 然后我们使用我们的slugify()函数从文章标题中生成一个蛞蝓:

        var slug = _.slugify(form.title.value);

接下来,我们使用所有其他表单字段将文章文档插入到Posts集合中。 对于timeCreated属性,我们使用moment包获取当前的 Unix 时间戳,该包是我们在Getting Started with Meteor中添加的。

owner字段将帮助我们确定这篇文章是由哪个用户创建的:

Posts.insert({
            title:          form.title.value,
            slug:           slug,
            description:    form.description.value,
            text:           form.text.value,
            timeCreated:    moment().unix(),
            author:         user.profile.name,
            owner:          user._id

        }, function(error) {
            if(error) {
                // display the error to the user
                alert(error.reason);
            } else {
                // Redirect to the post
                Router.go('Post', {slug: slug});
            }
        });
    }
});

我们传递给insert()函数的第二个参数是 Meteor 提供的一个回调函数,如果出现错误,它将接收一个错误参数。 如果发生了错误,我们就会警告它,如果一切正常,我们就会使用生成的 slug 重定向到新插入的帖子。

因为我们的路由控制器随后将订阅一个带有这个 slug 的帖子,所以它将能够加载我们新创建的帖子,并将其显示在 post 模板中。

现在,如果我们进入浏览器,填写表单,并点击保存按钮,我们应该已经创建了我们自己的第一篇文章!

编辑帖子

所以节省是有效的。 编辑呢?

当我们点击文章中的编辑按钮时,我们将再次显示editPost模板。 然而,这一次,表单字段是用来自文章的数据填充的。 到目前为止还不错,但如果我们现在按下保存按钮,我们将创建另一篇文章,而不是更新当前的文章。

更新当前岗位

由于我们设置了editPost模板的数据上下文,我们可以简单地使用帖子的_id字段作为更新的指示符,而不是插入帖子数据:

Template.editPost.events({
    'submit form': function(e, tmpl){
        e.preventDefault();
        var form = e.target,
            user = Meteor.user(),
            _this = this; // we need this to reference the slug in the callback

        // Edit the post
        if(this._id) {

            Posts.update(this._id, {$set: {
                title:          form.title.value,
                description:    form.description.value,
                text:           form.text.value

            }}, function(error) {
                if(error) {
                    // display the error to the user
                    alert(error.reason);
                } else {
                    // Redirect to the post
                    Router.go('Post', {slug: _this.slug});
                }
            });

        // SAVE
        } else {

            // The insertion process ...

        }
    }
});

知道了_id,我们可以简单地使用$set属性更新当前文档。 使用$set只会覆盖titledescriptiontext字段。 其他领域将保持原样。

注意,我们现在还需要在函数之上创建_this变量,以便在稍后的回调中访问当前数据上下文的slug属性。 这样,我们以后就可以重定向到编辑过的文章页面。

现在,如果我们保存文件并回到我们的浏览器,我们可以编辑文章并点击保存,所有的更改将按预期保存到我们的数据库中。

现在,我们可以创建和编辑帖子。 在下一节中,我们将学习如何通过添加允许和拒绝规则来限制对数据库的更新。

限制数据库更新

到目前为止,我们只是简单地将插入和更新功能添加到editPost模板中。 然而,任何人都可以插入和更新数据,如果他们只是输入一个insert语句到他们的浏览器控制台。

为了防止这种情况发生,我们需要在更新数据库之前在服务器端正确地检查插入和更新权限。

Meteor 的集合带有允许和拒绝功能,这将在每次插入或更新之前运行,以确定该动作是否被允许。

allow 规则允许我们更新某些文档或字段,而 deny 规则覆盖任何 allow 规则,并明确拒绝对其集合进行任何操作。

为了让这个更直观,让我们可视化一个例子,在这个例子中,我们定义了两个允许规则; 其中一个规则允许更改某些文档的title字段,另一个规则只允许编辑description字段,但附加的拒绝规则可以在任何情况下阻止特定文档的编辑。

删除不安全的包

为了使用 allow 和 deny 规则启动,我们需要从应用中删除insecure包,这样没有客户端可以简单地更改数据库而不通过我们的 allow 和 deny 规则。

在终端中使用Ctrl+C命令退出正在运行的meteor实例,执行如下命令:

$ meteor remove insecure

当我们成功移除包后,我们可以使用meteor命令再次运行 Meteor。

当我们现在进入浏览器并尝试编辑任何文章时,我们将看到一个警告窗口,声明访问被拒绝。 还记得我们以前在更新或插入操作失败时添加了这个alert()调用吗?

添加第一条允许规则

为了使我们的文章再次可编辑,我们需要添加允许规则来再次启用数据库更新。

为了做到这一点,我们将添加以下允许规则到我们的my-meteor-blog/collections.js文件,但在这种情况下,我们将只在服务器端通过检查 Meteor 的isServer变量来执行它们,如下所示:

if(Meteor.isServer) {

    Posts.allow({
        insert: function (userId, doc) {
            // The user must be logged in, and the document must be owned by the user
            return userId && doc.owner === userId && Meteor.user().roles.admin;
        },

插入规则,我们将插入文档只有在所有者匹配当前用户,如果用户是管理员,根据roles.admin属性,我们可以确定在前面的章节中,我们添加了。

如果 allow 规则返回false,则拒绝插入文档。 否则,我们将成功添加一个新帖子。 更新的工作方式相同,只是我们只检查当前用户是否是管理员:

        update: function (userId, doc, fields, modifier) {
            // User must be an admin
            return Meteor.user().roles.admin;
        },
        // make sure we only get this field from the documents
        fetch: ['owner']
    });
}

传递给update函数的参数如下表所示:

|

|

描述

| | --- | --- | | userId | 当前登录用户的用户 ID,执行update操作 | | doc | 从数据库中提取的文档,没有建议的更改 | | fields | 一个具有将要更新的字段参数的数组 | | modifier | 用户传递给update函数的修饰符,如{$set: {'name.first': "Alice"}, $inc: {score: 1}} |

我们在 allow 规则对象的最后指定的fetch属性,决定当前文档的哪些字段应该传递给更新规则。 在我们的例子中,更新规则只需要owner属性。 存在fetch属性是为了提高性能,防止不必要的大文档被传递给规则的函数。

注释

此外,我们可以指定remove()规则和transform()函数。 remove()规则将获得与insert()相同的参数,并允许或阻止删除文档。

transform()函数可用于在传递给允许或拒绝规则之前对文档进行转换,例如对其进行规范化。 但是,请注意,这不会更改插入到数据库中的文档。

如果我们现在尝试编辑我们网站上的帖子,我们应该能够编辑所有帖子以及创建新的帖子。

添加拒绝规则

为了提高的安全性,我们可以修复帖子的所有者和帖子创建的时间。 我们可以通过向Posts集合中添加一个额外的 deny 规则来防止所有者、timeCreatedslug字段的更改,如下所示:

if(Meteor.isServer) {

  // Allow rules

  Posts.deny({
    update: function (userId, docs, fields, modifier) {
      // Can't change owners, timeCreated and slug
      return _.contains(fields, 'owner') || _.contains(fields, 'timeCreated') || _.contains(fields, 'slug');
    }
  });
}

该规则将简单地检查fields参数是否包含一个受限制的字段。 如果有,我们拒绝更新这篇文章。 因此,即使我们之前的 allow 规则已经通过,我们的 deny 规则也会确保文档不会更改。

我们可以通过访问浏览器的控制台来尝试 deny 规则,当我们在发布页面时,输入以下命令:

Posts.update(Posts.findOne()._id, {$set: {'slug':'test'}}); 

这将给你一个错误,说明更新失败:访问拒绝,如下截图所示:

Adding a deny rule

虽然我们现在可以添加和更新贴子,但是有一个更好的方法来添加新贴子,而不是简单地从客户端插入Posts集合。

使用方法调用添加 posts

方法是可以从客户端调用并将在服务器上执行的函数。

方法存根和延迟补偿

方法的优势是它们可以在服务器上执行代码,拥有完整的数据库和客户机上的存根方法。

例如,我们可以让一个方法在服务器上执行某些操作,并在客户机上的存根方法中模拟预期的结果。 这样,用户就不必等待服务器的响应。 存根还可以调用接口更改,例如添加加载指示符。

一个本地方法调用的例子是 Meteor 的Collection.insert()函数,它将执行一个客户端函数,将文档立即插入到本地minimongo数据库中,并发送一个请求,在服务器上执行真正的insert方法。 如果插入成功,则客户端已经插入了文档。 如果发生错误,服务器将响应并再次从客户机删除插入的文档。

在 Meteor 中,这个概念被称为延迟补偿,因为界面对用户的响应立即做出响应,因此补偿了延迟,而服务器的往返则发生在后台。

使用方法调用插入一个帖子可以让我们简单地检查我们想要使用的帖子是否已经存在于另一个帖子中。 此外,我们可以使用服务器的时间timeCreated属性,以确保我们没有使用错误的用户时间戳。

更换按钮

在我们的示例中,我们将简单地使用方法存根功能,当我们在服务器上运行该方法时,将Save按钮的文本更改为Saving…。 为此,请执行以下步骤:

  1. 首先,让我们用一个模板助手来改变Save按钮的静态文本,这样我们就可以动态地改变它。 打开my-meteor-blog/client/templates/editPost.html,将按钮代码替换为保存按钮代码:

    <button type="submit" class="save">{{saveButtonText}}</button>

  2. Now open my-meteor-blog/client/templates/editPost.js and add the following template helper function at the beginning of the file:

    Session.setDefault('saveButton', 'Save Post'); Template.editPost.helpers({ saveButtonText: function(){ return Session.get('saveButton'); } });

    在这里,我们返回名为saveButton的会话变量,之前我们将其设置为默认值Save Post

更改会话将允许我们在稍后保存文档时更改Save按钮的文本。

添加方法

现在我们有了一个动态的Save按钮,让我们将实际的方法添加到我们的应用中。为此,我们将直接在my-meteor-blog文件夹中创建一个名为methods.js的新文件。 通过这种方式,它的代码将被加载到服务器和客户机上,这是作为存根在客户机上执行方法所必需的。

添加以下代码行以添加方法:

Meteor.methods({
    insertPost: function(postDocument) {

        if(this.isSimulation) {
            Session.set('saveButton', 'Saving...');
        }
    }
});

这将添加一个名为insertPost的方法。 在这个方法中,已经通过使用isSimulation属性添加了存根功能,该属性在 Meteor 函数的this对象中可用。

this对象也有以下属性:

  • unblock():这个函数在被调用时将防止该方法阻塞其他方法调用
  • userId:这个包含当前用户的 ID
  • setUserId():这是一个函数,用于连接当前客户端与某个用户
  • connection:这是服务器上的连接,通过它调用这个方法

如果isSimulation设置为true,该方法不会在服务器端运行,而是在客户端作为存根运行。 在这个条件中,我们简单地将saveButton会话变量设置为Saving…,这样按钮文本就会改变:

Meteor.methods({
  insertPost: function(postDocument) {

    if(this.isSimulation) {

      Session.set('saveButton', 'Saving...');

    } else {

为了完成该方法,我们将添加用于后期插入的服务器端代码:

       var user = Meteor.user();

       // ensure the user is logged in
       if (!user)
       throw new Meteor.Error(401, "You need to login to write a post");

在这里,我们让当前用户添加作者名和所有者 ID。

如果用户没有登录,我们抛出一个异常和new Meteor.Error。 这将停止方法的执行,并返回我们定义的错误消息。

我们也会搜索带有给定弹头的帖子。 如果我们找到一个,我们在蛞蝓前面加上一个随机字符串以防止重复。 这确保了每个蛞蝓是唯一的,我们可以成功地路由到我们新创建的帖子:

      if(Posts.findOne({slug: postDocument.slug}))
      postDocument.slug = postDocument.slug +'-'+ Math.random().toString(36).substring(3);

在我们插入新创建的帖子之前,我们使用moment库和authorowner属性添加timeCreated:

      // add properties on the serverside
      postDocument.timeCreated = moment().unix();
      postDocument.author      = user.profile.name;
      postDocument.owner       = user._id;

      Posts.insert(postDocument);

插入文档后,返回正确的 slug,它将作为第二个参数在方法调用的回调中被接收:

       // this will be received as the second argument of the method callback
       return postDocument.slug;
    }
  }
});

调用方法

现在我们已经创建了我们的insertPost方法,我们可以在提交事件中更改代码,在此之前我们在editPost.js文件中插入了 post,调用我们的方法:

var slug = _.slugify(form.title.value);

Meteor.call('insertPost', {
  title:          form.title.value
  slug:           slug,
  description:    form.description.value
  text:           form.text.value,

}, function(error, slug) {
  Session.set('saveButton', 'Save Post');

  if(error) {
    return alert(error.reason);
  }

  // Here we use the (probably changed) slug from the server side method
  Router.go('Post', {slug: slug});
});

正如我们在方法调用的回调中所看到的,我们使用我们在回调中收到的第二个参数slug变量路由到新创建的 post。 这确保了如果在服务器端修改了slug变量,我们将使用修改后的版本路由到 post。 此外,我们重置了saveButton会话变量,将文本再次更改为Save Post

就是这样! 现在,我们可以创建一个新的帖子,并使用我们新创建的insertPost方法保存它。 然而,编辑仍将使用Posts.update()从客户端完成,因为我们现在有允许和拒绝规则,这确保只修改允许的数据。

小结

在本章中,我们学习了如何允许和拒绝数据库更新。 我们建立了自己的允许和拒绝规则,并了解了如何通过将敏感进程移动到服务器端来提高安全性。 我们还改进了创建贴子的过程,检查了鼻涕虫是否已经存在,并添加了一个简单的进度指示器。

如果你想深入挖掘允许和拒绝规则或方法,请查看以下 Meteor 文档:

你可以在https://www.packtpub.com/books/content/support/17713或在 GitHub 上https://github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter8找到本章的代码示例。

在下一章中,我们将通过不断更新帖子的时间戳来实现界面的实时化。