วิธีการนำ REST API ที่ปลอดภัยไปใช้กับ node.js


204

ฉันเริ่มวางแผน REST API ด้วย node.js, express และ mongodb API ให้ข้อมูลสำหรับเว็บไซต์ (พื้นที่สาธารณะและส่วนตัว) และอาจเป็นแอปมือถือในภายหลัง ส่วนหน้าจะถูกพัฒนาด้วย AngularJS

บางวันฉันได้อ่านมากมายเกี่ยวกับการรักษาความปลอดภัย REST APIs แต่ฉันไม่ได้รับการแก้ไขขั้นสุดท้าย เท่าที่ฉันเข้าใจคือการใช้ HTTPS เพื่อให้ความปลอดภัยขั้นพื้นฐาน แต่ฉันจะป้องกัน API ในกรณีการใช้งานได้อย่างไร:

  • ผู้เยี่ยมชม / ผู้ใช้เว็บไซต์ / แอพเท่านั้นที่ได้รับอนุญาตให้รับข้อมูลสำหรับพื้นที่สาธารณะของเว็บไซต์ / แอพ

  • ผู้ใช้ที่ผ่านการรับรองและได้รับอนุญาตเท่านั้นที่ได้รับอนุญาตให้รับข้อมูลสำหรับพื้นที่ส่วนตัว (และเฉพาะข้อมูลที่ผู้ใช้ให้สิทธิ์)

ในขณะนี้ฉันคิดว่าจะอนุญาตให้ผู้ใช้ที่มีเซสชันที่ใช้งานอยู่ใช้ API เท่านั้น ในการอนุญาตผู้ใช้ฉันจะใช้หนังสือเดินทางและขออนุญาตฉันต้องใช้บางสิ่งเพื่อตัวเอง ทั้งหมดอยู่ด้านบนของ HTTPS

ใครสามารถให้การฝึกฝนหรือประสบการณ์ที่ดีที่สุดได้บ้าง? มี "สถาปัตยกรรม" ของฉันขาดหรือไม่


2
ฉันเดาว่า API นี้จะใช้จากส่วนหน้าที่คุณให้เท่านั้น ในกรณีดังกล่าวการใช้เซสชันเพื่อให้แน่ใจว่าผู้ใช้นั้นถูกต้องดูเหมือนเป็นทางออกที่ดี สำหรับสิทธิ์คุณอาจจะดูที่โหนดบทบาท
robertklep

2
ในที่สุดคุณทำอะไรเพื่อสิ่งนี้ รหัสแผ่นหม้อไอน้ำใด ๆ (ไคลเอนต์เซิร์ฟเวอร์ / แอปมือถือ) ที่คุณสามารถแชร์ได้
Morteza Shahriari Nia

คำตอบ:


175

ฉันมีปัญหาเดียวกันกับที่คุณอธิบาย เว็บไซต์ที่ฉันกำลังสร้างสามารถเข้าถึงได้จากโทรศัพท์มือถือและจากเบราว์เซอร์ดังนั้นฉันจึงต้องมี API เพื่ออนุญาตให้ผู้ใช้ลงชื่อสมัครเข้าใช้และทำงานบางอย่าง นอกจากนี้ฉันต้องรองรับการปรับขยายได้ซึ่งเป็นรหัสเดียวกันที่ทำงานบนกระบวนการ / เครื่องจักรที่แตกต่างกัน

เนื่องจากผู้ใช้สามารถสร้างทรัพยากร (aka การกระทำ POST / PUT) คุณต้องรักษาความปลอดภัย API ของคุณ คุณสามารถใช้ oauth หรือคุณสามารถสร้างวิธีการแก้ปัญหาของคุณเอง แต่โปรดจำไว้ว่าการแก้ปัญหาทั้งหมดสามารถใช้งานไม่ได้หากรหัสผ่านที่ค้นพบได้ง่ายจริงๆ แนวคิดพื้นฐานคือการตรวจสอบผู้ใช้โดยใช้ชื่อผู้ใช้รหัสผ่านและโทเค็นหรือที่รู้จักกันในชื่อ apitoken apitoken นี้สามารถสร้างขึ้นได้โดยใช้node-uuidและรหัสผ่านสามารถถูกแฮชโดยใช้pbkdf2

จากนั้นคุณต้องบันทึกเซสชัน หากคุณบันทึกไว้ในหน่วยความจำในวัตถุธรรมดาถ้าคุณฆ่าเซิร์ฟเวอร์และรีบูตอีกครั้งเซสชันจะถูกทำลาย นอกจากนี้ยังไม่สามารถปรับขนาดได้ หากคุณใช้ haproxy เพื่อโหลดบาลานซ์ระหว่างเครื่องหรือเพียงแค่ใช้คนงานสถานะเซสชันนี้จะถูกเก็บไว้ในกระบวนการเดียวดังนั้นหากผู้ใช้เดียวกันถูกเปลี่ยนเส้นทางไปยังกระบวนการ / เครื่องอื่นจะต้องตรวจสอบอีกครั้ง ดังนั้นคุณต้องจัดเก็บเซสชั่นในสถานที่ทั่วไป โดยทั่วไปจะทำได้โดยใช้ Redis

เมื่อผู้ใช้มีการรับรองความถูกต้อง (ชื่อผู้ใช้ + รหัสผ่าน + apitoken) สร้างโทเค็นอื่นสำหรับเซสชั่น aka aka accesstoken อีกครั้งกับ node-uuid ส่งไปยังผู้ใช้ accesstoken และหมายเลขผู้ใช้ หมายเลขผู้ใช้ (คีย์) และ accesstoken (ค่า) จะถูกเก็บไว้ใน Redis ด้วยและเวลาหมดอายุเช่น 1 ชั่วโมง

ตอนนี้ทุกครั้งที่ผู้ใช้ดำเนินการใด ๆ โดยใช้ส่วนที่เหลือ API จะต้องส่งหมายเลขผู้ใช้และ accesstoken

หากคุณอนุญาตให้ผู้ใช้ลงทะเบียนโดยใช้ api ส่วนที่เหลือคุณจะต้องสร้างบัญชีผู้ดูแลระบบด้วยผู้ดูแลระบบ apitoken และเก็บไว้ในแอพมือถือ (เข้ารหัสชื่อผู้ใช้ + รหัสผ่าน + apitoken) เพราะผู้ใช้ใหม่จะไม่มี apitoken เมื่อ พวกเขาลงทะเบียน

เว็บยังใช้ API นี้ แต่คุณไม่จำเป็นต้องใช้ apitokens คุณสามารถใช้ express กับร้าน redis หรือใช้เทคนิคเดียวกับที่อธิบายข้างต้น แต่ผ่านการตรวจสอบ apitoken และกลับไปยังผู้ใช้ userid + accesstoken ในคุกกี้

หากคุณมีพื้นที่ส่วนตัวให้เปรียบเทียบชื่อผู้ใช้กับผู้ใช้ที่ได้รับอนุญาตเมื่อพวกเขารับรองความถูกต้อง คุณยังสามารถใช้บทบาทกับผู้ใช้

สรุป:

แผนภาพลำดับ

ทางเลือกอื่นที่ไม่มี apitoken คือการใช้ HTTPS และเพื่อส่งชื่อผู้ใช้และรหัสผ่านในส่วนหัวการอนุญาตและแคชชื่อผู้ใช้เป็นสีแดง


1
ฉันยังใช้ mongodb แต่มันง่ายในการจัดการถ้าคุณบันทึกเซสชัน (accesstoken) โดยใช้ redis (ใช้การปฏิบัติการปรมาณู) apitoken ถูกสร้างขึ้นในเซิร์ฟเวอร์เมื่อผู้ใช้สร้างบัญชีและส่งกลับไปยังผู้ใช้ จากนั้นเมื่อผู้ใช้ต้องการตรวจสอบสิทธิ์นั้นจะต้องส่งชื่อผู้ใช้ + รหัสผ่าน + apitoken (ใส่ไว้ใน http ร่างกาย) โปรดทราบว่า HTTP ไม่ได้เข้ารหัสร่างกายเพื่อให้สามารถดมรหัสผ่านและ apitoken ได้ ใช้ HTTPS หากเป็นข้อกังวลสำหรับคุณ
Gabriel Llamas

1
จุดประสงค์ในการใช้apitokenคืออะไร เป็นรหัสผ่าน "รอง" หรือไม่
Salvatorelab

2
@TheBronx apitoken มี 2 กรณีการใช้งาน: 1) ด้วย apitoken คุณสามารถควบคุมการเข้าถึงของผู้ใช้กับระบบของคุณและคุณสามารถตรวจสอบและสร้างสถิติของผู้ใช้แต่ละคน 2) เป็นมาตรการรักษาความปลอดภัยเพิ่มเติมเป็นรหัสผ่าน "รอง"
Gabriel Llamas

1
ทำไมคุณควรส่งรหัสผู้ใช้ซ้ำแล้วซ้ำอีกหลังจากตรวจสอบสิทธิ์สำเร็จแล้ว โทเค็นควรเป็นความลับเดียวที่คุณต้องใช้เพื่อทำการเรียก API
Axel Napolitano

1
แนวคิดของโทเค็น - นอกเหนือจากการใช้เพื่อติดตามกิจกรรมของผู้ใช้ที่ไม่เหมาะสมคือผู้ใช้ไม่จำเป็นต้องมีชื่อผู้ใช้และรหัสผ่านสำหรับการใช้แอปพลิเคชัน: โทเค็นเป็นรหัสการเข้าถึงที่ไม่ซ้ำกัน วิธีนี้ทำให้ผู้ใช้สามารถวางกุญแจได้ทุกเมื่อที่มีผลกระทบต่อแอพเท่านั้น แต่ไม่รวมถึงบัญชีผู้ใช้ สำหรับเว็บเซอร์โทเค็นค่อนข้างไม่สะดวก - นั่นเป็นสาเหตุที่การล็อกอินเริ่มต้นสำหรับเซสชันคือสถานที่ที่ผู้ใช้รับโทเค็นนั้น - สำหรับลูกค้า "ปกติ" โทเค็นโทเค็นไม่มีปัญหา: ป้อนเพียงครั้งเดียว ;)
Axel Napolitano

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' ฉัน hardcoded ของฉัน / etc / hosts อนุญาตให้เชื่อมต่อ https บนเครื่องเดียวกันได้
ไหม

11

ฉันเพิ่งเสร็จสิ้นแอพตัวอย่างที่ทำสิ่งนี้ในแบบพื้นฐานที่ค่อนข้างชัดเจน มันใช้พังพอนกับ mongodb ในการจัดเก็บผู้ใช้และหนังสือเดินทางสำหรับการจัดการรับรองความถูกต้อง

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


7
คุณกำลังใช้คุกกี้เพื่อรักษาความปลอดภัย API ฉันไม่คิดว่ามันถูกต้อง
วินซ์หยวน

9

มีคำถามมากมายเกี่ยวกับรูปแบบ REST auth ที่นี่ใน SO สิ่งเหล่านี้เกี่ยวข้องกับคำถามของคุณมากที่สุด:

โดยทั่วไปคุณต้องเลือกระหว่างการใช้คีย์ API (ปลอดภัยน้อยที่สุดเนื่องจากผู้ใช้ที่ไม่ได้รับอนุญาตอาจค้นพบกุญแจ) คีย์แอปและโทเค็นคอมโบ (กลาง) หรือการใช้ OAuth เต็มรูปแบบ (ปลอดภัยที่สุด)


ฉันอ่านมากเกี่ยวกับ oauth 1.0 และ oauth 2.0 และทั้งสองรุ่นดูเหมือนจะไม่ปลอดภัยมาก Wikipedia เขียนว่ามีการรั่วไหลของความปลอดภัยใน oauth 1.0 ฉันยังพบบทความที่เกี่ยวกับหนึ่งในนักพัฒนาหลักออกจากทีมเพราะ oauth 2.0 คือการไม่ปลอดภัย
tschiela

12
@tschiela คุณควรเพิ่มการอ้างอิงถึงสิ่งที่คุณอ้างถึงที่นี่
mikemaccana

3

หากคุณต้องการรักษาความปลอดภัยแอปพลิเคชันของคุณคุณควรเริ่มต้นอย่างแน่นอนด้วยการใช้ HTTPS แทน HTTPสิ่งนี้จะช่วยสร้างความมั่นใจในการสร้างช่องทางที่ปลอดภัยระหว่างคุณกับผู้ใช้ที่จะป้องกันไม่ให้ดมข้อมูลที่ส่งกลับไปยังผู้ใช้ แลกเปลี่ยนเป็นความลับ

คุณสามารถใช้ JWTs (JSON Web Tokens) เพื่อรักษาความปลอดภัย RESTful APIsซึ่งมีประโยชน์มากมายเมื่อเทียบกับเซสชันฝั่งเซิร์ฟเวอร์ประโยชน์ส่วนใหญ่:

1- ปรับขนาดได้มากขึ้นเนื่องจากเซิร์ฟเวอร์ API ของคุณจะไม่ต้องรักษาเซสชันสำหรับผู้ใช้แต่ละคน (ซึ่งอาจเป็นภาระใหญ่เมื่อคุณมีหลายเซสชัน)

2- เจดับบลิวทีมีอยู่ในตัวเอง & มีการเรียกร้องที่กำหนดบทบาทของผู้ใช้เช่น & สิ่งที่เขาสามารถเข้าถึงและออก ณ วันที่ & วันหมดอายุ (หลังจากที่เจดับบลิวทีจะไม่ถูกต้อง)

3- ง่ายต่อการจัดการข้าม load balancer & หากคุณมีเซิร์ฟเวอร์ API หลายตัวเนื่องจากคุณไม่ต้องแชร์ข้อมูลเซสชันหรือกำหนดค่าเซิร์ฟเวอร์เพื่อกำหนดเส้นทางเซสชันไปยังเซิร์ฟเวอร์เดียวกันเมื่อใดก็ตามที่คำขอที่มี JWT เข้าชมเซิร์ฟเวอร์ใด ๆ & ได้รับอนุญาต

4- ลดแรงกดดันต่อฐานข้อมูลของคุณรวมถึงคุณไม่จำเป็นต้องจัดเก็บและรับรหัสเซสชันและข้อมูลสำหรับคำขอแต่ละครั้ง

5- คุณไม่สามารถดัดแปลง JWT ได้หากคุณใช้รหัสที่รัดกุมเพื่อลงชื่อ JWT ดังนั้นคุณสามารถเชื่อถือข้อเรียกร้องใน JWT ที่ส่งมาพร้อมกับคำขอโดยไม่ต้องตรวจสอบเซสชันผู้ใช้และไม่ว่าเขาจะได้รับอนุญาตหรือไม่ คุณสามารถตรวจสอบ JWT จากนั้นคุณพร้อมที่จะรู้ว่าใครและสิ่งที่ผู้ใช้นี้สามารถทำได้

ห้องสมุดหลายแห่งมีวิธีง่าย ๆ ในการสร้างและตรวจสอบ JWT ในภาษาการเขียนโปรแกรมส่วนใหญ่ตัวอย่างเช่น: ใน node.js หนึ่งในความนิยมมากที่สุดคือjsonwebtoken

เนื่องจาก REST APIs โดยทั่วไปมีจุดประสงค์เพื่อให้เซิร์ฟเวอร์ไร้สถานะดังนั้นJWTจึงเข้ากันได้กับแนวคิดดังกล่าวมากขึ้นเนื่องจากแต่ละคำขอจะถูกส่งด้วยโทเค็นการอนุญาตที่มีอยู่ในตัวเอง(JWT)โดยเซิร์ฟเวอร์ไม่ต้องติดตามเซสชันของผู้ใช้ เซิร์ฟเวอร์แจ้งว่าผู้ใช้และบทบาทของเขาจำได้อย่างไรเซสชันก็ถูกใช้อย่างกว้างขวาง & มีข้อดีของพวกเขาซึ่งคุณสามารถค้นหาได้หากคุณต้องการ

สิ่งสำคัญที่ควรทราบคือคุณต้องส่งมอบ JWT ให้กับลูกค้าอย่างปลอดภัยโดยใช้ HTTPS และบันทึกไว้ในที่ปลอดภัย (ตัวอย่างเช่นในที่จัดเก็บในตัวเครื่อง)

คุณสามารถเรียนรู้เพิ่มเติมเกี่ยวกับ JWT ได้จากลิงค์นี้


1
ฉันชอบคำตอบของคุณที่ดูเหมือนจะเป็นการปรับปรุงที่ดีที่สุดจากคำถามเก่านี้ ฉันได้ถามคำถามอื่นกับตัวเองในหัวข้อเดียวกันและคุณอาจมีประโยชน์เช่นกัน => stackoverflow.com/questions/58076644/…
pbonnefoi

ขอบคุณดีใจที่ฉันสามารถช่วยฉันโพสต์คำตอบสำหรับคำถามของคุณ
Ahmed Elkoussy

2

หากคุณต้องการให้มีพื้นที่ที่ถูกล็อคไว้อย่างสมบูรณ์ของการใช้งานเว็บของคุณซึ่งผู้ดูแลระบบสามารถเข้าถึงได้จาก บริษัท ของคุณเท่านั้นการอนุญาต SSL อาจเหมาะสำหรับคุณ มันจะประกันว่าไม่มีใครสามารถเชื่อมต่อกับอินสแตนซ์ของเซิร์ฟเวอร์ได้เว้นแต่จะมีใบรับรองที่ได้รับอนุญาตติดตั้งในเบราว์เซอร์ สัปดาห์ที่แล้วฉันเขียนบทความเกี่ยวกับวิธีการตั้งค่าเซิร์ฟเวอร์: บทความ

นี่เป็นหนึ่งในการตั้งค่าที่ปลอดภัยที่สุดที่คุณจะพบเนื่องจากไม่มีชื่อผู้ใช้ / รหัสผ่านที่เกี่ยวข้องดังนั้นจึงไม่มีใครสามารถเข้าถึงได้เว้นแต่ผู้ใช้คนใดคนหนึ่งของคุณมอบไฟล์สำคัญให้แฮกเกอร์ที่มีศักยภาพ


บทความที่ดี แต่พื้นที่ส่วนตัวสำหรับผู้ใช้
tschiela

ขอบคุณ - ใช่แล้วคุณควรไปหาทางออกอื่นการแจกจ่ายใบรับรองจะเป็นความเจ็บปวด
ExxKA
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.