🌱SeSAC x CodingOn 웹 취업 부트캠프

[새싹/코딩온] 풀스택 웹 개발자 취업 부트캠프 7주차 (2): JWT, 비밀번호 암호화

haeriyouu 2025. 1. 5. 18:30

JWT

💡OAuth란? (Open Authorization)

  • 사용자의 리소스에 대한 접근 권한을 안전하게 위임하기 위한 개방형 표준 프로토콜!
  • 사용자가 비밀번호를 제공하지 않고도 제3자 어플리케이션에 권한을 부여할 수 있다.
  • 주로 소셜 미디어 로그인이나 외부 서비스 연동에 사용된다.

💡 JWT (Json Web Token)

  • 사용자 인증에 주로 사용되는 토큰 기반 인증 방식!
  • JSON 형식의 정보를 안전하게 전송하기 위한 표준 규격
  • Header, Payload, Signature 세 부분으로 구성된다.
  • 토큰 자체에 사용자 정보를 포함하고 있어 서버에서 별도의 세션 저장소가 필요 없다.
  • 주로 웹 어플리케이션에서 사용자 인증을 위해 사용.
더보기
// app.js

// POST /login
app.post("/login", (req, res) => {
  try {
    const { id, pw } = req.body;
    const { id: realId, pw: realPw } = userInfo; // DB에 저장되어있는 데이터

    // DB 데이터와 비교
    if (id === realId && pw === realPw) {
      // 로그인 성공
      // jwt 발급
      const token = jwt.sign({ id: id }, SECRET); // sign:(payload, signature)
      console.log("토큰>>>", token);
      res.send({ result: true, token }); // {result: true, token: 'eyJhbGciOi(중략)'}
      // jwt token은 클라이언트에서 관리하기 때문에 클라이언트에게 토큰을 보내주어야 함
    } else {
      // 로그인 실패
      res.send({ message: "로그인 정보가 올바르지 않습니다.", result: false });
      // {message: '로그인 정보가 올바르지 않습니다.', result: false}
    }
  } catch (err) {
    console.log("post /login err", err);
    res.status(500).send({ message: "서버에러" });
  }
});

💻 JWT와 OAuth의 차이점

  1. 목적: JWT는 주로 인증(authentication)에, OAuth는 권한 부여(authorization)에 사용
  2. 복잡성: JWT는 비교적 단순한 구조를 가지고 있지만, OAuth는 여러 단계와 참여자가 있는 복잡한 프로토콜
  3. 사용 사례: JWT는 단일 어플리케이션 내에서의 인증에 적합하고, OAuth는 여러 서비스 간의 권한 위임에 적합
  4. 토큰 내용: JWT는 사용자 정보를 직접 포함하지만, OAuth의 액세스 토큰은 일반적으로 랜덤한 문자열

비밀번호 암호화

💻 암호화 종류

💡 단방향 암호화

  • 암호화된 데이터를 원래의 형태로 복호화 할 수 없다.
  • 동일한 입력값에 대해 항상 같은 해시값을 생성!
  • 주로 해시 함수를 사용하여 구현 
  • 대표적인 알고리즘: SHA-256, SHA-512

👍🏻 장점

  • 복호화가 불가능하므로 보안성이 높다.
  • 데이터베이스가 해킹되어도 원본 비밀번호를 알아내기 어렵다.
더보기
const crypto = require("crypto");
/*
1. crypto를 통해 단방향 암호화 구현 -> 복호화 불가능
- createHash(알고리즘)
- pdkdf2Sync(비밀번호, sort, 해시 반복횟수, 키의 길이, 알고리즘)
*/

// 1-1) createHash(알고리즘).update(평문).digest(인코딩방식)
// 인코딩 방식: base64, hex, ascii, binary
// digest: 암호화 된 문장을 우리가 읽을 수 있는 문자열로 인코딩하는 것
const createHashPW = (pw) => {
  return crypto.createHash("sha512").update(pw).digest("base64");
};

console.log(createHashPW("1234"));
console.log(createHashPW("1234"));
console.log(createHashPW("1234")); // 전부 똑같은 값
console.log(createHashPW("1234.")); // 조금만 변해도 전혀 다른 해시값이 나옴

// 1-2) pdkdf2Sync(비밀번호, sort, 해시 반복횟수, 키의 길이, 알고리즘).toString(인코딩방식)
function saltAndHashPW(pw) {
  const salt = crypto.randomBytes(16).toString("base64");
  const iterations = 100;
  const keylen = 64;
  const algorithm = "sha512";

  const hash = crypto
    .pbkdf2Sync(pw, salt, iterations, keylen, algorithm) // buffer 객체를 리턴하는 중
    .toString("base64");
  return { salt, hash };
} // DB에 salt와 hash 모두 저장해야 함

console.log("pbkdf2Sync >> ", saltAndHashPW("1234"));
console.log("pbkdf2Sync >> ", saltAndHashPW("1234"));
console.log("pbkdf2Sync >> ", saltAndHashPW("1234"));

// 암호 비교 함수
function checkPw(inputPW, savedSalt, savedHash) {
  const iterations = 100;
  const keylen = 64;
  const algorithm = "sha512";
  // 반복횟수, 길이, 알고리즘 모두 saltAndHashPW와 같아야 함 (비교를 위해)

  // pbkdf2Sync의 모든 인자가 똑같다면 해시값도 똑같다.
  const hash = crypto
    .pbkdf2Sync(inputPW, savedSalt, iterations, keylen, algorithm)
    .toString("base64");

  return hash === savedHash;
}
const realPW = "qwer1234";
console.log("====================");
const data = saltAndHashPW(realPW);
console.log("data >> ", data);
const { salt: DBsalt, hash: DBhash } = data;
console.log("=========비밀번호 일치 여부 확인==========");
const isMatch = checkPw("qwqw", DBsalt, DBhash);
const isMatch2 = checkPw("qwer1234", DBsalt, DBhash);
console.log(isMatch); // false, 비밀번호 불일치
console.log(isMatch2); // true, 비밀번호 일치

#️⃣해시(Hash)#️⃣

  • 임의의 길이를 가진 데이터를 고정된 길이의 값으로 변환하는 과정 또는 그 결과를 말한다. 이 과정에서 사용되는 함수를 해시 함수라고 한다!

#️⃣ 해시의 주요 특징

  1. 고정 길이 출력: 입력 데이터의 크기와 상관없이 항상 동일한 길이의 해시값을 생성
  2. 일방향성: 해시값으로부터 원본 데이터를 복원하는 것은 계산적으로 불가능
  3. 결정론적: 동일한 입력에 대해 항상 같은 해시값을 생성
  4. 충돌 저항성: 서로 다른 입력에 대해 동일한 해시값이 나오는 확률이 매우 낮음

💡 양방향 암호화

  • 암호화와 복호화가 모두 가능
  • 대칭키와 비대칭키(공개키) 방식으로 나뉨

1️⃣ 대칭키

  • 암호화와 복호화에 동일한 키를 사용
  • 속도가 빠르지만 키 관리가 어렵다.
  • 대표적인 알고리즘: AES, DES

2️⃣ 비대칭키(공개키)

  • 암호화와 복호화에 서로 다른 키를 사용
  • 키 관리가 용이하지만 속도가 느리다.
  • 대표적인 알고리즘: RSA
더보기
/*
2. 양방향 알고리즘
- createCipheriv: 암호화
- createDecipheriv: 복호화
*/
const key = crypto.randomBytes(32);
const iv = crypto.randomBytes(16); // 비밀 수
const algorithm = "aes-256-cbc";
const originalMessage = "hello, world!"; // 원본 메세지, 평문

// 암호화
function encrypt(text) {
  // 1. 암호화 객체 생성
  // const cipher = crypto.createCipheriv(algorithm, key, iv);
  const cipher = crypto.createCipheriv(algorithm, key, iv);

  // 2. 실제 데이터 암호화
  // let encrypted = cipher.update(평문, 입력 인코딩, 출력 인코딩);
  let encrypted = cipher.update(text, "utf8", "base64");

  // 3. 최종 결과 생성
  encrypted += cipher.final("base64");
  return encrypted; // 암호화된 데이터
}
console.log(encrypt(originalMessage));
console.log(encrypt(originalMessage));

// 복호화
function decrypt(encryptedText) {
  // 1. 복호화 객체 생성
  const decipher = crypto.createDecipheriv(algorithm, key, iv);

  // 2. 실제 데이터 복호화
  // base64 방식으로 인코딩된 문자열이 utf8로 복호화됨
  let decrypted = decipher.update(encryptedText, "base64", "utf8");

  // 3. 최종 결과 생성
  decrypted += decipher.final("utf8");
  return decrypted;
}
const encryptedMessage = encrypt(originalMessage);
console.log("암호화 된 문장: ", encryptedMessage);

const decryptedMessage = decrypt(encryptedMessage);
console.log("복호화 된 문장: ", decryptedMessage);