node.js로 보안 REST API를 구현하는 방법


204

node.js, express 및 mongodb로 REST API 계획을 시작합니다. API는 웹 사이트 (공개 및 개인 영역) 및 나중에 모바일 앱에 대한 데이터를 제공합니다. 프론트 엔드는 AngularJS와 함께 개발 될 것입니다.

며칠 동안 REST API 보안에 대해 많이 읽었지만 최종 솔루션을 얻지 못했습니다. 내가 이해하는 한 HTTPS를 사용하여 기본 보안을 제공하는 것입니다. 그러나 해당 사용 사례에서 API를 보호하는 방법은 다음과 같습니다.

  • 웹 사이트 / 앱 방문자 / 사용자 만 웹 사이트 / 앱의 공개 영역에 대한 데이터를 얻을 수 있습니다

  • 인증되고 권한이 부여 된 사용자 만 개인 영역에 대한 데이터 (및 사용자가 권한을 부여한 데이터 만)를 얻을 수 있습니다.

현재는 활성 세션을 가진 사용자 만 API를 사용할 수있게하려고합니다. 사용자에게 권한을 부여하려면 여권을 사용하고 허가를 받으려면 본인을 위해 무언가를 구현해야합니다. HTTPS 상단에 모두 있습니다.

누군가 모범 사례 나 경험을 제공 할 수 있습니까? "아키텍처"에 부족이 있습니까?


2
귀하가 제공하는 프론트 엔드에서만 API를 사용해야한다고 생각합니까? 이 경우 세션을 사용하여 사용자가 유효한지 확인하는 것이 좋습니다. 권한에 대해서는 node-roles를 살펴볼 수 있습니다 .
robertklep

2
마지막으로 무엇을 했습니까? 보일러 플레이트 코드 (서버 / 모바일 앱 클라이언트)를 공유 할 수 있습니까?
Morteza Shahriari Nia

답변:


175

나는 당신이 묘사 한 것과 같은 문제를 겪었습니다. 내가 구축하는 웹 사이트는 휴대 전화와 브라우저에서 액세스 할 수 있으므로 사용자가 가입, 로그인 및 특정 작업을 수행 할 수 있도록 API가 필요합니다. 또한 다른 프로세스 / 기계에서 실행되는 동일한 코드 인 확장 성을 지원해야합니다.

사용자는 리소스 (POST / PUT 작업이라고도 함)를 만들 수 있으므로 API를 보호해야합니다. oauth를 사용하거나 자체 솔루션을 구축 할 수 있지만 암호를 찾기가 쉬운 경우 모든 솔루션이 손상 될 수 있습니다. 기본 아이디어는 사용자 이름, 비밀번호 및 토큰, 즉 apitoken을 사용하여 사용자를 인증하는 것입니다. 이 apitoken을 사용하여 생성 될 수있는 노드 UUID를 암호 해시가 이용 될 수 PBKDF2을

그런 다음 세션을 어딘가에 저장해야합니다. 메모리에 일반 객체로 저장하면 서버를 종료하고 다시 부팅하면 세션이 삭제됩니다. 또한 이것은 확장 할 수 없습니다. haproxy를 사용하여 시스템간에로드 밸런스를 수행하거나 작업자를 단순히 사용하는 경우이 세션 상태는 단일 프로세스에 저장되므로 동일한 사용자가 다른 프로세스 / 시스템으로 경로 재 지정된 경우 다시 인증해야합니다. 따라서 세션을 공통 위치에 저장해야합니다. 이것은 일반적으로 redis를 사용하여 수행됩니다.

사용자가 인증되면 (username + password + apitoken) 세션에 대한 다른 토큰 (일명 accesstoken)을 생성합니다. 다시 node-uuid로. 액세스 토큰과 사용자 ID를 사용자에게 보냅니다. 사용자 ID (키) 및 액세스 토큰 (값)은 1 시간과 같이 redis에 저장되고 만료 시간이됩니다.

이제 사용자가 나머지 API를 사용하여 작업을 수행 할 때마다 사용자 ID와 액세스 토큰을 보내야합니다.

사용자가 나머지 API를 사용하여 가입 할 수 있도록 허용하는 경우, 새로운 사용자에게는 apitoken이 없기 때문에 admin apitoken을 사용하여 관리자 계정을 만들어 모바일 앱에 저장해야합니다 (username + password + apitoken 암호화). 그들은 가입합니다.

웹에서도이 API를 사용하지만 아 피토 켄을 사용할 필요는 없습니다. redis 상점에서 express를 사용하거나 위에서 설명한 것과 동일한 기술을 사용할 수 있지만 apitoken 확인을 무시하고 쿠키의 userid + accesstoken을 사용자에게 반환 할 수 있습니다.

개인 영역이있는 경우 인증시 허용 된 사용자와 사용자 이름을 비교하십시오. 사용자에게 역할을 적용 할 수도 있습니다.

요약:

시퀀스 다이어그램

apitoken이없는 대안은 HTTPS를 사용하고 Authorization 헤더에 사용자 이름과 비밀번호를 보내고 사용자 이름을 redis로 캐시하는 것입니다.


1
나는 mongodb도 사용하지만 redis (원자 작업 사용)를 사용하여 세션 (액세스 토큰)을 저장하면 관리하기가 쉽습니다. 사용자가 계정을 생성하여 사용자에게 다시 보내면 서버에서 API가 생성됩니다. 그런 다음 사용자가 인증을 받으려면 username + password + apitoken을 보내야합니다 (http 본문에 입력). HTTP는 본문을 암호화하지 않으므로 비밀번호와 apitoken을 스니핑 할 수 있습니다. 이것이 우려되는 경우 HTTPS를 사용하십시오.
Gabriel Llamas

1
사용에 대한 요점은 apitoken무엇입니까? "보조"비밀번호입니까?
Salvatorelab

2
@TheBronx apitoken에는 2 가지 사용 사례가 있습니다. 1) apitoken을 사용하면 시스템에 대한 사용자의 액세스를 제어하고 각 사용자의 통계를 모니터링하고 구축 할 수 있습니다. 2) 추가 보안 수단 인 "보조"암호입니다.
Gabriel Llamas

1
인증에 성공한 후 사용자 ID를 계속해서 다시 보내야하는 이유는 무엇입니까? 토큰은 API 호출을 수행하는 데 필요한 유일한 비밀이어야합니다.
Axel Napolitano

1
사용자 활동을 추적하기 위해 토큰을 남용하는 것 외에도 토큰에 대한 아이디어는 사용자가 응용 프로그램을 사용하기 위해 사용자 이름과 암호가 이상적으로 필요하지 않다는 것입니다. 토큰은 고유 한 액세스 키입니다. 이를 통해 사용자는 언제든지 앱에만 영향을 주지만 사용자 계정에는 영향을주지 않는 키를 삭제할 수 있습니다. 웹 서비스의 경우 토큰이 매우 다루기 힘들 기 때문에 세션에 대한 초기 로그인이 사용자가 해당 토큰을 얻는 장소입니다. "일반적인"클라이언트 ab의 경우 토큰에 문제가 없습니다. 한 번만 입력하면 거의 완료됩니다 ;)
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'과 일치하지 않습니다. 동일한 컴퓨터에서 https 연결을 허용하도록 내 / etc / hosts를 하드 코딩했습니다.
mastervv


9

여기에 REST 인증 패턴에 대한 많은 질문이 있습니다. 다음은 귀하의 질문과 가장 관련이 있습니다.

기본적으로 API 키 (무단 사용자가 키를 발견 할 수 있으므로 가장 안전하지 않음), 앱 키와 토큰 콤보 (중간) 또는 전체 OAuth 구현 (가장 안전한) 중 하나를 선택해야합니다.


oauth 1.0 및 oauth 2.0에 대해 많이 읽었으며 두 버전 모두 매우 안전하지 않은 것 같습니다. Wikipedia는 oauth 1.0의 보안 누수라고 썼습니다. 또한 oauth 2.0이 안전하지 않기 때문에 핵심 개발자 중 하나가 팀을 떠나는 기사를 발견했습니다.
tschiela

12
@tschiela 여기에 인용 한 내용에 대한 참조를 추가해야합니다.
mikemaccana

3

애플리케이션을 보호 하려면 HTTP 대신 HTTPS를 사용하여 시작해야합니다. . 이렇게하면 사용자와 사용자 사이에 전송되는 데이터를 스니핑하지 못하게하고 데이터를 유지하는 데 도움이되는 사용자와 사용자간에 안전한 채널을 만들 수 있습니다 기밀 교환.

JWT (JSON 웹 토큰)를 사용하여 RESTful API를 보호 할 수 있습니다. 이는 서버 측 세션과 비교할 때 많은 이점이 있으며 주로 다음과 같은 이점이 있습니다.

1- API 서버가 각 사용자에 대해 세션을 유지할 필요가 없기 때문에 확장 성이 뛰어납니다 (세션이 많을 때 큰 부담이 될 수 있음)

2- JWT는 자체 포함되어 있으며 사용자 역할을 정의하는 청구 및 날짜 및 만료 날짜에 액세스 및 발행 할 수있는 항목 (JWT가 유효하지 않은 후)

3-로드 밸런서에서보다 쉽게 ​​처리 할 수 ​​있으며 JWT 요청이 서버에 도달 할 때마다 세션 데이터를 공유하거나 세션을 동일한 서버로 라우팅하도록 서버를 구성 할 필요가없는 여러 API 서버가있는 경우 인증 가능 & 승인

4- DB에 대한 부담이 적을뿐만 아니라 각 요청에 대해 세션 ID 및 데이터를 지속적으로 저장 및 검색 할 필요가 없습니다.

5- 강력한 키를 사용하여 JWT에 서명하면 JWT를 무단으로 변경할 수 없으므로 사용자 세션을 확인하지 않고 요청과 함께 전송 된 JWT의 클레임을 신뢰할 수 있는지 여부 , 당신은 JWT를 확인하면 모든 사용자와이 사용자가 무엇을 할 수 있는지 알 수 있습니다.

많은 라이브러리는 대부분의 프로그래밍 언어에서 JWT를 작성하고 유효성을 검증하는 쉬운 방법을 제공합니다 (예 : node.js에서 가장 인기있는 것 중 하나는 jsonwebtoken입니다).

REST API는 일반적으로 서버를 무 상태로 유지하는 것을 목표로하기 때문에 각 요청이 자체 포함 된 권한 부여 토큰으로 전송 될 때 JWT가 해당 개념과 더 호환됩니다. 서버가 사용자 세션을 추적하지 않고도 (JWT) . 서버는 사용자와 그의 역할을 기억할 수 있도록 세션 상태를 유지하지만 세션은 널리 사용되며 전문가가 있으므로 원하는 경우 검색 할 수 있습니다.

한 가지 중요한 점은 HTTPS를 사용하여 JWT를 클라이언트에 안전하게 전달하고 안전한 장소 (예 : 로컬 스토리지)에 저장해야한다는 것입니다.

이 링크에서 JWT에 대해 자세히 알아볼 수 있습니다.


1
나는이 오래된 질문에서 가장 좋은 업데이트 인 것처럼 당신의 대답을 좋아합니다. 나는 같은 주제에 대해 다른 질문을했으며 도움이 될 수도 있습니다. => stackoverflow.com/questions/58076644/…
pbonnefoi

감사합니다, 도와 드리겠습니다, 귀하의 질문에 대한 답변을 게시합니다
Ahmed Elkoussy

2

회사의 관리자 만 액세스 할 수있는 웹 응용 프로그램의 영역을 완전히 잠 그려면 SSL 인증이 필요할 수 있습니다. 브라우저에 인증 된 인증서가 설치되어 있지 않으면 아무도 서버 인스턴스에 연결할 수 없습니다. 지난주 서버 설정 방법에 대한 기사를 썼습니다. 기사

이것은 사용자 이름 / 암호가 포함되어 있지 않기 때문에 가장 안전한 설정 중 하나이므로 사용자 중 한 명이 잠재적 인 해커에게 키 파일을 전달하지 않으면 아무도 액세스 할 수 없습니다.


좋은 기사. 그러나 개인 영역은 사용자를위한 공간입니다.
tschiela

고마워-그럼 다른 해결책을 찾아야합니다. 인증서를 배포하는 것은 고통 스럽습니다.
ExxKA
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.