웹사이트 검색

JavaScript에서 BcryptsJS를 사용하여 비밀번호를 안전하게 처리하는 방법


저자는 Write for DOnations 프로그램의 일환으로 기부금을 받기 위해 OWASP 재단을 선택했습니다.

소개

웹사이트 비밀번호를 보호하는 것은 모든 개발자가 갖춰야 할 필수 기술입니다. JavaScript는 JavaScript의 BcryptJS 모듈에서 제공하는 해싱 알고리즘을 사용하여 비밀번호나 기타 민감한 데이터를 안전하게 저장하고 처리하는 옵션을 제공합니다.

이 튜토리얼에서는 원시 문자열 대신 데이터베이스에 해시로 비밀번호를 저장하고 검색하여 비밀번호를 인증하는 기본 Express 서버를 설정하기 위한 BcryptJS 및 해싱에 대해 알아봅니다.

전제조건

이 튜토리얼을 계속하려면 다음 설정이 필요합니다.

  1. 12.x 버전 이상으로 컴퓨터에 Node.js의 안정적인 버전이 설치되어 있습니다. 이 DigitalOcean 튜토리얼을 사용하여 컴퓨터에 최신 Node Js 버전을 설치할 수 있습니다.

  2. JavaScript로 코딩하는 방법을 알아야 합니다.

  3. _컴퓨터에 Express JS가 설치되어 있어야 합니다. 이 가이드를 사용하여 Express 서버를 설정하는 방법을 배울 수 있습니다.

  4. 마지막으로 이 튜토리얼을 완료하려면 MongoDB 커뮤니티 또는 Atlas 데이터베이스가 필요합니다. MongoDB 설치 방법에 대한 DigitalOcean 가이드 중 하나를 사용하여 설치할 수 있습니다.

BcryptJS를 사용하는 이유는 무엇입니까?

Bcrypt는 데이터 침해가 발생할 경우 비밀번호를 저장하기 위해 비밀번호 해시를 생성하는 해싱 알고리즘입니다. 이 고급 해싱 알고리즘은 솔트를 사용하므로 무차별 공격과 같은 공격으로 해독하기 어렵습니다.

BcryptJS는 Bcrypt 해싱 알고리즘의 JavaScript 구현으로, 복잡한 해싱 기능을 사용하지 않고도 해시 암호화를 사용할 수 있습니다. BcryptJS를 비밀번호 보안을 위한 탁월한 선택으로 만드는 몇 가지 이유는 다음과 같습니다.

  1. 보안 – BcryptJS는 느린 알고리즘(해싱에서는 좋은 점)인 Bcrypt 알고리즘을 구현하며 강력한 계산 능력이 필요합니다. 이로 인해 공격자가 비밀번호 해시를 해독하는 것은 엄격한 작업이 되어 데이터 침해가 발생한 경우에도 비밀번호의 안전이 보장됩니다._

  2. 솔팅 – BcryptJS는 저장소 보안을 보장하기 위해 비밀번호에 대한 무작위 솔트 생성을 처리합니다(다음 섹션에서 해시와 솔트에 대해 자세히 알아볼 것입니다). 솔트는 상대적으로 취약한 비밀번호의 해시를 더욱 복잡하게 만들어 해독하기 어렵게 만듭니다.

  3. 사용 용이성 – BcryptJS는 JavaScript 개발자에게 해싱에 대한 깊은 이해 없이도 비밀번호를 암호화할 수 있는 도구를 제공합니다._

다음 단계에서는 해시와 솔트에 대해 간략하게 알아 보겠습니다.

해싱은 어떻게 작동하나요?

프로젝트에서 이러한 개념을 사용하기 전에 해싱 및 솔팅이 작동하는 방식을 이해해야 합니다.

해싱

해싱은 간단한 문자열이나 일반 텍스트를 임의의 문자로 구성된 문자열(암호화)로 변환하는 것입니다. 이를 통해 민감한 데이터를 안전하게 저장 및/또는 전송할 수 있습니다. 해싱에는 다음과 같은 주요 단계가 포함됩니다.

데이터 입력 – 먼저 일반 텍스트 또는 문자열 등 모든 유형(이진수, 문자, 십진수 등)이 될 수 있는 데이터가 저장됩니다.

해시 함수 – 해시 함수는 데이터에서 입력을 받아 이를 문자 또는 해시 코드 집합으로 변환하는 수학적 알고리즘입니다. 해시 함수는 결정론적(동일한 입력에 대해 동일한 출력 생성) 및 단방향 함수(즉, 해시 함수의 출력, 즉 입력 데이터에 해시를 역설계하는 것이 거의 불가능함을 의미함)입니다.

충돌 저항 – 이는 충돌 저항 개념을 염두에 두고 해시 함수를 생성한다는 의미입니다. 즉, 두 개의 서로 다른 입력이 동일한 출력(해시 코드)을 가질 수 없습니다.

인증 – 해시 함수는 결정적이며 동일한 입력에 대해 동일한 해시를 생성합니다. 따라서 해시로 저장된 비밀번호를 인증할 때, 인증할 비밀번호가 데이터베이스에 저장된 해시와 일치하면 비밀번호가 맞는다는 것이 일반적인 생각이다.

염장

해싱이 수십 년 동안 사용된 이래로 다양한 해싱 알고리즘을 기반으로 하는 데이터 문자열과 해당 해시를 포함하는 수십억 개의 데이터 항목을 포함하는 레인보우 테이블과 같은 개발이 있었습니다.

이제 웹사이트에 계정을 만드는 사용자가 취약한 비밀번호를 만드는 상황을 생각해 보세요. 따라서 데이터 유출이 발생하는 경우 공격자는 사용자의 해시를 조회하고 비밀번호가 취약한 사용자 계정과 일치하는 해시를 찾을 수 있습니다. 이는 보안 수준이 높은 애플리케이션에서는 재앙이 될 수 있습니다. 이런 일이 발생하는 것을 방지하기 위해 소금이 사용됩니다.

솔팅은 비밀번호를 데이터베이스에 저장하기 전에 비밀번호 해시에 임의의 문자열을 추가하여 해시에 보안을 추가하는 추가 계층입니다. 따라서 침해로 인해 데이터가 유출되더라도 공격자가 솔트가 포함된 해시를 해독하기는 어렵습니다. 다음 예를 고려하십시오.

Password = ‘sammy’
Hash = £%$^&£!23!3%!!
Salt = 2vqw£4Df$%sdfk
Hash + Salt = £%$^&£!23!3%!!2vqw£4Df$%sdfk

우리가 분명히 알 수 있듯이 솔트에 저장된 비밀번호는 해시의 결정론적 특성으로 인해 해독될 가능성이 더 높습니다. 따라서 예를 들어 공격자가 레인보우 테이블에서 이 해시+비밀번호 문자열을 검색하면 실제 비밀번호 문자열이 아니라 완전히 다른 비밀번호 문자열을 얻게 됩니다.

이제 BcryptJS를 사용하고 업계 표준 방식으로 비밀번호를 보호할 준비가 되었습니다.

BcryptJS 및 기타 필수 모듈 설치

이제 해싱과 솔팅에 대해 알았으니 컴퓨터를 가져와 코딩을 시작하기만 하면 됩니다. 프로젝트 구조는 다음과 같습니다.

먼저 다음 단계에 따라 npm 프로젝트를 생성하는 것부터 시작하겠습니다.

  1. 폴더를 열고 app.js. 파일을 만듭니다.

  2. 이 폴더에서 터미널 창을 열고 다음 명령을 입력하세요.

npm init

그 후에는 입력하라는 메시지가 표시되지만 입력을 제공하지 않고 Enter를 누를 수 있습니다.

  1. 그런 다음 3개의 파일을 더 만듭니다.
    auth.js
    db.js
    User.js
  1. 동일한 터미널 창에서 다음 명령을 입력하여 필요한 패키지를 설치합니다.
npm install express mongoose BcryptJS nodemon

이제 이 튜토리얼에서 따라야 할 완전한 프로젝트 환경 설정이 완료되었습니다. 다음 단계에서는 MongoDB로 비밀번호를 안전하게 저장하고 인증하기 위해 BcryptJS를 사용하는 서버를 만드는 방법을 배웁니다.

Express JS로 서버 설정

이제 프로젝트 구조를 설정했으므로 bcrytjs를 사용하여 암호를 해시로 저장하고 인증함으로써 암호를 보호하는 서버를 생성할 수 있습니다. 다음과 같은 적절한 단계에 따라 서버를 생성하겠습니다.

1단계 - MongoDB 데이터베이스에 대한 연결 만들기

mongoDB에 연결하기 위해 커뮤니티 에디션을 사용하고 있습니다. 프로젝트를 정리된 상태로 유지하려면 db.js 파일에 연결 설정을 위한 코드를 저장합니다.

const mongoose = require("mongoose");
const mongoURI = "mongodb://127.0.0.1:27017/bcrypt_database";
const connectMongo = async () => {

  try {
    await mongoose.connect(mongoURI);
    console.log("Connected to MongoDB!");
  } catch (error) {
    console.error("Error connecting to MongoDB: ", error.message);
  }
};

module.exports = connectMongo;

여기서는 javascript를 MongoDB에 연결하기 위한 API를 제공하는 mongoose 패키지를 가져옵니다. 또한 이 경우 연결 URI는 mongoDB의 로컬 설치를 위한 것입니다. Atlas와 같은 클라우드 데이터베이스를 사용하는 경우에는 URI를 특정 데이터베이스의 URI로 변경하기만 하면 됩니다.

정보 URI는 리소스의 이름과 ID, 인터넷에서의 위치를 식별할 수 있다는 점을 제외하면 서버 URL과 유사합니다. 대조적으로, URL은 후자만 수행할 수 있는 URI의 하위 집합입니다.

connectToMongo 함수는 mongoose.connect가 자바스크립트 약속을 반환하기 때문에 비동기 함수입니다. 이 기능은 성공적인 실행에 대한 연결을 제공합니다. 그렇지 않으면 오류가 반환됩니다.

마지막으로 db.js 모듈을 가져올 때마다 module.exports를 사용하여 이 함수를 내보냅니다.

2단계 – 사용자 스키마 생성

MongoDB 데이터베이스로 사용자를 생성하거나 인증하려면 기본 스키마가 필요합니다. 스키마가 무엇인지 모르는 경우 이 훌륭한 DO 가이드를 사용하여 MongoDB에서 스키마를 이해하고 생성할 수 있습니다. 스키마에는 email 및 password라는 두 개의 필드만 사용합니다.

User.js 모듈에서 다음 코드를 사용하세요.

const mongoose  = require("mongoose");
const UserSchema = new mongoose.Schema({
    email:{
        type:String,
        required:true,
        unique:true
    },
    password:{
        type:String,
        required:true
    }
});
module.exports = mongoose.model('user', UserSchema)

이 스키마에 email 및 password라는 두 개의 필드를 만듭니다. 둘 다 필수 필드이며 문자열 데이터 유형입니다. 또한 email은 고유한 필드입니다. 이는 하나의 이메일이 이 서버에 계정을 갖는 데 한 번만 사용될 수 있음을 의미합니다.

마지막으로 users라는 모델을 내보냅니다. 모델은 데이터베이스 측의 스키마를 나타냅니다. 스키마는 모델을 정의하기 위한 규칙으로 생각할 수 있지만 모델은 MongoDB 데이터베이스에 컬렉션으로 저장됩니다.

mongoose 라이브러리의 model() 함수를 사용하여 스키마를 모델로 변환할 수 있습니다.

3단계 - app.js에서 서버 설정

이전 단계를 수행한 후 MongoDB 데이터베이스에 연결하기 위한 모델과 모듈을 성공적으로 생성했습니다. 이제 서버를 설정하는 방법을 배우겠습니다. app.js 파일에서 다음 코드를 사용하세요.

const connectToMongo = require("./db");
const express = require("express");
const app = express();
connectToMongo();
app.use(express.json());
app.use("/auth", require("./auth"));
const port = 3300;
app.listen(port, () => {
  console.log(`Listening at http://localhost:${port}`);
});

여기서는 MongoDB에 연결하기 위해 express 및 db.js 모듈을 가져옵니다. 그런 다음 express.json() 미들웨어를 사용하여 JSON 응답을 처리합니다. 경로는 코드를 깔끔하고 체계적으로 유지하기 위해 다른 모듈(auth.js)에서 생성됩니다. 그런 다음 마지막으로 서버가 localhost의 3300 포트를 수신할 수 있는 엔드포인트를 만듭니다. (원하는 포트를 사용해도 됩니다)

비밀번호를 암호화하여 MongoDB 데이터베이스에 저장

지금까지 서버가 거의 준비되었으므로 이제 서버에 대한 엔드포인트를 생성하겠습니다. 가입 및 로그인이라는 두 개의 엔드포인트를 생성합니다. 가입 엔드포인트에서는 새로운 사용자로부터 이메일과 비밀번호를 가져와 BcryptJS를 사용하여 비밀번호 암호화와 함께 저장합니다.

auth.js 파일에 다음 코드를 입력합니다.

const express = require("express");
const router = express.Router();
const User = require("./User");
const bcrypt = require("bcryptjs");

// ROUTE 1:
router.post("/signup", async (req, res) => {
  const salt = await bcrypt.genSalt(10);
  const secPass = await bcrypt.hash(req.body.password, salt);
  let user = await User.create({
    email: req.body.email,
    password: secPass,
  });
  res.json({ user });
});
module.exports = router;

필요한 가져오기를 수행한 다음 Express Router를 설정하여 /signup 엔드포인트를 생성합니다. 자격 증명이 애플리케이션의 URL에 공개되지 않도록 POST 메서드를 사용하고 있습니다. 그런 다음 scripts 패키지의 genSalt 함수를 사용하여 Salt를 생성합니다. genSalt() 함수에 전달된 매개변수에는 솔트 문자의 길이가 포함되어 있습니다. 그런 다음 필수 매개변수, 해시 코드로 변환할 비밀번호 문자열, 선택적 인수인 솔트 문자열을 사용하는 BcryptJS의 hash() 함수를 사용합니다. 그런 다음 password와 salt를 모두 포함하는 해시를 반환합니다.

그런 다음 mongoose 모듈의 create() 함수를 사용하여 users 모델의 규칙에 따라 정의된 데이터베이스에 문서를 생성합니다. 이메일과 비밀번호가 포함된 Javascript 개체를 사용하지만 원시 문자열을 제공하는 대신 secPass(password hash + salt)를 데이터베이스에 저장합니다. 이런 방식으로 원시 문자열 대신 솔트와 결합된 해시를 사용하여 데이터베이스에 비밀번호를 안전하게 저장했습니다. 궁극적으로 우리는 사용자 모델이 포함된 JSON 응답을 반환합니다. (이 응답 전송 방법은 개발 단계에만 사용됩니다. 프로덕션에서는 이를 인증 토큰이나 다른 것으로 대체합니다.)

이 엔드포인트를 테스트하려면 먼저 서버를 실행해야 하며, 이는 터미널에 다음 명령을 입력하여 수행할 수 있습니다.

cd <path to your project folder>
nodemon ./app.js

이 명령은 localhost 및 포트 3300(또는 지정한 포트)에서 서버를 실행합니다. 그런 다음 다음 본문을 사용하여 http://localhost:3300/auth/signup URL로 HTTP 요청을 보낼 수 있습니다.

{
  "email":"sammy@linux-console.net",
  "password":"sammy"
}

그러면 다음과 같은 출력/응답이 생성됩니다.

{
  "user": {
    "email": "sammy@linux-console.net",
    "password": "$2a$10$JBka/WyJD0ohkzyu5Wu.JeCqQm33UIx/1xqIeNJ1AQI9kYZ0Gr0IS",
    "_id": "654510cd8f1edaa59a8bb589",
    "__v": 0
  }
}

참고: 비밀번호 해시와 ID는 항상 고유하므로 동일하지 않습니다.

다음 섹션에서는 비밀번호에 대해 저장된 해시에 액세스하고 로그인/인증 시 제공된 비밀번호로 인증하는 방법을 알아봅니다.

암호화된 비밀번호에 접근하여 인증에 사용

지금까지 BcryptJS, 해싱, 솔팅 및 새로운 사용자를 생성하는 express 서버 개발에 대해 배웠습니다. 해시로 저장된 비밀번호로. 이제 저장된 비밀번호를 사용하는 방법과 애플리케이션에 로그인을 시도할 때 사용자를 인증하는 방법을 알아봅니다.

인증 방법을 추가하려면 /signup 경로 뒤에 auth.js에서 다음 경로를 추가하세요.

// ROUTE 2:
router.post("/login", async (req, res) => {
  let user = await User.findOne({ email: req.body.email });
  if (!user) {
    return res.status(400).json({ error: "Login with proper credentials!" });
  }
  
  const passwordCompare = await bcrypt.compare(req.body.password, user.password);
  if (!passwordCompare) {
    return res
      .status(400)
      .json({ error: "Login with proper credentials!" });
  }
  
  res.json({ success: "Authenticated!" });
});

여기서는 /auth/login 하위 경로를 사용하여 기존 사용자에 대한 로그인 인증을 수행합니다. /auth/signup 엔드포인트와 유사하게 BcryptJS가 Promise를 반환하므로 이는 비동기 대기 기능이 됩니다.

먼저, 주어진 검색어를 기반으로 컬렉션에서 문서를 찾는 데 사용되는 Mongoose 라이브러리의 findOne 기능을 사용합니다. 이 경우 이메일을 기반으로 사용자를 검색합니다. 제공된 이메일을 가진 사용자가 없으면 잘못된 자격 증명에 대한 상태 코드 400이 포함된 응답을 보냅니다. (공격자가 해당 정보를 사용하여 기존 계정을 찾을 수 있으므로 로그인 중에 어떤 매개변수가 올바르지 않은지 제공하는 것은 좋은 습관이 아닙니다.)

제공된 이메일을 가진 사용자가 존재하는 경우 프로그램은 비밀번호 비교로 이동합니다. 이를 위해 BcryptJS는 원시 문자열을 첫 번째 인수로 사용하고 해시(< 포함 또는 제외)를 사용하는 compare() 메서드를 제공합니다.salt)를 두 번째 인수로 사용합니다. 그런 다음 부울 약속을 반환합니다. 비밀번호가 해시와 일치하면 true이고, 일치하지 않으면 false입니다. 그런 다음 if 문을 사용하여 간단한 검사를 추가하고 비교 결과에 따라 성공 또는 오류를 반환할 수 있습니다.

마지막으로 app.js 시작 지점에 대해 module.exports를 사용하여 express Router를 내보내 경로에 사용할 것입니다.

이 경로를 테스트하려면 다음과 같이 본문과 함께 이 URL http://localhost:3300/auth/login에 또 다른 HTTP 응답을 보낼 수 있습니다.

{
  "email":"sammy@linux-console.net",
  "password":"sammy"
}

이 요청은 다음과 같은 응답을 제공합니다.

{
  "success": "Authenticated!"
}

마지막으로 auth.js는 다음과 같습니다:

const express = require("express");
const router = express.Router();
const User = require("./User");
const bcrypt = require("bcryptjs");

// ROUTE 1:
router.post("/signup", async (req, res) => {
  const salt = await bcrypt.genSalt(10);
  const secPass = await bcrypt.hash(req.body.password, salt);
  let user = await User.create({
    email: req.body.email,
    password: secPass,
  });
  res.json({ user });
});

// ROUTE 2:
router.post("/login", async (req, res) => {
  let user = await User.findOne({ email: req.body.email });
  if (!user) {
    return res.status(400).json({ error: "Login with proper credentials!" });
  }
  const passwordCompare = await bcrypt.compare(req.body.password, user.password);
  if (!passwordCompare) {
    return res
      .status(400)
      .json({ error: "Login with proper credentials!" });
  }
  res.json({ success: "Authenticated!" });
});
module.exports = router;

결론

이 튜토리얼에서는 데이터베이스 비밀번호를 안전하게 저장하고 액세스하기 위한 BcryptJS의 사용법을 설명하기 위해 서버를 만들었습니다. 다음을 통해 추가로 진행할 수 있습니다.

  • 사용자 자격 증명 가져오기, 사용자 자격 증명 업데이트 등과 같은 다른 작업을 위한 더 많은 경로 구현

  • 응답 등으로 보낼 인증 토큰 구현

비밀번호를 안전하게 처리하는 여정이 시작됩니다. 다양한 기술과 기술을 사용하여 언제든지 더 많은 보안을 추가할 수 있으므로 더욱 안전하고 탄력적인 애플리케이션을 만들 수 있습니다.

관련기사: