我正在使用Loopback开发NodeJS应用程序。
我对nodejs和REST API都很陌生,所以如果我在概念上错了,请纠正我。
Loopback会自动构建CRUD REST API,这是我想要使用的功能,以避免自己编写API,但我需要限制用户只能看到他们的数据。
例如,假设我的数据库中有3个表,user
,book
和关系表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
发生多个请求,那么更改默认范围是否为关键操作?
答案 0 :(得分:3)
我们实现这一目标的方法是创建一个mixin。看一下github中的环回时间戳混合。我建议混合创建一个"所有者"与您的用户模型的关系。以下是它的工作原理:
/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。所有其他步骤都相同。