Как реализовать безопасный REST API с помощью node.js


204

Я начинаю планировать REST API с помощью node.js, express и mongodb. API предоставляет данные для веб-сайта (публичного и частного) и, возможно, мобильного приложения. Интерфейс будет разработан с AngularJS.

В течение нескольких дней я много читал о защите REST API, но не нашел окончательного решения. Насколько я понимаю, это использовать HTTPS для обеспечения базовой безопасности. Но как я могу защитить API в этих случаях использования:

  • Только посетители / пользователи веб-сайта / приложения могут получать данные для публичной части веб-сайта / приложения.

  • Только аутентифицированные и авторизованные пользователи могут получать данные для приватной области (и только данные, для которых пользователю предоставлены разрешения).

На данный момент я думаю, что только пользователи с активным сеансом могут использовать API. Для авторизации пользователей я буду использовать паспорт, а для разрешения мне нужно что-то реализовать для себя. Все на вершине HTTPS.

Может ли кто-нибудь предоставить лучшую практику или опыт? Есть ли недостаток в моей «архитектуре»?


2
Я предполагаю, что API будет использоваться только с интерфейса, который вы предоставляете? В этом случае использование сеанса для проверки правильности пользователя кажется хорошим решением. Для получения разрешений вы можете взглянуть на роли узлов .
Робертклеп

2
Что ты наконец сделал для этого? Любой код котельной пластины (сервер / клиент мобильного приложения), которым вы можете поделиться?
Мортеза Шахриари Ниа

Ответы:


176

У меня была та же проблема, которую вы описали. Веб-сайт, который я создаю, доступен с мобильного телефона и из браузера, поэтому мне нужен API, чтобы пользователи могли зарегистрироваться, войти в систему и выполнить некоторые конкретные задачи. Кроме того, мне нужно поддерживать масштабируемость, один и тот же код, работающий на разных процессах / машинах.

Поскольку пользователи могут СОЗДАТЬ ресурсы (также называемые действиями POST / PUT), вам необходимо защитить свой API. Вы можете использовать oauth или создать собственное решение, но имейте в виду, что все решения могут быть взломаны, если пароль действительно легко найти. Основная идея состоит в том, чтобы аутентифицировать пользователей, используя имя пользователя, пароль и токен, он же apitoken. Этот apitoken может быть сгенерирован с помощью node-uuid, а пароль - с помощью pbkdf2.

Затем вам нужно где-то сохранить сессию. Если вы сохраните его в памяти в виде простого объекта, если вы убьете сервер и перезагрузите его снова, сеанс будет уничтожен. Кроме того, это не масштабируется. Если вы используете haproxy для балансировки нагрузки между компьютерами или просто используете рабочих, это состояние сеанса будет сохранено в одном процессе, поэтому, если тот же пользователь перенаправлен на другой процесс / машину, ему потребуется снова пройти аутентификацию. Поэтому вам нужно хранить сессию в общем месте. Обычно это делается с помощью Redis.

Когда пользователь проходит проверку подлинности (имя пользователя + пароль + apitoken), генерируется еще один токен для сеанса, он же accesstoken. Опять же, с node-uuid. Отправьте пользователю маркер доступа и ИД пользователя. Идентификатор пользователя (ключ) и токен доступа (значение) сохраняются в redis со временем истечения срока действия, например, 1 час.

Теперь каждый раз, когда пользователь выполняет какую-либо операцию, используя остальные API, ему нужно будет отправить идентификатор пользователя и маркер доступа.

Если вы разрешите пользователям регистрироваться, используя остальные API, вам нужно будет создать учетную запись администратора с помощью apitoken администратора и сохранить их в мобильном приложении (зашифровать имя пользователя + пароль + apitoken), поскольку у новых пользователей не будет apitoken, когда они подписываются.

Интернет также использует этот API, но вам не нужно использовать apitokens. Вы можете использовать Express с Redis Store или использовать тот же метод, описанный выше, но обходя проверку apitoken и возвращая пользователю идентификатор пользователя + accesstoken в cookie.

Если у вас есть личные области, сравните имя пользователя с разрешенными пользователями при аутентификации. Вы также можете применять роли к пользователям.

Резюме:

схема последовательности

Альтернативой без использования apitoken было бы использование HTTPS и отправка имени пользователя и пароля в заголовке авторизации и кэширование имени пользователя в redis.


1
Я также использую mongodb, но управлять им довольно просто, если сохранить сессию (accesstoken) с помощью redis (использовать атомарные операции). Апитокен генерируется на сервере, когда пользователь создает учетную запись и отправляет ее обратно пользователю. Затем, когда пользователь хочет пройти аутентификацию, он должен отправить имя пользователя + пароль + apitoken (поместить их в тело http). Имейте в виду, что HTTP не шифрует тело, поэтому пароль и apitoken могут быть обнаружены. Используйте HTTPS, если это вас беспокоит.
Габриэль Ламас

1
какой смысл использовать apitoken? это "вторичный" пароль?
Salvatorelab

2
@TheBronx Apitoken имеет 2 варианта использования: 1) с помощью apitoken вы можете контролировать доступ пользователей к вашей системе, а также отслеживать и создавать статистику по каждому пользователю. 2) Это дополнительная мера безопасности, «вторичный» пароль.
Габриэль Ламас

1
Почему вы должны отправлять идентификатор пользователя снова и снова после успешной аутентификации. Токен должен быть единственным секретом, необходимым для выполнения вызовов API.
Аксель Наполитано

1
Идея токена - помимо злоупотребления им для отслеживания активности пользователя - заключается в том, что пользователю в идеале не нужно никакого имени пользователя и пароля для использования приложения: токен является уникальным ключом доступа. Это позволяет пользователям в любое время отбрасывать любую клавишу, затрагивая только приложение, но не учетную запись пользователя. Для веб-сервиса токен довольно неудобен - поэтому начальный вход в систему для сеанса - это место, где пользователь получает этот токен - для «обычного» клиента ab токен не проблема: введите его один раз, и вы почти закончили ;)
Аксель Наполитано

22

Я хотел бы внести этот код в качестве структурного решения поставленного вопроса в соответствии с (я надеюсь, что так) принятым ответом. (Вы можете очень легко настроить его).

// ------------------------------------------------------
// server.js 

// .......................................................
// requires
var fs = require('fs');
var express = require('express'); 
var myBusinessLogic = require('../businessLogic/businessLogic.js');

// .......................................................
// security options

/*
1. Generate a self-signed certificate-key pair
openssl req -newkey rsa:2048 -new -nodes -x509 -days 3650 -keyout key.pem -out certificate.pem

2. Import them to a keystore (some programs use a keystore)
keytool -importcert -file certificate.pem -keystore my.keystore
*/

var securityOptions = {
    key: fs.readFileSync('key.pem'),
    cert: fs.readFileSync('certificate.pem'),
    requestCert: true
};

// .......................................................
// create the secure server (HTTPS)

var app = express();
var secureServer = require('https').createServer(securityOptions, app);

// ------------------------------------------------------
// helper functions for auth

// .............................................
// true if req == GET /login 

function isGETLogin (req) {
    if (req.path != "/login") { return false; }
    if ( req.method != "GET" ) { return false; }
    return true;
} // ()

// .............................................
// your auth policy  here:
// true if req does have permissions
// (you may check here permissions and roles 
//  allowed to access the REST action depending
//  on the URI being accessed)

function reqHasPermission (req) {
    // decode req.accessToken, extract 
    // supposed fields there: userId:roleId:expiryTime
    // and check them

    // for the moment we do a very rigorous check
    if (req.headers.accessToken != "you-are-welcome") {
        return false;
    }
    return true;
} // ()

// ------------------------------------------------------
// install a function to transparently perform the auth check
// of incoming request, BEFORE they are actually invoked

app.use (function(req, res, next) {
    if (! isGETLogin (req) ) {
        if (! reqHasPermission (req) ){
            res.writeHead(401);  // unauthorized
            res.end();
            return; // don't call next()
        }
    } else {
        console.log (" * is a login request ");
    }
    next(); // continue processing the request
});

// ------------------------------------------------------
// copy everything in the req body to req.body

app.use (function(req, res, next) {
    var data='';
    req.setEncoding('utf8');
    req.on('data', function(chunk) { 
       data += chunk;
    });
    req.on('end', function() {
        req.body = data;
        next(); 
    });
});

// ------------------------------------------------------
// REST requests
// ------------------------------------------------------

// .......................................................
// authenticating method
// GET /login?user=xxx&password=yyy

app.get('/login', function(req, res){
    var user = req.query.user;
    var password = req.query.password;

    // rigorous auth check of user-passwrod
    if (user != "foobar" || password != "1234") {
        res.writeHead(403);  // forbidden
    } else {
        // OK: create an access token with fields user, role and expiry time, hash it
        // and put it on a response header field
        res.setHeader ('accessToken', "you-are-welcome");
        res.writeHead(200); 
    }
    res.end();
});

// .......................................................
// "regular" methods (just an example)
// newBook()
// PUT /book

app.put('/book', function (req,res){
    var bookData = JSON.parse (req.body);

    myBusinessLogic.newBook(bookData, function (err) {
        if (err) {
            res.writeHead(409);
            res.end();
            return;
        }
        // no error:
        res.writeHead(200);
        res.end();
    });
});

// .......................................................
// "main()"

secureServer.listen (8081);

Этот сервер можно протестировать с помощью curl:

echo "----   first: do login "
curl -v "https://localhost:8081/login?user=foobar&password=1234" --cacert certificate.pem

# now, in a real case, you should copy the accessToken received before, in the following request

echo "----  new book"
curl -X POST  -d '{"id": "12341324", "author": "Herman Melville", "title": "Moby-Dick"}' "https://localhost:8081/book" --cacert certificate.pem --header "accessToken: you-are-welcome" 

Спасибо за этот пример, он очень полезен, однако я пытаюсь следовать этому, и когда я подключаюсь, чтобы войти, он говорит следующее: curl: (51) SSL: имя субъекта сертификата 'xxxx' не соответствует имени целевого хоста 'xxx.net'. Я жестко запрограммировал мои / etc / hosts, чтобы разрешить подключение https на той же машине
mastervv

11

Я только что закончил пример приложения, которое делает это довольно простым, но понятным способом. Он использует mongoose с mongodb для хранения пользователей и паспорт для управления аутентификацией.

https://github.com/Khelldar/Angular-Express-Train-Seed


7
Вы используете куки для защиты API. Я не думаю, что это правильно.
Винс Юань

9

Есть много вопросов о шаблонах аутентификации REST здесь на SO. Вот наиболее актуальные для вашего вопроса:

В основном вам нужно выбирать между использованием ключей API (наименее безопасным, так как ключ может быть обнаружен неавторизованным пользователем), ключом приложения и комбинацией токенов (средний) или полной реализацией OAuth (наиболее безопасный).


Я много читал об oauth 1.0 и oauth 2.0, и обе версии кажутся не очень безопасными. Википедия написала, что в oauth 1.0 есть некоторые утечки безопасности. Также я нашел статью о том, что один из разработчиков ядра покидает команду, потому что oauth 2.0 небезопасен.
tschiela

12
@tschiela Вы должны добавить ссылки на все, что вы цитируете здесь.
mikemaccana

3

Если вы хотите защитить свое приложение, то вам определенно следует начать с использования HTTPS вместо HTTP , это обеспечит создание безопасного канала между вами и пользователями, что предотвратит перехват данных, отправляемых пользователям назад и вперед, и поможет сохранить данные обменялись конфиденциально.

Вы можете использовать JWT (JSON Web Tokens) для защиты API RESTful , это имеет много преимуществ по сравнению с сеансами на стороне сервера, преимущества в основном:

1 - Более масштабируемый, так как ваши серверы API не должны будут поддерживать сеансы для каждого пользователя (что может быть большим бременем, когда у вас много сеансов)

2. JWT являются самодостаточными и имеют утверждения, которые определяют роль пользователя, например, и к чему он может получить доступ, а также выдан на дату и дату истечения срока действия (после чего JWT не будет действительным)

3. Легче обрабатывать балансировщики нагрузки, и если у вас есть несколько серверов API, поскольку вам не нужно обмениваться данными сеанса и не настраивать сервер для маршрутизации сеанса на один и тот же сервер, когда запрос с JWT попадает на любой сервер, он может быть аутентифицирован & авторизовано

4- Меньшая нагрузка на вашу базу данных, а также вам не придется постоянно хранить и извлекать идентификатор сессии и данные для каждого запроса

5- JWT не могут быть подделаны, если вы используете сильный ключ для подписи JWT, поэтому вы можете доверять утверждениям в JWT, отправляемом с запросом, без необходимости проверять сеанс пользователя и проверять, авторизован он или нет , вы можете просто проверить JWT, и тогда вы все будете знать, кто и что может сделать этот пользователь.

Многие библиотеки предоставляют простые способы создания и проверки JWT на большинстве языков программирования, например: в node.js одним из самых популярных является jsonwebtoken

Так как API REST обычно стремятся сохранить сервер без сохранения состояния, JWT более совместимы с этой концепцией, поскольку каждый запрос отправляется с помощью автономного токена авторизации (JWT), при этом серверу не нужно отслеживать сеанс пользователя по сравнению с сеансами, которые делают Сервер с состоянием, так что он запоминает пользователя и его роль, однако сессии также широко используются и имеют свои плюсы, которые вы можете искать, если хотите.

Важно отметить, что вы должны безопасно доставить JWT клиенту, используя HTTPS, и сохранить его в безопасном месте (например, в локальном хранилище).

Вы можете узнать больше о JWT по этой ссылке


1
Мне нравится ваш ответ, который кажется лучшим обновлением из этого старого вопроса. Я задал себе другой вопрос на ту же тему, и вы могли бы также помочь. => stackoverflow.com/questions/58076644/…
pbonnefoi

Спасибо, рад, что смог помочь, выкладываю ответ на ваш вопрос
Ахмед Элкуси

2

Если вы хотите иметь полностью заблокированную область вашего веб-приложения, доступ к которой могут получить только администраторы вашей компании, тогда SSL-авторизация может быть для вас. Это гарантирует, что никто не сможет подключиться к экземпляру сервера, если в его браузере не установлен авторизованный сертификат. На прошлой неделе я написал статью о том, как настроить сервер: Статья

Это одна из самых безопасных настроек, которую вы найдете, так как здесь не используются имена пользователей и пароли, поэтому никто не сможет получить доступ, пока один из ваших пользователей не передаст файлы ключей потенциальному хакеру.


хорошая статья Но приватная зона для пользователей.
tschiela

Спасибо - правильно, тогда вам нужно искать другое решение, распространение сертификатов было бы проблемой.
ExxKA
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.