#node.js #express #design-patterns #acl
#node.js #экспресс #шаблоны проектирования #acl
Вопрос:
Я внедряю простую систему контроля доступа в Node, и мне интересно, какой может быть наилучший подход к тому, что я делаю.
Я использую Node ACL, и мне непонятно, как блокировать для каждого ресурса.
Давайте рассмотрим следующий пример: USER ->* PROJECT ->* ENTRY
. У пользователей может быть несколько проектов, содержащих много записей. Пользователями могут быть ADMIN
или USER
.
Я создал конечную точку, /entry/{ID}
где пользователь может получить доступ к деталям записи. Конечная точка доступна для всех, ADMIN
пользователи могут видеть все записи, но для User
мне нужно сделать что-то подобное:
app.get('/entry/{id}', (req, res) => {
if (user.admin) {
// Return eveything
}
else {
if (entry.project == user.project) {
// return it
}
else {
// Unathorized
}
}
})
Существует ли лучший подход / шаблон для реализации этой проверки владения ресурсом?
Ответ №1:
Это очень широкий вопрос, поэтому я постараюсь дать вам пару подсказок в качестве моего ответа, но
Существует ли шаблон ACL в javascript?
Существует ряд решений, но я бы не назвал ни одно из них шаблоном. Я буду сейчас очень субъективен, но способы passport.js
и подобных модулей, мягко говоря, непрозрачны — и на самом деле это не ACL…
Кто-то может сказать — эй, это node.js для этого должен быть модуль, который сделает ваши node_modules тяжелее, но в поисках хорошего модуля acl в npm я нашел только несколько устаревших и тесно связанных с express. Поскольку вашего вопроса не было which is the best npm module for acl
, я отказался от поиска такого на странице 3, что не означает, что там нет чего-то готового, поэтому вы можете захотеть присмотреться повнимательнее.
Я думаю, что вашу реализацию можно считать приемлемой, с некоторыми незначительными исправлениями или подсказками, как я упоминал:
Отделите логику вашего запроса от логики управления доступом
В вашем коде все происходит за один обратный вызов — это определенно очень эффективно, но также очень сложно поддерживать в долгосрочной перспективе. Видите ли, это закончится в том же коде во множестве приведенных выше if во всех обратных вызовах. Разделить логику очень просто — просто реализуйте один и тот же путь в двух обратных вызовах (они будут выполняться в том порядке, в котором они были определены), поэтому:
app.all('/entry/{id}', (req, res, next) => {
const {user, entry} = extractFromRequest(req);
if (user.admin || entry.project === user.project) {
next();
} else {
res.status(403).send("Forbidden");
}
});
app.get('/entry/{id}', (req, res) => {
// simply respond here
})
Таким образом, первый обратный вызов проверяет, есть ли у пользователя доступ, и это не повлияет на логику ответа. Использование next()
специфично для фреймворков, подобных express, которые, как я предположил, вы используете, глядя на свой код — при его вызове будет выполнен следующий обработчик, в противном случае никакие другие обработчики запускаться не будут.
Смотрите Express.js приложение. вся документация для примера acl.
Используйте acl для всей службы
Гораздо безопаснее хранить базовый ACL в одном месте и не определять его для каждого пути без необходимости. Таким образом, вы не пропустите ни одного пути и не оставите брешь в безопасности где-нибудь в середине запроса. Для этого нам нужно разделить ACL на части:
- Проверка доступа к URL (является ли путь общедоступным / открытым для всех пользователей)
- Проверка подлинности пользователя и сеанса (пользователь вошел в систему, срок действия сеанса не истек)
- Проверка администратором / пользователем (уровень разрешений so)
- В противном случае мы ничего не разрешаем.
app.all('*', (req, res, next) => {
if (path.isPublic) next(); // public paths can be unlogged
else if (user.valid amp;amp; user.expires > Date.now()) next(); // session and user must be valid
else if (user.admin) next(); // admin can go anywhere
else if (path.isOpen amp;amp; user.valid) next(); // paths for logged in users may also pass
else throw new Error("Forbidden");
});
Эта проверка не является очень строгой, но нам не нужно будет повторяться. Также обратите внимание на ошибку throw внизу — мы обработаем это в обработчике ошибок:
app.use(function (err, req, res, next) {
if (err.message === "Forbidden") res.status(403).send("Forbidden");
else res.status(500).send("Something broke");
})
Любой обработчик с 4 аргументами будет считаться обработчиком ошибок с помощью Express.js .
На определенном уровне пути, если есть какая-либо необходимость в ACL, просто отправьте обработчику сообщение об ошибке:
app.all('/entry/{id}', (req, res, next) => {
if (!user.admin amp;amp; user.project !== entry.project) throw new Error("Forbidden");
// then respond...
});
Что напоминает мне о другом намеке…
Не используйте user.admin
Хорошо, прекрасно, используй это, если хочешь. Я этого не делаю. Первой попыткой взломать ваш код будет попытка установить admin для любого объекта, у которого есть свойства. Это обычное имя при обычной проверке безопасности, поэтому это все равно, что оставить вход в точку доступа Wi-Fi с заводскими настройками по умолчанию.
Я бы рекомендовал использовать роли и разрешения. Роль содержит набор разрешений, у пользователя есть несколько ролей (или одна роль, которая проще, но дает вам меньше возможностей). Роли также могут быть назначены проекту.
Об этом можно написать целую статью, поэтому вот некоторые дополнительные сведения о ACL на основе ролей.
Используйте стандартные HTTP-ответы
Кое-что из этого упоминалось выше, но хорошей практикой является просто использовать один из стандартных статусов кода HTTP 4xx в качестве ответа — это будет иметь смысл для клиента. По сути, отвечайте, 401
когда пользователь не вошел в систему (или истек срок действия сеанса), 403
когда нет достаточных привилегий, 429
когда превышены ограничения на использование. дополнительные коды и что делать, когда запрос является чайником в Википедии.
Что касается самой реализации, мне нравится создавать простой класс authError и использовать его для выдачи ошибок из приложения.
class AuthError extends Error {
constructor(status, message = "Access denied") {
super(message);
this.status = status;
}
}
Действительно легко как обработать, так и выдать такую ошибку в коде, например:
app.all('*', (req, res, next) => {
// check if all good, but be more talkative otherwise
if (!path.isOpen amp;amp; !user.valid) throw new AuthError(401, "Unauthenticated");
throw new AuthError(403);
});
function checkRoles(user, entry) {
// do some checks or...
throw new AuthError(403, "Insufficient Priviledges");
}
app.get('/entry/{id}', (req, res) => {
checkRoles(user, entry); // throws AuthError
// or respond...
})
И в вашем обработчике ошибок вы отправляете свой статус / сообщение как перехваченное из вашего кода:
app.use(function (err, req, res, next) {
if (err instanceof AuthError) res.send(err.status).send(err.message);
else res.status(500).send('Something broke!')
})
Не отвечайте немедленно
Наконец, это скорее функция безопасности одновременно. Каждый раз, когда вы отвечаете сообщением об ошибке, почему бы не подождать пару секунд? Это повредит вам с точки зрения памяти, но это повредит совсем немного, и это сильно повредит возможному злоумышленнику, потому что они дольше будут ждать результата. Более того, это очень просто реализовать всего в одном месте:
app.use(function (err, req, res, next) {
// some errors from the app can be handled here - you can respond immediately if
// you think it's better.
if (err instanceof AppError) return res.send(err.status).send(err.message);
setTimeout(() => {
if (err instanceof AuthError) res.send(err.status).send(err.message);
else res.status(500).send('Something broke!')
}, 3000);
})
Фух… Я не думаю, что этот список является исчерпывающим, но, на мой взгляд, это разумное начало.