Node.js 环回授权用户仅查看其数据
我正在使用Loopback开发一个NodeJS应用程序 我对nodejs和restapi都很陌生,所以如果我在概念上有错误,请纠正我 Loopback自动构建crudrestapi,这是我想要使用的一个特性,以避免自己编写api,但我需要限制用户只能看到他们的数据 例如,假设我的数据库中有3个表,Node.js 环回授权用户仅查看其数据,node.js,express,loopbackjs,Node.js,Express,Loopbackjs,我正在使用Loopback开发一个NodeJS应用程序 我对nodejs和restapi都很陌生,所以如果我在概念上有错误,请纠正我 Loopback自动构建crudrestapi,这是我想要使用的一个特性,以避免自己编写api,但我需要限制用户只能看到他们的数据 例如,假设我的数据库中有3个表,user,book和一个关系表user\u book 例如: table user id | name --------- 1 | user1 2 | user2
user
,book
和一个关系表user\u 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\u author]='author1'
应仅返回作者为'author1'的用户X
的书籍
我发现loopback在执行远程方法之前和之后提供了附加,并且还提供了所谓的
[…]指定可以作为方法调用引用的常用查询
关于模型[…]
我正在考虑使用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
发生多个请求,那么更改默认范围是否是一项关键操作?我们实现这一点的方法是创建一个mixin。看看github中的环回时间戳混合。我建议您创建一个与您的用户模型的“所有者”关系。简而言之,它的工作原理如下:
- 使用mixin的每个模型都将在模型和用户之间创建一个关系
- 每次创建模型的新实例时,用户ID都将与实例一起保存
- 每次调用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
}
}
{
"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": "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": []
}
每次使用Owner mixing时,该模型将在每次创建或保存新实例时自动添加和填充ownerId属性,并且每次“获取”数据时,结果将自动过滤。我认为解决方案是使用环回关系。必须设置关系:
-用户通过用户手册拥有许多书籍
-书中有许多用户通过用户书
这与回送文档提供的示例类似:
因此,假设用户在使用函数之前应通过身份验证,然后您可以传递user/userId/books以获得特定用户可以访问的图书
如果要限制访问,则应使用ACL。对于这种情况,必须使用自定义角色解析程序,回送提供了相同的示例:
如果应用了此解析器,则用户只能访问属于他们的书籍
希望这有帮助以下是我解决您问题的方法:
/common/models/user\u book.json
{
"mixins": {
"Owner": true
}
}
{
"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": "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": []
}
/common/models/book
{
"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
{
"mixins": {
"Owner": true
}
}
{
"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": "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”创建一个定制的远程方法,并具有route“/books”。在其处理函数中,您应该获取经过身份验证的用户实例(带有访问令牌信息),并只返回user.books(通过对直通关系的环回实现),以获取用户\ 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\u token=[authenticated token government from POST/users/login]
获取属于已验证用户的书籍列表。
有关环回中has多直通关系的使用,请参阅参考资料:
祝你好运!:)
使用此mixim而不是@YeeHaw1234 answer。所有其他步骤都是相同的。我想补充一下YeeHaw1234的答案。我计划按照他描述的方式使用mixin,但我需要更多的字段,而不仅仅是用户ID来过滤数据。我还有另外3个字段要添加到访问令牌中,以便在尽可能低的级别强制执行数据规则
我想在会话中添加一些字段,但不知道如何在环回中添加。我查看了express session和cookie express,但问题是我不想重写环回登录,登录似乎是应该设置会话字段的地方
我的解决方案是创建一个自定义用户和自定义访问令牌,并添加我需要的字段。然后,在写入新的访问令牌之前,我使用操作挂钩(保存之前)插入新字段
现在每次有人登录,我都会得到额外的字段。如果有更简单的方法向会话添加字段,请随时通知我。我计划添加一个更新访问令牌,这样,如果用户的权限在登录时发生更改,他们将在会话中看到这些更改
下面是一些代码
/通用/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答案中。最后一个大问题是我有next()
ou