티스토리 뷰

1. 이번 목표:
    jwt와 비밀번호 암호화를 배워보자!

 

2. 배운 내용 요약

1. JWT

JWT(JSON Web Token)는 두 시스템 간에 JSON 형식으로 정보를 안전하게 전송하기 위한 토큰.
주로 인중과 정보 교환 목적으로 사용.

 

JWT의 구성

JWT는 .으로 구분된 헤더, 페이로드, 서명 세 가지 부분으로 이루어져 있다.

 

 

헤더(Header)

JWT의 타입과 해싱 알고리즘 정보를 담고 있다.
{
  "alg": "HS256",  // 알고리즘 (HMAC SHA256)
  "typ": "JWT"     // 타입
}

 

페이로드(Payload)

토큰에 담을 실제 데이터를 포함.
사용자 정보 같은 클레임(Claims)들이 여기에 포함되며, Base64로 인코딩되어 저장된다.
{
  "sub": "1234567890", // 사용자 ID
  "name": "John Doe",  // 사용자 이름
  "admin": true,       // 추가 클레임
  "exp": 1672531200    // 만료 시간 (UNIX Timestamp)
}

 

서명(Signature)

헤더와 페이로드의 무결성을 검증하기 위한 서명.
서명은 비밀 키와 함께 생성되며, 이를 통해 토큰이 변조되지 않았음을 확인할 수 있다.
HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

 

JWT 특징

1. 자체 포함:
    JWT는 필요한 모든 정보를 자체적으로 포함. 따라서 별도의 세션 상태를 서버에 저장하지 않아도 된다.
2. JSON 기반:
    JSON 형식을 사용하므로, 가볍고 읽기 쉬우며 다양한 프로그래밍 언어에서 쉽게 구현할 수 있다.
3. Base64URL 인코딩:
    토큰은 URL 안전한 문자열(Base64URL)로 인코딩되므로 HTTP 헤더, URL, 쿠키에 안전하게 전달할 수 있다.
4. 보안:
    비밀 키를 통해 서명되므로 데이터의 무결성을 보장하지만 암호화가 아닌 인코딩 방식이므로 민감한 데이터를
    포함하면 안 된다.

 

JWT 작동방식

1. 클라이언트 인증 요청:
    클라이언트가 사용자 이름과 비밀번호로 서버에 인증을 요청.
2. JWT 생성 및 반환:
    서버는 사용자 인증이 성공하면 JWT를 생성하여 클라이언트에 반환.
3. JWT 사용:
    클라이언트는 요청마다 JWT를 HTTP 헤더(Authorization: Bearer <token>)에 포함하여 서버에 전달.
4. 서버 검증:
    서버는 JWT를 받아 유효성을 검증하고, 페이로드 정보를 이용해 요청을 처리.

 

app.js

더보기
const express = require("express");
const jwt = require("jsonwebtoken");
const app = express();
const PORT = 8080;
const SECRET_KEY = "MLSureEDn3ItQYhD"; // 나중에는 .env에 저장해서 사용!

// Express 설정
app.set("view engine", "ejs");
app.use(express.urlencoded({ extended: false }));
app.use(express.json());

// DB 정보(가짜유저정보)
const userInfo = {
  id: "cocoa",
  pw: "1234",
  name: "코코아",
  age: 18,
};

app.get("/", (req, res) => {
  res.render("index");
});

app.get("/login", (req, res) => {
  res.render("login");
});

// POST /login
app.post("/login", (req, res) => {
  try {
    const { id, pw } = req.body;
    const { id: realId, pw: realPw } = userInfo;
    if (id === realId && pw === realPw) {
      // 로그인 성공 => jwt 발급
      const token = jwt.sign({ id }, SECRET);
      res.send({ result: true, token });
    } else {
      // 로그인 실패
      res.send({ message: "로그인 정보가 올바르지 않습니다", result: false });
    }
    res.send("login response");
  } catch (error) {
    res.status(500).send({ message: "서버 에러" });
  }
});

app.post("/token", (req, res) => {
  try {
    console.log(req.headers.authorization); // Bearer sdfs.sdfs.ddfd
    if (req.headers.authorization) {
      const token = req.headers.authorization.split(" ")[1];
      try {
        // 토큰 검증 작업
        const auth = jwt.verify(token, SECRET);
        if (auth.id === userInfo.id) {
          res.send({ result: true, name: userInfo.name });
        }
      } catch (error) {
        console.log("토큰 인증 에러");
        res
          .status(401)
          .send({ result: false, message: "인증된 회원이 아닙니다." });
      }
    } else {
      // 인증정보 없을 때
      res.redirect("/login");
    }
  } catch (error) {
    res.status(500).send({ message: "서버 에러" });
  }
});

app.listen(PORT, () => {
  console.log(`http://localhost:${PORT}`);
});

 

index.ejs

더보기
<!DOCTYPE html>
<html lang="ko">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>JWT</title>
    <!-- axios CDN -->
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  </head>
  <body>
    <h1>JWT</h1>
    <div id="info"></div>
    <script>
      (async function () {
        const token = localStorage.getItem("login");
        const info = document.getElementById("info");
        let data;
        if (!token) {
          data = '<a href="/login">로그인</a>';
        } else {
          const response = await axios({
            method: "post",
            url: "/token",
            headers: {
              Authorization: `Bearer ${token}`,
            },
          });
          if (response.data.result) {
            data = `<p>${response.data.name}님 환영합니다!</p>
            <button onclick="logout()">로그아웃</button>`;
          }
        }
        info.innerHTML = data;
      })();

      function logout() {
        localStorage.clear();
        document.location.reload();
      }
    </script>
  </body>
</html>

 

 

login.ejs

더보기
<!DOCTYPE html>
<html lang="ko">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>login</title>
    <!-- axios CDN -->
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  </head>
  <body>
    <h1>로그인</h1>
    <form name="login-form">
      <input type="text" id="id" placeholder="아이디를 입력하세요" />
      <input type="password" id="pw" placeholder="비밀번호를 입력하세요" />

      <button type="button" onclick="login()">로그인</button>
    </form>
    <script>
      // POST /login
      async function login() {
        const form = document.forms["login-form"];
        const loginResponse = await axios({
          method: "post",
          url: "/login",
          data: {
            id: form.id.value,
            pw: form.pw.value,
          },
        });
        console.log(loginResponse.data);

        const { message, token, result } = loginResponse.data;
        if (result) {
          // 토큰 정보 로컬스토리지에 저장
          localStorage.setItem("login", token);
          document.location.href = "/";
        } else {
          alert(message);
          form.reset();
        }
      }
    </script>
  </body>
</html>

 

 

2. 비밀번호 암호화/복호화

crypto: 데이터 암호화 및 해시 생성이 필요한 경우 사용.
bcrypt: 비밀번호 해싱 및 비교와 같이 사용자 인증 시스템에 최적화.

crypto 암호화

Node.js의 내장 모듈로, 다양한 암호화 알고리즘을 제공하여 데이터를 암호화하거나 해싱하는 데 사용.
주요 기능:
    단방향 해시: 데이터를 복호화할 수 없는 고유의 값으로 변환 (e.g., 비밀번호 해시).
    대칭키 암호화: 동일한 키로 데이터를 암호화/복호화.
    비대칭키 암호화: 공개 키와 개인 키를 사용해 데이터를 암호화/복호화.

 

단방향 해시(crypto.createHash)

const crypto = require('crypto');

// 단방향 해시 생성
const password = "mySecurePassword";
const hash = crypto.createHash('sha256').update(password).digest('hex');

console.log("원본 비밀번호:", password);
console.log("SHA-256 해시:", hash);

// 실행 결과
// 원본 비밀번호: mySecurePassword
// SHA-256 해시: 4a8a08f09d37b73795649038408b5f33b3d8b0291f251e20b5f9b0b3b2c9eb5e

 

 

대칭키 암호화(crypto.createCipheriv)

데이터를 암호화한 후 같은 키를 사용해 복호화하는 방식
중요한 데이터를 클라이언트-서버 간 안전하게 전송할 때 사용
const crypto = require('crypto');

// 암호화/복호화에 사용할 키와 IV
const key = crypto.randomBytes(32); // 32바이트 키
const iv = crypto.randomBytes(16);  // 16바이트 IV (초기화 벡터)

const encrypt = (text) => {
  const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
  let encrypted = cipher.update(text, 'utf8', 'hex');
  encrypted += cipher.final('hex');
  return encrypted;
};

const decrypt = (encryptedText) => {
  const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
  let decrypted = decipher.update(encryptedText, 'hex', 'utf8');
  decrypted += decipher.final('utf8');
  return decrypted;
};

const text = "Hello, World!";
const encrypted = encrypt(text);
const decrypted = decrypt(encrypted);

console.log("원본 텍스트:", text);
console.log("암호화된 텍스트:", encrypted);
console.log("복호화된 텍스트:", decrypted);

// 실행 결과
// 원본 텍스트: Hello, World!
// 암호화된 텍스트: d4f68cd909d98abfc9a2f8e75e6c09e9
// 복호화된 텍스트: Hello, World!

 

 

bcrypt 암호화

비밀번호를 암호화(해시)하고, 검증할 때 사용되는 라이브러리.
단방향 해싱에 특화되어 있으며, 해시와 함께 고유한 "솔트(salt)"를 생성해 보안을 강화한다.
사용자 비밀번호 암호화, 인증 시스템에 주로 사용된다.
주요 기능:
    솔트(salt) 추가: 동일한 비밀번호라도 해시 값이 매번 다르게 생성됩니다.
    비밀번호 검증: 해시 값과 입력 비밀번호를 비교하여 인증.
const bcrypt = require('bcrypt');

const saltRounds = 10; // 솔트 라운드 수 (값이 클수록 보안 강화, 성능 저하)
const password = "mySecurePassword";

// 비밀번호 해시 생성
bcrypt.hash(password, saltRounds, (err, hash) => {
  if (err) throw err;

  console.log("원본 비밀번호:", password);
  console.log("해시된 비밀번호:", hash);

  // 비밀번호 검증
  bcrypt.compare(password, hash, (err, result) => {
    if (err) throw err;
    console.log("비밀번호 일치 여부:", result); // true
  });
});


// 실행 결과
// 원본 비밀번호: mySecurePassword
// 해시된 비밀번호: $2b$10$gXZnPd1V0UjEhfGzWbb9ke6jc5c5nqUNyJ1ydtyZ5Wih6ZMz7PZPC
// 비밀번호 일치 여부: true

 

특징 crypto bcrypt
용도 일반적인 암호화 및 해시 생성 비밀번호 해싱 및 검증
솔트 수동으로 솔트 추가 필요 자동으로 솔트 추가
복호화 가능 여부 대칭키 암호화의 경우 복호화 가능 단방향 해싱으로 복호화 불가능
사용사례 데이터 암호화, 전송 보안 비밀번호 암호화 및 비교

 

 

 

3. 회고

더보기

예전에 부트캠프에서 jwt 관련하여 팀원과 마찰이 벌어졌던 적이 있다. 팀원은 유저가 로그인 하면 아이디랑 비밀번호를 프론트 단에서 상태관리 하자고 했고, 나는 백에서 jwt로 토큰을 전해줄텐데 굳이 보안 위험을 감수하면서 그래야 하는 이유가 무엇이냐는 입장이었다. 이번 강의에서 jwt와 비밀번호 암호화를 배우며 다시 한 번 jwt가 왜 필요한지 깨닫게 되었다. 그리고 나는 jwt도 결국 암호화가 되는 것이라고 착각했는데 사용자 데이터를 안전하게 지키기 위해서는 암호화 방식도 같이 사용해야 한다는 것을 제대로 알게 되었다.

앞으로 프로젝트 할 때 사용자 인증/인가 부분은 제대로 신경써서 작성할 수 있을 것 같다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2026/06   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함