环回授权用户仅查看他的数据

时间:2017-07-12 14:21:22

标签: node.js express loopbackjs

我正在使用Loopback开发NodeJS应用程序。

我对nodejs和REST API都很陌生,所以如果我在概念上错了,请纠正我。

Loopback会自动构建CRUD REST API,这是我想要使用的功能,以避免自己编写API,但我需要限制用户只能看到他们的数据。

例如,假设我的数据库中有3个表,userbook和关系表user_book

例如:

table user
    id | name
    ---------
    1 | user1
    2 | user2
    3 | user3

table book
    id | title | author
    -------------------
    1 | title1 | author1
    2 | title2 | author1
    3 | title3 | author2
    4 | title4 | author2
    5 | title5 | author3

table user_book
    id | user_id | book_id
    -------------------
    1 |     1    |    1
    2 |     1    |    4
    3 |     1    |    3
    4 |     2    |    3
    5 |     2    |    2
    6 |     2    |    1
    7 |     3    |    3

对用户X进行身份验证后,API /books应该只回答X的书籍,而不是表格中的每本书。例如,如果用户user1已被记录并调用/books,那么他应该只获取他的图书,因此ID为1, 3, 4的图书。

同样,/books?filter[where][book_author]='author1'只应返回作者为#author;'的用户X的图书。

我发现loopback在执行远程方法之前和之后提供了remote hooks,并且还提供了所谓的scopes

  

[...]指定可以作为方法调用引用的常用查询   在模型上[...]

我正在考虑使用2的组合来限制对表books的访问,以便只运行调用API的用户行。

module.exports = function (book) {

  // before every operation on table book
  book.beforeRemote('**', function (ctx, user, next) {
    [HERE I WOULD PERFORM A QUERY TO FIND THE BOOKS ASSOCIATED WITH THE USER, LET'S CALL ID book_list]

    ctx._ds = book.defaultScope; // save the default scope
    book.defaultScope = function () {
      return {
        'where': {
          id in book_list
        }
      };
    };

    next();
  });

  book.afterRemote('**', function (ctx, user, next) {
    book.defaultScope = ctx._ds; // restore the default scope
    next();
  });
};

此解决方案有效吗?特别是,我特别关注并发性。如果来自不同用户的/books发生多个请求,那么更改默认范围是否为关键操作?

5 个答案:

答案 0 :(得分:3)

我们实现这一目标的方法是创建一个mixin。看一下github中的环回时间戳混合。我建议混合创建一个"所有者"与您的用户模型的关系。以下是它的工作原理:

  • 使用mixin的每个模型都将在模型和用户之间创建关系
  • 每次创建模型的新实例时,userId都将与实例一起保存
  • 每次调用 find findById 时,都会修改查询以添加 {where:{userId:[当前登录的用户ID]}} 条款

/common/mixins/owner.js

'use strict';
module.exports = function(Model, options) {
  // get the user model
  var User = Model.getDataSource().models.User;
  // create relation to the User model and call it owner
  Model.belongsTo(User, {as: 'owner', foreignKey: 'ownerId'});

  // each time your model instance is saved, make sure the current user is set as the owner
  // need to do this for upsers too (code not here)
  Model.observe('before save', (ctx, next)=>{
    var instanceOrData = ctx.data ? 'data' : 'instance';
    ctx[instanceOrData].ownerId = ctx.options.accessToken.userId;
  });

  // each time your model is accessed, add a where-clause to filter by the current user
  Model.observe('access', (ctx, next)=>{
    const userId = safeGet(ctx, 'options.accessToken.userId');
    if (!userId) return next();  // no access token, internal or test request;
    var userIdClause = {userId: userId};

    // this part is tricky because you may need to add
    // the userId filter to an existing where-clause

    ctx.query = ctx.query || {};
    if (ctx.query.where) {
      if (ctx.query.where.and) {
        if (!ctx.query.where.and.some((andClause)=>{
          return andClause.hasOwnProperty('userId');
        })) {
          ctx.query.where.and.push(userIdClause);
        }
      } else {
        if (!ctx.query.where.userId) {
          var tmpWhere = ctx.query.where;
          ctx.query.where = {};
          ctx.query.where.and = [tmpWhere, userIdClause];
        }
      }
    } else {
      ctx.query.where = userIdClause;
    }
    next();
  });
};

<强> /common/models/book.json

{
  "mixins": {
    "Owner": true
  }
}

每次使用所有者混合时,每次创建或保存新实例时,该模型都会自动添加并填充ownerId属性,并且每当您获得&#34; get&#34;数据。

答案 1 :(得分:1)

我认为解决方案是使用环回关系。您必须设置关系: - 用户通过用户手册有很多书 - 图书有很多用户通过用户手册

它类似于环回文档提供的示例:loopback docs

因此,假设用户在使用功能之前应进行身份验证,然后您可以传递用户/ userId / books以获取特定用户可访问的图书。

如果要限制访问权限,则应使用ACL。对于这种情况,您必须使用自定义角色解析器,环回提供相同的示例:roleResolver

如果您应用此解析程序,则用户只能访问属于他们的图书。

希望这有帮助

答案 2 :(得分:1)

我想补充一下YeeHaw1234的答案。我打算按照他的描述使用Mixins,但是我需要更多字段而不只是User ID来过滤数据。我还想将3个其他字段添加到访问令牌中,以便可以在最低级别上强制执行数据规则。

我想在会话中添加一些字段,但无法弄清楚如何在Loopback中使用。我查看了express-session和cookie-express,但问题是我不想重写Loopback登录,而Login似乎是应该设置会话字段的地方。

我的解决方案是创建一个“自定义用户”和“自定义访问令牌”并添加我需要的字段。然后,在写入新的访问令牌之前,我使用了一个操作钩子(在保存之前)插入了我的新字段。

现在,每次有人登录时,我都会得到我的额外字段。随时让我知道是否有更简单的方法可以将字段添加到会话中。我计划添加一个更新的访问令牌,以便如果用户登录时更改了权限,他们将在会话中看到这些更改。

这里是一些代码。

/common/models/mr-access-token.js

var app = require('../../server/server');

module.exports = function(MrAccessToken) {

  MrAccessToken.observe('before save', function addUserData(ctx, next) {
    const MrUser = app.models.MrUser;
    if (ctx.instance) {
      MrUser.findById(ctx.instance.userId)
        .then(result => {
        ctx.instance.setAttribute("role");
        ctx.instance.setAttribute("teamId");
        ctx.instance.setAttribute("leagueId");
        ctx.instance.setAttribute("schoolId");
        ctx.instance.role = result.role;
        ctx.instance.teamId = result.teamId;
        ctx.instance.leagueId = result.leagueId;
        ctx.instance.schoolId = result.schoolId;
        next();
      })
      .catch(err => {
        console.log('Yikes!');
      })
    } else {
      MrUser.findById(ctx.instance.userId)
        .then(result => {
        ctx.data.setAttribute("role");
        ctx.data.setAttribute("teamId");
        ctx.data.setAttribute("leagueId");
        ctx.data.setAttribute("schoolId");
        ctx.data.role = result.role;
        ctx.data.teamId = result.teamId;
        ctx.data.leagueId = result.leagueId;
        ctx.data.schoolId = result.schoolId;
        next();
      })
      .catch(err => {
        console.log('Yikes!');
      })
    }
  })


};

这花了我很长时间进行调试。这是我遇到的一些障碍。我最初以为它需要在/ server / boot中,但是我没有看到保存时触发的代码。当我将其移动到/ common / models时,它开始触发。在文档中没有试图找出如何从观察者内部引用第二个模型。 var app = ...在另一个SO答案中。最后一个大问题是我在异步findById之外有next(),因此实例被原样返回,然后异步代码将修改该值。

/common/models/mr-user.js

{
  "name": "MrUser",
  "base": "User",
  "options": {
    "idInjection": false,
    "mysql": {
      "schema": "matrally",
      "table": "MrUser"
    }
  },
  "properties": {
    "role": {
      "type": "String",
      "enum": ["TEAM-OWNER",
        "TEAM-ADMIN",
        "TEAM-MEMBER",
        "SCHOOL-OWNER",
        "SCHOOL-ADMIN",
        "SCHOOL-MEMBER",
        "LEAGUE-OWNER",
        "LEAGUE-ADMIN",
        "LEAGUE-MEMBER",
        "NONE"],
      "default": "NONE"
    }
  },
  "relations": {
    "accessTokens": {
      "type": "hasMany",
      "model": "MrAccessToken",
      "foreignKey": "userId",
      "options": {
        "disableInclude": true
      }
    },
    "league": {
      "model": "League",
      "type": "belongsTo"
    },
    "school": {
      "model": "School",
      "type": "belongsTo"
    },
    "team": {
      "model": "Team",
      "type": "belongsTo"
    }
  }
}

/common/models/mr-user.js

{
  "name": "MrAccessToken",
  "base": "AccessToken",
  "options": {
    "idInjection": false,
    "mysql": {
      "schema": "matrally",
      "table": "MrAccessToken"
    }
  },
  "properties": {
    "role": {
      "type": "String"
    }
  },
  "relations": {
    "mrUser": {
      "model": "MrUser",
      "type": "belongsTo"
    },
    "league": {
      "model": "League",
      "type": "belongsTo"
    },
    "school": {
      "model": "School",
      "type": "belongsTo"
    },
    "team": {
      "model": "Team",
      "type": "belongsTo"
    }
  }
}

/server/boot/mrUserRemoteMethods.js

var senderAddress = "curtis@abcxyz.com"; //Replace this address with your actual address
var config = require('../../server/config.json');
var path = require('path');


module.exports = function(app) {
  const MrUser = app.models.MrUser;


  //send verification email after registration
  MrUser.afterRemote('create', function(context, user, next) {
    var options = {
      type: 'email',
      to: user.email,
      from: senderAddress,
      subject: 'Thanks for registering.',
      template: path.resolve(__dirname, '../../server/views/verify.ejs'),
      redirect: '/verified',
      user: user
    };

    user.verify(options, function(err, response) {
      if (err) {
        MrUser.deleteById(user.id);
        return next(err);
      }
      context.res.render('response', {
        title: 'Signed up successfully',
        content: 'Please check your email and click on the verification link ' +
            'before logging in.',
        redirectTo: '/',
        redirectToLinkText: 'Log in'
      });
    });
  });

  // Method to render
  MrUser.afterRemote('prototype.verify', function(context, user, next) {
    context.res.render('response', {
      title: 'A Link to reverify your identity has been sent '+
        'to your email successfully',
      content: 'Please check your email and click on the verification link '+
        'before logging in',
      redirectTo: '/',
      redirectToLinkText: 'Log in'
    });
  });

  //send password reset link when requested
  MrUser.on('resetPasswordRequest', function(info) {
    var url = 'http://' + config.host + ':' + config.port + '/reset-password';
    var html = 'Click <a href="' + url + '?access_token=' +
        info.accessToken.id + '">here</a> to reset your password';

    MrUser.app.models.Email.send({
      to: info.email,
      from: senderAddress,
      subject: 'Password reset',
      html: html
    }, function(err) {
      if (err) return console.log('> error sending password reset email');
      console.log('> sending password reset email to:', info.email);
    });
  });

  //render UI page after password change
  MrUser.afterRemote('changePassword', function(context, user, next) {
    context.res.render('response', {
      title: 'Password changed successfully',
      content: 'Please login again with new password',
      redirectTo: '/',
      redirectToLinkText: 'Log in'
    });
  });

  //render UI page after password reset
  MrUser.afterRemote('setPassword', function(context, user, next) {
    context.res.render('response', {
      title: 'Password reset success',
      content: 'Your password has been reset successfully',
      redirectTo: '/',
      redirectToLinkText: 'Log in'
    });
  });

};

这直接来自示例,但尚不清楚应在/ boot中注册它。在我将自定义用户从/ common / models移至/ server / boot之前,我无法让其自定义用户发送电子邮件。

答案 3 :(得分:0)

以下是我解决问题的方法:

<强> /common/models/user_book.json

{
  "name": "user_book",
  "base": "PersistedModel",
  "idInjection": true,
  "properties": {
    "id": {
      "type": "number",
      "required": true
    },
    "user_id": {
      "type": "number",
      "required": true
    },
    "book_id": {
      "type": "number",
      "required": true
    }
  },
  "validations": [],
  "relations": {
    "user": {
      "type": "belongsTo",
      "model": "user",
      "foreignKey": "user_id"
    },
    "book": {
      "type": "belongsTo",
      "model": "book",
      "foreignKey": "book_id"
    }
  },
  "acls": [{
      "accessType": "*",
      "principalType": "ROLE",
      "principalId": "$authenticated",
      "permission": "ALLOW",
      "property": "*"
    }],
  "methods": []
}

<强> /普通/模型/书

{
  "name": "book",
  "base": "PersistedModel",
  "idInjection": true,
  "properties": {
    "id": {
      "type": "number",
      "required": true
    },
    "title": {
      "type": "string",
      "required": true
    },
    "author": {
      "type": "string",
      "required": true
    }
  },
  "validations": [],
  "relations": {
      "users": {
        "type": "hasMany",
        "model": "user",
        "foreignKey": "book_id",
        "through": "user_book"
      }
  },
  "acls": [{
      "accessType": "*",
      "principalType": "ROLE",
      "principalId": "$authenticated",
      "permission": "ALLOW",
      "property": "*"
    }],
  "methods": []
}

<强> /common/models/user.json

{
  "name": "user",
  "base": "User",
  "idInjection": true,
  "properties": {},
  "validations": [],
  "relations": {
    "projects": {
      "type": "hasMany",
      "model": "project",
      "foreignKey": "ownerId"
    },
    "teams": {
      "type": "hasMany",
      "model": "team",
      "foreignKey": "ownerId"
    },
    "books": {
      "type": "hasMany",
      "model": "book",
      "foreignKey": "user_id",
      "through": "user_book"
    }
  },
  "acls": [{
      "accessType": "*",
      "principalType": "ROLE",
      "principalId": "$everyone",
      "permission": "ALLOW",
      "property": "listMyBooks"
    }],
  "methods": []
}

然后在用户模型js文件中,您需要使用HTTP动词“GET”创建自定义远程方法,并具有路径“/ books”。在其处理函数中,您应该获取经过身份验证的用户实例(带有访问令牌信息)并返回user.books(通过loopback实现的贯通关系)以获取user_book模型指定的相关书籍。这是代码示例:

<强> /common/models/user.js

module.exports = function(User) {
  User.listMyBooks = function(accessToken,cb) {
    User.findOne({where:{id:accessToken.userId}},function(err,user) {
      user.books(function (err,books){
          if (err) return cb(err);
          return cb(null,books);
      });
    });
  };
  User.remoteMethod('listMyBooks', {
    accepts: [{arg: 'accessToken', type: 'object', http: function(req){return req.res.req.accessToken}}],
    returns: {arg: 'books', type: 'array'},
    http: {path:'/books', verb: 'get'}
  });
};

请确保公开远程方法以供公众访问:

<强> /server/model-config.json:

  ...
  "user": {
    "dataSource": "db",
    "public": true
  },
  "book": {
    "dataSource": "db",
    "public": true
  },
  "user_book": {
    "dataSource": "db",
    "public": true
  }
  ...

通过这些,您应该可以致电GET /users/books?access_token=[authenticated token obtained from POST /users/login] 获取属于经过身份验证的用户的书籍列表。 请参阅环回中使用has-many-through关系的参考资料:https://loopback.io/doc/en/lb3/HasManyThrough-relations.html

祝你好运! :)

答案 4 :(得分:0)

'use strict';
module.exports = function(Model, options) {
  // get the user model
  var User = Model.getDataSource().models.User;
  var safeGet = require("l-safeget");
  // create relation to the User model and call it owner
  Model.belongsTo(User, {as: 'owner', foreignKey: 'ownerId'});

  // each time your model instance is saved, make sure the current user is set as the owner
  // need to do this for upsers too (code not here)
  Model.observe('before save', (ctx, next)=>{
    var instanceOrData = ctx.data ? 'data' : 'instance';
    ctx[instanceOrData].ownerId = ctx.options.accessToken.userId;
    next();
  });

Model.observe('access', (ctx, next)=>{
    const userId = safeGet(ctx, 'options.accessToken.userId');
    if (!userId) return next();  // no access token, internal or test request;
    var userIdClause = {ownerId: userId};

    // this part is tricky because you may need to add
    // the userId filter to an existing where-clause

    ctx.query = ctx.query || {};
    if (ctx.query.where) {
        if (!ctx.query.where.ownerId) {
          var tmpWhere = ctx.query.where;
          ctx.query.where = {};
          ctx.query.where.and = [tmpWhere, userIdClause];

  }     }
     else {
      ctx.query.where = userIdClause;

    }
    next();
 });
};

使用此mixim代替@ YeeHaw1234 answer。所有其他步骤都相同。