JavaScript에서 BcryptsJS로 안전하게 암호를 다루는 방법

소개

웹사이트 비밀번호 보호는 개발자가 반드시 가져야 할 필수 기술입니다. JavaScriptJavaScriptBcryptJS 모듈에서 제공하는 해시 알고리즘을 사용하여 비밀번호 또는 기타 중요한 데이터의 안전한 저장 및 처리를 보장하는 옵션을 제공합니다.

이 자습서에서는 BcryptJS와 해싱에 대해 알아보고, 비밀번호를 원시 문자열이 아닌 해시로 데이터베이스에 저장하고 인증하기 위해 이를 검색하는 기본 express 서버를 설정하는 방법을 배울 것입니다.

사전 준비 사항

이 자습서를 진행하기 위해 다음 설정이 필요합니다.

  1. 컴퓨터에 안정적인 버전의 Node.js가 설치되어 있어야 합니다. 버전은 12.x 이상이어야 합니다. 컴퓨터에 최신 Node.js 버전을 설치하기 위해 여기의 DigitalOcean 자습서를 사용할 수 있습니다.

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

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

  4. 마지막으로, 이 튜토리얼을 완료하기 위해 MongoDB 커뮤니티 또는 Atlas 데이터베이스가 필요합니다. MongoDB를 설치하는 방법은 다음 중 하나의 DigitalOcean 가이드를 사용하여 설치할 수 있습니다_ MongoDB 설치 방법.

BcryptJS를 사용하는 이유는 무엇인가요?

Bcrypt는 데이터 유출 사고의 경우에 비밀번호의 해시를 생성하기 위한 해싱 알고리즘입니다. 이 고급 해싱 알고리즘은 소금을 사용하여 브루트 포싱과 같은 공격으로부터 어렵게 크랙되도록 만듭니다.

BcryptJS는 Bcrypt 해시 알고리즘의 JavaScript 구현입니다. 복잡한 해싱 함수에 개입하지 않고 해시 암호화를 사용할 수 있게 해줍니다. BcryptJS가 비밀번호 보안에 좋은 선택인 몇 가지 이유는 다음과 같습니다:

  1. 보안 – BcryptJS는 Bcrypt 알고리즘을 구현하여 느린 알고리즘(해싱에서는 좋은 것)과 강력한 계산 능력을 요구합니다. 이를 통해 공격자가 비밀번호 해시를 해독하는 것이 어려워지며, 데이터 침해의 경우에도 비밀번호의 안전을 보장합니다.

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

  3. 사용 편의성 – BcryptJS는 JavaScript 개발자에게 해시를 깊이 이해하지 않고 비밀번호를 암호화하는 도구를 제공합니다.

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

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

이 개념을 프로젝트에서 사용하기 전에, 해싱과 솔트 작동 방식을 이해해야 합니다.

해싱

해싱은 간단한 문자열이나 평문을 무작위 문자열(암호화)로 변환하는 것입니다. 이를 통해 민감한 데이터를 안전하게 저장하거나 전송할 수 있습니다. 해싱은 다음과 같은 주요 단계를 포함합니다:

데이터 입력 – 우선, 이진, 문자, 십진 등 어떤 유형의 데이터든 평문 또는 문자열로 저장됩니다.

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

충돌 저항성 – 이것은 해시 함수가 충돌 저항성을 고려하여 생성된다는 것을 의미합니다. 즉, 두 개의 다른 입력은 동일한 출력(해시 코드)을 가질 수 없습니다.

인증 – 해시 함수는 결정론적이며, 동일한 입력에 대해 항상 동일한 해시를 생성합니다. 따라서 해시로 저장된 암호를 인증할 때 일반적인 개념은 인증할 암호가 데이터베이스에 저장된 해시와 일치하는 경우 암호가 올바르다는 것입니다.

솔트

해싱은 수십 년 동안 사용되어 왔기 때문에 무지개 테이블과 같은 개발이 있었습니다. 이러한 테이블에는 다양한 해싱 알고리즘을 기반으로한 데이터 문자열과 해당 해시를 포함하는 수십억 개의 데이터 항목이 포함되어 있습니다.

이제 사용자가 웹 사이트에서 약한 암호를 생성하는 상황을 고려해 보겠습니다. 따라서 데이터 침해가 발생하는 경우, 공격자는 사용자의 해시를 찾아 약한 암호를 가진 사용자 계정의 해시와 일치하는 것을 찾을 수 있습니다. 이는 고안안전 애플리케이션에서 재앙이 될 수 있습니다. 이러한 일이 발생하지 않도록하기 위해 솔트가 사용됩니다.

솔트는 해시에 임의의 문자열을 추가하여 암호의 해시를 데이터베이스에 저장하기 전에 추가된 보안 계층입니다. 따라서 데이터가 누출되더라도 솔트가 포함된 해시를 해독하는 것은 공격자에게 어렵습니다. 다음 예를 고려해 보겠습니다:

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

우리는 명확히 볼 수 있듯이 소금으로 저장된 비밀번호는 해시의 결정론적 특성으로 인해 크랙되기 어렵습니다. 따라서, 예를 들어, 공격자가 무지개 테이블에서 이 해시+비밀번호 문자열을 찾아도 실제 비밀번호 문자열을 얻지 못하고 완전히 다른 것을 얻게 됩니다.

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

BcryptJS 및 기타 필요한 모듈 설치

해싱과 소금에 대해 알게 되었으니 이제 컴퓨터를 준비하고 코딩을 시작할 차례입니다. 프로젝트 구조는 다음과 같이 될 것입니다:

Project Structure

먼저, 다음 단계로 npm 프로젝트를 생성해보겠습니다:

  1. 폴더를 열고 app.js 파일을 생성하세요.

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

npm init

이후, 입력을 요구하는 메시지가 표시되지만 아무 입력 없이 Enter 키를 누를 수 있습니다.

  1. 그런 다음, 다음과 같이 3개의 파일을 더 생성하세요.
  • auth.js
  • db.js
  • User.js
  1. 같은 터미널 창에서 필요한 패키지를 설치하기 위해 다음 명령을 입력하세요.
npm install express mongoose BcryptJS nodemon

이제 튜토리얼을 따라할 완전한 프로젝트 환경 설정이 있습니다. 다음 단계에서는 BcryptJS를 사용하여 MongoDB와 함께 비밀번호를 안전하게 저장하고 인증하는 서버를 만드는 방법을 배우게 됩니다.

Express JS를 사용하여 서버 설정

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

단계 1 – MongoDB 데이터베이스에 연결 생성

mongoDB에 연결하기 위해 커뮤니티 에디션을 사용합니다. 프로젝트를 구성 정리하기 위해 연결 설정 코드를 db.js 파일에 저장합니다.

  1. const mongoose = require("mongoose");
  2. const mongoURI = "mongodb://127.0.0.1:27017/bcrypt_database";
  3. const connectMongo = async () => {
  4. try {
  5. await mongoose.connect(mongoURI);
  6. console.log("Connected to MongoDB!");
  7. } catch (error) {
  8. console.error("Error connecting to MongoDB: ", error.message);
  9. }
  10. };
  11. module.exports = connectMongo;

여기에서는 mongoose 패키지를 가져오며, 이 패키지는 자바스크립트를 MongoDB에 연결하기 위한 API를 제공합니다. 또한, 이 경우에는 로컬에서 mongoDB를 설치한 경우의 연결 URI입니다. 클라우드 데이터베이스(예: Atlas)를 사용하는 경우 URI를 해당 데이터베이스의 URI로 변경하면 됩니다.

정보
URI는 서버 URL과 유사하지만, URI는 인터넷 상의 리소스의 이름과 식별자 및 위치를 식별할 수 있습니다. 반면에 URL은 URI의 하위 집합으로, 후자만 가능합니다.

connectToMongo 함수는 비동기 함수입니다. 왜냐하면 mongoose.connect자바스크립트 프로미스를 반환하기 때문입니다. 이 함수는 성공적인 실행에 대한 연결을 제공합니다. 그렇지 않으면 오류를 반환합니다.

마지막으로, db.js 모듈이 가져올 때마다 이 함수를 내보내기 위해 module.exports를 사용합니다.

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

몽고디비 데이터베이스를 사용하여 사용자를 생성하거나 인증하기 위해 기본 스키마가 필요합니다. 스키마가 무엇인지 모르는 경우, 이 훌륭한 DO 가이드를 사용하여 몽고디비에서 스키마를 이해하고 생성할 수 있습니다. 우리는 스키마에 emailpassword라는 두 가지 필드만 사용할 것입니다.

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

  1. const mongoose = require("mongoose");
  2. const UserSchema = new mongoose.Schema({
  3. email:{
  4. type:String,
  5. required:true,
  6. unique:true
  7. },
  8. password:{
  9. type:String,
  10. required:true
  11. }
  12. });
  13. module.exports = mongoose.model('user', UserSchema)

이 스키마에는 emailpassword라는 두 가지 필드를 생성합니다. 둘 다 필수 필드이며 문자열 데이터 유형입니다. 또한, email은 고유 필드로, 한 이메일은 이 서버에서 계정 하나만 만들 수 있습니다.

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

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

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

이전 단계를 따르면 모델과 MongoDB 데이터베이스에 연결하기 위한 모듈을 성공적으로 만들었습니다. 이제 서버를 설정하는 방법을 배워보겠습니다. app.js 파일에 다음 코드를 사용하세요.

  1. const connectToMongo = require("./db");
  2. const express = require("express");
  3. const app = express();
  4. connectToMongo();
  5. app.use(express.json());
  6. app.use("/auth", require("./auth"));
  7. const port = 3300;
  8. app.listen(port, () => {
  9. console.log(`Listening at http://localhost:${port}`);
  10. });

여기에서는 expressdb.js 모듈을 가져와 MongoDB에 연결합니다. 그런 다음, JSON 응답을 처리하기 위해 express.json() 미들웨어를 사용합니다. 라우트는 코드를 청소하고 구성하기 위해 다른 모듈(auth.js)에 생성됩니다. 마지막으로, 서버의 엔드포인트를 localhost의 포트 3300에서 듣도록 만듭니다. (원하는 포트를 사용할 수 있습니다)

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

지금까지 서버는 거의 준비가 되었고, 이제 서버를 위한 엔드포인트를 생성할 것입니다. 우리는 signuplogin 두 개의 엔드포인트를 생성할 것입니다.

signup 엔드포인트에서는 새로운 사용자의 이메일과 비밀번호를 가져와서 BcryptJS를 사용하여 비밀번호를 암호화한 후 저장합니다.

  1. const express = require("express");
  2. const router = express.Router();
  3. const User = require("./User");
  4. const bcrypt = require("bcryptjs");
  5. // ROUTE 1:
  6. router.post("/signup", async (req, res) => {
  7. const salt = await bcrypt.genSalt(10);
  8. const secPass = await bcrypt.hash(req.body.password, salt);
  9. let user = await User.create({
  10. email: req.body.email,
  11. password: secPass,
  12. });
  13. res.json({ user });
  14. });
  15. module.exports = router;

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

이후에는 mongoose 모듈의 create() 함수를 사용하여 users 모델의 규칙에 따라 데이터베이스에 문서를 생성합니다. 이 함수는 이메일과 비밀번호를 포함하는 Javascript 객체를 입력으로 받지만, 우리는 원시 문자열 대신에 데이터베이스에 저장할 secPass (비밀번호 해시 + 솔트)를 제공합니다. 이렇게 함으로써, 원시 문자열 대신 비밀번호의 해시와 솔트를 조합하여 안전하게 비밀번호를 저장할 수 있습니다. 마지막으로, 사용자 모델을 포함한 JSON 응답을 반환합니다. (이 응답 방식은 개발 단계에만 사용되며, 운영 단계에서는 인증 토큰 또는 다른 방법으로 대체해야 합니다).

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

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

이 명령은 서버를 로컬호스트의 포트 3300 (또는 지정한 포트)에서 실행합니다. 그런 다음, 다음 몸체를 가진 HTTP 요청을 URL http://localhost:3300/auth/signup로 보낼 수 있습니다:

  1. {
  2. "email":"[email protected]",
  3. "password":"sammy"
  4. }

이로써 다음 출력/응답을 얻을 수 있습니다:

{
  "user": {
    "email": "[email protected]",
    "password": "$2a$10$JBka/WyJD0ohkzyu5Wu.JeCqQm33UIx/1xqIeNJ1AQI9kYZ0Gr0IS",
    "_id": "654510cd8f1edaa59a8bb589",
    "__v": 0
  }
}

참고: 비밀번호 해시와 ID는 항상 고유하기 때문에 동일하지 않을 것입니다.

다음 섹션에서는 저장된 비밀번호의 해시에 접근하고 로그인/인증 시 제공된 비밀번호를 인증하는 방법을 배우게 됩니다.

암호화된 비밀번호에 액세스하고 인증에 사용하기

지금까지 BcryptJS, 해싱, 소금치기, 및 비밀번호를 해시로 저장하는 익스프레스 서버를 개발하는 방법에 대해 배웠습니다. 이제 애플리케이션에 로그인하려는 사용자의 저장된 비밀번호를 사용하여 사용자를 인증하는 방법을 배우게 될 것입니다.

인증 방법을 추가하려면 auth.js에서 /signup 라우트 뒤에 다음의 라우트를 추가하십시오:

  1. // ROUTE 2:
  2. router.post("/login", async (req, res) => {
  3. let user = await User.findOne({ email: req.body.email });
  4. if (!user) {
  5. return res.status(400).json({ error: "Login with proper credentials!" });
  6. }
  7. const passwordCompare = await bcrypt.compare(req.body.password, user.password);
  8. if (!passwordCompare) {
  9. return res
  10. .status(400)
  11. .json({ error: "Login with proper credentials!" });
  12. }
  13. res.json({ success: "Authenticated!" });
  14. });

여기에서는 /auth/login 하위 경로를 사용하여 이미 존재하는 사용자의 로그인 인증을 수행합니다. /auth/signup 엔드포인트와 유사하게, BcryptJS는 프로미스를 반환하므로 이는 async-await 함수가 될 것입니다.

먼저, 우리는 Mongoose 라이브러리의 findOne 함수를 사용합니다. 이 함수는 주어진 검색 쿼리를 기반으로 컬렉션에서 문서를 찾는 데 사용됩니다. 이 경우에는 이메일을 기반으로 사용자를 검색하고 있습니다. 제공된 이메일을 가진 사용자가 존재하지 않는 경우, 이는 잘못된 자격 증명으로 응답을 보내게 될 것입니다. (로그인 중 어떤 매개변수가 잘못되었는지 제공하는 것은 좋지 않은 실천입니다. 공격자가 그 정보를 사용하여 기존 계정을 찾을 수 있기 때문입니다).

제공된 이메일을 갖는 사용자가 존재하는 경우, 프로그램은 비밀번호 비교로 이동합니다. 이를 위해 BcryptJS는 첫 번째 인수로 원시 문자열을, 두 번째 인수로 해시 (소금과 함께 있는 경우 또는 없는 경우)를 취하는 compare() 메서드를 제공합니다. 그런 다음, 이는 Boolean promise를 반환합니다. 비밀번호가 해시와 일치하면 true를 반환하고, 그렇지 않으면 false를 반환합니다. 그런 다음, if 문을 사용하여 간단한 확인을 추가하고 비교에 기반하여 성공 또는 오류를 반환할 수 있습니다.

마지막으로, 라우터를 express router로 내보내어 app.js 시작 지점에서 라우트에 사용할 수 있도록 합니다.

이 라우트를 테스트하기 위해 다음 URL http://localhost:3300/auth/login로 다른 HTTP 응답을 보낼 수 있으며, 요청 본문은 다음과 같아야 합니다:

  1. {
  2. "email":"[email protected]",
  3. "password":"sammy"
  4. }

이 요청에는 다음과 같은 응답이 제공됩니다:

{
  "success": "Authenticated!"
}

최종적으로, auth.js는 다음과 같이 보일 것입니다:

  1. const express = require("express");
  2. const router = express.Router();
  3. const User = require("./User");
  4. const bcrypt = require("bcryptjs");
  5. // ROUTE 1:
  6. router.post("/signup", async (req, res) => {
  7. const salt = await bcrypt.genSalt(10);
  8. const secPass = await bcrypt.hash(req.body.password, salt);
  9. let user = await User.create({
  10. email: req.body.email,
  11. password: secPass,
  12. });
  13. res.json({ user });
  14. });
  15. // ROUTE 2:
  16. router.post("/login", async (req, res) => {
  17. let user = await User.findOne({ email: req.body.email });
  18. if (!user) {
  19. return res.status(400).json({ error: "Login with proper credentials!" });
  20. }
  21. const passwordCompare = await bcrypt.compare(req.body.password, user.password);
  22. if (!passwordCompare) {
  23. return res
  24. .status(400)
  25. .json({ error: "Login with proper credentials!" });
  26. }
  27. res.json({ success: "Authenticated!" });
  28. });
  29. module.exports = router;

결론

이 자습서에서는 BcryptJS를 사용하여 데이터베이스 비밀번호를 안전하게 저장하고 액세스하는 방법을 설명하기 위해 서버를 생성했습니다. 다음과 같은 추가 작업을 수행하여 더 나아갈 수 있습니다:

  • 사용자 자격 증명 검색, 사용자 자격 증명 업데이트 등 기타 작업에 대한 라우트 추가

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

안전하게 비밀번호를 처리하기 위한 여정이 시작됩니다. 다양한 기술과 기술을 사용하여 추가적인 보안을 추가하여 보다 안전하고 견고한 애플리케이션을 만들 수 있습니다.

Source:
https://www.digitalocean.com/community/tutorials/how-to-handle-passwords-safely-with-bcryptsjs-in-javascript