Cách triển khai API REST an toàn với node.js


204

Tôi bắt đầu lập kế hoạch API REST với node.js, express và mongodb. API cung cấp dữ liệu cho một trang web (khu vực công cộng và riêng tư) và sau đó có thể là một ứng dụng di động. Frontend sẽ được phát triển với AngularJS.

Trong một số ngày, tôi đã đọc rất nhiều về việc bảo mật API REST, nhưng tôi không đi đến một giải pháp cuối cùng. Theo tôi hiểu là sử dụng HTTPS để cung cấp bảo mật cơ bản. Nhưng làm thế nào tôi có thể bảo vệ API trong các trường hợp sử dụng đó:

  • Chỉ khách truy cập / người dùng của trang web / ứng dụng mới được phép lấy dữ liệu cho khu vực công cộng của trang web / ứng dụng

  • Chỉ người dùng được xác thực và được ủy quyền mới được phép lấy dữ liệu cho khu vực riêng tư (và chỉ dữ liệu, nơi người dùng được cấp quyền)

Hiện tại tôi nghĩ sẽ chỉ cho phép người dùng có phiên hoạt động sử dụng API. Để ủy quyền cho người dùng, tôi sẽ sử dụng hộ chiếu và để được cho phép, tôi cần phải tự mình thực hiện điều gì đó. Tất cả nằm trên đỉnh của HTTPS.

Ai đó có thể cung cấp một số thực hành hoặc kinh nghiệm tốt nhất? Có thiếu trong kiến ​​trúc của tôi không?


2
Tôi đoán API chỉ được sử dụng từ giao diện bạn cung cấp? Trong trường hợp đó, sử dụng phiên để đảm bảo người dùng hợp lệ có vẻ như là một giải pháp tốt. Để có quyền, bạn có thể xem vai trò của nút .
robertklep

2
Cuối cùng bạn đã làm gì cho điều này? Bất kỳ mã tấm nồi hơi (máy chủ / ứng dụng khách ứng dụng di động) bạn có thể chia sẻ?
Morteza Shahriari Nia

Câu trả lời:


175

Tôi đã có cùng một vấn đề bạn mô tả. Trang web tôi đang xây dựng có thể được truy cập từ điện thoại di động và từ trình duyệt vì vậy tôi cần một api để cho phép người dùng đăng ký, đăng nhập và thực hiện một số tác vụ cụ thể. Hơn nữa, tôi cần hỗ trợ khả năng mở rộng, cùng một mã chạy trên các quy trình / máy khác nhau.

Bởi vì người dùng có thể TẠO tài nguyên (còn gọi là hành động POST / PUT), bạn cần bảo mật api của mình. Bạn có thể sử dụng oauth hoặc bạn có thể xây dựng giải pháp của riêng mình nhưng hãy nhớ rằng tất cả các giải pháp đều có thể bị phá vỡ nếu mật khẩu thực sự dễ dàng khám phá. Ý tưởng cơ bản là xác thực người dùng bằng tên người dùng, mật khẩu và mã thông báo, còn gọi là apitoken. Apitoken này có thể được tạo bằng nút-uuid và mật khẩu có thể được băm bằng pbkdf2

Sau đó, bạn cần lưu phiên ở đâu đó. Nếu bạn lưu nó vào bộ nhớ trong một đối tượng đơn giản, nếu bạn giết máy chủ và khởi động lại thì phiên sẽ bị hủy. Ngoài ra, điều này là không thể mở rộng. Nếu bạn sử dụng haproxy để tải cân bằng giữa các máy hoặc nếu bạn chỉ sử dụng công nhân, trạng thái phiên này sẽ được lưu trữ trong một quy trình duy nhất, vì vậy nếu cùng một người dùng được chuyển hướng đến quy trình / máy khác, nó sẽ cần xác thực lại. Do đó, bạn cần lưu trữ phiên ở một nơi chung. Điều này thường được thực hiện bằng cách sử dụng redis.

Khi người dùng được xác thực (tên người dùng + mật khẩu + apitoken) sẽ tạo một mã thông báo khác cho phiên, còn gọi là accesstoken. Một lần nữa, với nút-uuid. Gửi cho người dùng accesstoken và userid. Userid (khóa) và accesstoken (giá trị) được lưu trữ trong redis với và hết thời gian, ví dụ 1h.

Bây giờ, mỗi khi người dùng thực hiện bất kỳ thao tác nào bằng cách sử dụng api còn lại, nó sẽ cần gửi userid và accesstoken.

Nếu bạn cho phép người dùng đăng ký bằng api còn lại, bạn sẽ cần tạo tài khoản quản trị viên với lời xin lỗi quản trị viên và lưu trữ chúng trong ứng dụng di động (mã hóa tên người dùng + mật khẩu + apitoken) vì người dùng mới sẽ không có lời xin lỗi khi họ đăng ký.

Web cũng sử dụng api này nhưng bạn không cần sử dụng apitokens. Bạn có thể sử dụng express với cửa hàng redis hoặc sử dụng kỹ thuật tương tự được mô tả ở trên nhưng bỏ qua kiểm tra apitoken và trả lại cho người dùng userid + accesstoken trong cookie.

Nếu bạn có khu vực riêng, hãy so sánh tên người dùng với người dùng được phép khi họ xác thực. Bạn cũng có thể áp dụng vai trò cho người dùng.

Tóm lược:

sơ đồ trình tự

Một cách khác mà không có apitoken sẽ là sử dụng HTTPS và gửi tên người dùng và mật khẩu trong tiêu đề Ủy quyền và lưu trữ tên người dùng trong redis.


1
Tôi cũng sử dụng mongodb nhưng nó khá dễ quản lý nếu bạn lưu phiên (accesstoken) bằng redis (sử dụng các thao tác nguyên tử). Apitoken được tạo trong máy chủ khi người dùng tạo tài khoản và gửi lại cho người dùng. Sau đó, khi người dùng muốn xác thực, nó phải gửi tên người dùng + mật khẩu + apitoken (đặt chúng vào phần thân http). Hãy nhớ rằng HTTP không mã hóa cơ thể để mật khẩu và apitoken có thể được đánh hơi. Sử dụng HTTPS nếu đây là một mối quan tâm cho bạn.
Gabriel Llamas

1
những gì về việc sử dụng một apitoken? nó có phải là mật khẩu "phụ" không?
Salvatorelab

2
@TheBronx Apitoken có 2 trường hợp sử dụng: 1) với apitoken bạn có thể kiểm soát quyền truy cập của người dùng vào hệ thống của mình và bạn có thể theo dõi và xây dựng số liệu thống kê của từng người dùng. 2) Đó là một biện pháp bảo mật bổ sung, mật khẩu "phụ".
Gabriel Llamas

1
Tại sao bạn nên gửi id người dùng nhiều lần sau khi xác thực thành công. Mã thông báo phải là bí mật duy nhất bạn cần để thực hiện các lệnh gọi API.
Axel Napolitano

1
Ý tưởng về mã thông báo - bên cạnh việc lạm dụng nó để theo dõi hoạt động của người dùng - là, lý tưởng là người dùng không cần bất kỳ tên người dùng và mật khẩu nào để sử dụng ứng dụng: Mã thông báo là khóa truy cập duy nhất. Điều này cho phép người dùng thả bất kỳ khóa nào bất kỳ lúc nào chỉ ảnh hưởng đến ứng dụng chứ không ảnh hưởng đến tài khoản người dùng. Đối với dịch vụ web, mã thông báo khá khó hiểu - đó là lý do tại sao đăng nhập ban đầu cho phiên là nơi người dùng nhận được mã thông báo đó - đối với máy khách "thông thường", mã thông báo không có vấn đề gì: Nhập một lần và bạn gần như đã hoàn thành ;)
Axel Napolitano

22

Tôi muốn đóng góp mã này như một giải pháp cấu trúc cho câu hỏi được đặt ra, theo (tôi hy vọng vậy) cho câu trả lời được chấp nhận. (Bạn có thể rất dễ dàng tùy chỉnh nó).

// ------------------------------------------------------
// 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);

Máy chủ này có thể được kiểm tra với 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" 

Cảm ơn vì mẫu này rất hữu ích, tuy nhiên tôi cố gắng làm theo, và khi tôi kết nối để đăng nhập, nó nói thế này: curl: (51) SSL: tên chủ đề chứng chỉ 'xxxx' không khớp với tên máy chủ đích 'xxx.net'. Tôi đã mã hóa / etc / hosts của mình để cho phép kết nối https trên cùng một máy
mastervv

11

Tôi vừa hoàn thành một ứng dụng mẫu thực hiện điều này theo một cách khá cơ bản, nhưng rõ ràng. Nó sử dụng cầy mangut với mongodb để lưu trữ người dùng và hộ chiếu để quản lý xác thực.

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


7
Bạn đang sử dụng cookie để bảo mật api. Tôi không nghĩ đó là chính xác.
Vince Yuan

9

Có rất nhiều câu hỏi về các mẫu auth REST ở đây trên SO. Đây là những câu hỏi phù hợp nhất cho câu hỏi của bạn:

Về cơ bản, bạn cần chọn giữa việc sử dụng các khóa API (ít an toàn nhất vì khóa có thể được phát hiện bởi người dùng trái phép), tổ hợp khóa ứng dụng và mã thông báo (trung bình) hoặc triển khai OAuth đầy đủ (an toàn nhất).


Tôi đã đọc rất nhiều về oauth 1.0 và oauth 2.0 và cả hai phiên bản có vẻ không an toàn lắm. Wikipedia đã viết rằng một số rò rỉ bảo mật trong oauth 1.0. Ngoài ra tôi đã tìm thấy một bài viết rằng về một trong những nhà phát triển cốt lõi rời khỏi nhóm vì họ không an toàn.
tschiela

12
@tschiela Bạn nên thêm tài liệu tham khảo cho bất cứ điều gì bạn trích dẫn ở đây.
mikemaccana

3

Nếu bạn muốn bảo mật ứng dụng của mình, thì bạn chắc chắn nên bắt đầu bằng cách sử dụng HTTPS thay vì HTTP , điều này đảm bảo tạo kênh an toàn giữa bạn và người dùng sẽ ngăn chặn việc đánh hơi dữ liệu được gửi qua lại cho người dùng & sẽ giúp giữ dữ liệu trao đổi bí mật.

Bạn có thể sử dụng JWT (Mã thông báo Web JSON) để bảo mật API RESTful , điều này có nhiều lợi ích khi so sánh với các phiên phía máy chủ, các lợi ích chủ yếu là:

1- Khả năng mở rộng hơn, vì các máy chủ API của bạn sẽ không phải duy trì phiên cho mỗi người dùng (đây có thể là một gánh nặng lớn khi bạn có nhiều phiên)

2- Các JWT độc lập và có các khiếu nại xác định vai trò người dùng chẳng hạn & những gì anh ta có thể truy cập và ban hành vào ngày & ngày hết hạn (sau đó JWT sẽ không hợp lệ)

3- Dễ dàng xử lý trên các bộ cân bằng tải hơn và nếu bạn có nhiều máy chủ API vì bạn sẽ không phải chia sẻ dữ liệu phiên cũng như cấu hình máy chủ để định tuyến phiên tới cùng một máy chủ, bất cứ khi nào một yêu cầu với JWT có thể được xác thực & ủy quyền

4- Ít áp lực hơn đối với DB của bạn cũng như bạn sẽ không phải liên tục lưu trữ & truy xuất id & dữ liệu phiên cho mỗi yêu cầu

5- Các JWT không thể bị can thiệp nếu bạn sử dụng khóa mạnh để ký JWT, vì vậy bạn có thể tin tưởng các khiếu nại trong JWT được gửi cùng với yêu cầu mà không phải kiểm tra phiên người dùng & liệu anh ta có được ủy quyền hay không , bạn chỉ có thể kiểm tra JWT và sau đó bạn đã sẵn sàng để biết ai & người dùng này có thể làm gì.

Nhiều thư viện cung cấp các cách dễ dàng để tạo và xác thực JWT trong hầu hết các ngôn ngữ lập trình, ví dụ: trong node.js, một trong những phổ biến nhất là jsonwebtoken

Do API REST thường nhằm mục đích giữ cho máy chủ không trạng thái, do đó JWT tương thích hơn với khái niệm đó vì mỗi yêu cầu được gửi với mã thông báo ủy quyền được bao gồm (JWT) mà không cần máy chủ phải theo dõi phiên người dùng so với phiên làm cho máy chủ có trạng thái để nó ghi nhớ người dùng và vai trò của anh ta, tuy nhiên, các phiên cũng được sử dụng rộng rãi và có ưu điểm của họ, mà bạn có thể tìm kiếm nếu muốn.

Một điều quan trọng cần lưu ý là bạn phải cung cấp JWT an toàn cho khách hàng bằng HTTPS và lưu nó ở nơi an toàn (ví dụ: trong bộ nhớ cục bộ).

Bạn có thể tìm hiểu thêm về JWT từ liên kết này


1
Tôi thích câu trả lời của bạn có vẻ như là bản cập nhật tốt nhất từ ​​câu hỏi cũ này. Tôi đã tự hỏi mình một câu hỏi khác trong cùng một chủ đề và bạn cũng có thể hữu ích. => stackoverflow.com/questions/58076644/ từ
pbonnefoi

Cảm ơn, rất vui vì tôi có thể giúp, tôi đang đăng câu trả lời cho câu hỏi của bạn
Ahmed Elkoussy

2

Nếu bạn muốn có một khu vực bị khóa hoàn toàn trong ứng dụng web của mình mà chỉ có thể được quản trị viên từ công ty của bạn truy cập, thì ủy quyền SSL có thể dành cho bạn. Nó sẽ đảm bảo rằng không ai có thể tạo kết nối đến phiên bản máy chủ trừ khi họ có chứng chỉ được ủy quyền được cài đặt trong trình duyệt của họ. Tuần trước tôi đã viết một bài viết về cách thiết lập máy chủ: Bài viết

Đây là một trong những thiết lập an toàn nhất mà bạn sẽ tìm thấy vì không có tên người dùng / mật khẩu liên quan để không ai có thể truy cập trừ khi một trong những người dùng của bạn trao các tệp chính cho một hacker tiềm năng.


bài viết hay Nhưng khu vực riêng tư là dành cho người dùng.
tschiela

Cảm ơn - đúng rồi, bạn nên đi tìm giải pháp khác, phân phối chứng chỉ sẽ là một nỗi đau.
ExxKA
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.