40일차 Node.js

정상희·2025년 5월 20일

코딩공부

목록 보기
50/60

현재 오즈코딩스쿨 강의를 통해 프론트엔드를 학습하고 있습니다.
본 포스트는 해당 강의에 대한 내용 정리를 목적으로 합니다.

Token

1. JWT 토큰

JWT (JSON Web Token)는 로그인한 사용자를 인증하기 위해 서버가 발급하는 디지털 신분증이다.
JWT 공식사이트

Base64 Decode & Encode

1) JWT의 구성

a. Header (헤더)

  • 어떤 암호화 알고리즘을 사용하는지 명시하는 부분이다.
  • 예: HS256이라는 알고리즘을 사용한다.

b. Payload (페이로드)

  • 사용자 정보(예: userId, name, role 등)를 담는 부분이다.
  • 중요한 정보(비밀번호 등)는 넣지 않는 것이 좋다.

c. Signature (서명)

  • 헤더와 페이로드를 서버의 비밀키로 암호화한 값이다.
  • 토큰이 위조되었는지 검증하는 데 사용된다.

2) JWT의 작동 방식

  • 사용자가 ID와 비밀번호로 로그인하면, 서버는 확인 후 JWT를 발급한다.

  • 클라이언트는 받은 토큰을 로컬스토리지나 쿠키 등에 저장한다.

  • 이후 API 요청 시, 토큰을 Authorization 헤더에 담아 보낸다.

  • 서버는 토큰을 해석하여 사용자를 식별하고, 유효하면 요청을 처리한다.


3) JWT의 장점

  • 서버가 로그인 상태를 따로 저장하지 않아도 되므로 무상태(stateless) 인증이 가능하다.

  • 토큰만으로 사용자 확인이 가능해 빠르다.

  • 여러 시스템에서 공통으로 사용할 수 있어 SSO(싱글사인온) 구현에 유리하다.


4) 주의 사항

  • JWT는 누구나 디코딩할 수 있기 때문에 중요한 정보는 절대 넣지 말아야 한다.
  • 토큰을 저장할 때는 XSS나 CSRF 공격을 방지할 수 있는 안전한 방식이어야 한다.
    예를 들어, 민감한 정보는 HttpOnly 쿠키에 저장하는 것이 바람직하다.



2. 토큰의 종류

  • 인증 토큰: 사용자의 신원을 확인하고 세션을 유지하기 위해 사용하는 토큰이다.
    예: JWT(JSON Web Token), OAuth 토큰 등이 있다.

  • 접근 토큰(access token): 특정 자원에 대한 접근 권한을 부여받았음을 나타내는 토큰이다.
    일반적으로 API 요청 시 헤더에 포함되어 사용된다.

  • 리프레시 토큰(refresh token): 접근 토큰이 만료되었을 때, 새로운 접근 토큰을 발급받기 위해 사용하는 토큰이다.
    보안 상 서버에만 저장하는 것이 일반적이다.

  • CSRF 토큰: 크로스 사이트 요청 위조(CSRF)를 방지하기 위해 사용하는 임시 토큰이다.
    주로 폼 전송 시 함께 전송되어 위조 요청을 구분하는 데 사용된다.

  • 세션 토큰: 사용자의 로그인 세션을 식별하기 위해 서버에서 생성하는 토큰이다.
    쿠키에 저장되어 클라이언트와 서버 간의 상태를 유지하는 데 사용된다.

  • API 토큰: 특정 API를 사용할 수 있는 권한을 인증하기 위해 발급되는 토큰이다.
    사용량 제한(rate limit)이나 권한 범위(scope)를 설정할 수 있다.

  • 임베디드 토큰: URL 파라미터 등에 포함되어 인증 또는 식별 목적으로 전달되는 토큰이다.
    보안에 취약하므로 사용 시 주의가 필요하다.



3. 실습

1) 모듈 설치

npm i jsonwebtoken

2) 코드

server-header.js

const express = require('express') // Express 프레임워크를 가져옵니다. 웹 서버를 쉽게 구축할 수 있게 도와줍니다.
const cors = require('cors') // CORS 미들웨어를 가져옵니다. 다른 도메인에서의 요청을 허용/제한하는 데 사용됩니다.
const cookieParser = require('cookie-parser') // cookie-parser 미들웨어를 가져옵니다. (이 코드에서는 사용되지 않지만 포함되어 있습니다.)
const jwt = require('jsonwebtoken') // jsonwebtoken 라이브러리를 가져옵니다. JWT(JSON Web Token)를 생성하고 검증하는 데 사용됩니다.

// 임시 사용자 데이터 배열입니다. 실제 애플리케이션에서는 데이터베이스를 사용해야 합니다.
const users = [
  {
    user_id: 'test',
    user_password: '1234',
    user_name: '테스트 유저',
    user_info: '테스트 유저입니다'
  }
]

const app = express() // Express 애플리케이션 인스턴스를 생성합니다.

// CORS (Cross-Origin Resource Sharing) 설정입니다.
// 클라이언트가 다른 도메인에서 서버로 요청을 보낼 수 있도록 허용합니다.
app.use(cors({
  // `origin`: 요청을 보낼 수 있는 클라이언트의 도메인을 설정합니다.
  // 개발 환경에서는 'http://127.0.0.1:[사용중인 포트번호]' 또는 'http://localhost:[사용중인 포트번호]'를 사용합니다.
  origin: ['http://127.0.0.1:[사용중인 포트번호]', 'http://localhost:[사용중인 포트번호]'],
  // `methods`: 허용할 HTTP 요청 메서드를 설정합니다.
  methods: ['OPTIONS', 'POST', "GET", "DELETE"],
  // `credentials`: 클라이언트와 서버 간에 자격 증명(쿠키 등)을 주고받을 수 있도록 허용합니다.
  // 이 코드에서는 JWT를 쿠키가 아닌 `Authorization` 헤더로 주고받으므로, 이 설정은 필수는 아니지만,
  // 이전 코드와의 일관성을 위해 유지될 수 있습니다.
  credentials: true
}))

// `cookieParser` 미들웨어는 요청 헤더에 포함된 쿠키를 파싱하여 `req.cookies` 객체에 저장합니다.
// 현재 코드에서는 쿠키를 사용하지 않으므로 이 미들웨어는 필요하지 않습니다.
app.use(cookieParser())
// `express.json()` 미들웨어는 요청 본문에 JSON 형식의 데이터가 있을 경우 이를 파싱하여 `req.body` 객체에 저장합니다.
app.use(express.json())

// JWT 서명 및 검증에 사용될 비밀 키입니다.
// 실제 프로덕션 환경에서는 이 키를 환경 변수 등으로 관리하고, 더 길고 복잡하게 생성해야 합니다.
const secretKey = 'ozcodingschool'

// POST 요청 핸들러: 로그인 처리
app.post('/', (req, res) => {
  const { userId, userPassword } = req.body // 요청 본문에서 userId와 userPassword를 추출합니다.
  // 사용자 배열에서 제공된 ID와 비밀번호에 해당하는 사용자를 찾습니다.
  const userInfo = users.find(el => el.user_id === userId && el.user_password === userPassword)

  // 사용자를 찾지 못했거나 비밀번호가 일치하지 않을 경우
  if (!userInfo) {
    res.status(401).send('로그인 실패') // 401 Unauthorized 상태 코드와 메시지를 응답합니다.
  } else {
    // 사용자를 찾은 경우 JWT를 생성합니다.
    // `jwt.sign()`: JWT를 생성하는 함수입니다.
    // 첫 번째 인자: 페이로드 (JWT에 담을 데이터, 여기서는 사용자 ID)
    // 두 번째 인자: 비밀 키
    // 세 번째 인자: 옵션 (여기서는 토큰 만료 시간 10분)
    const accessToken = jwt.sign({ userId: userInfo.user_id }, secretKey, { expiresIn: 1000 * 60 * 10 })
    // 생성된 JWT를 클라이언트에 응답 본문으로 직접 전송합니다.
    // 이 방식은 클라이언트가 토큰을 localStorage나 sessionStorage에 저장하여 관리하게 됩니다.
    res.send(accessToken) // 생성된 토큰을 응답합니다.
  }
})

// GET 요청 핸들러: 사용자 정보 조회 (인증 필요)
app.get('/', (req, res) => {
  // `req.headers.authorization`: 클라이언트가 'Authorization' 헤더에 담아 보낸 토큰을 가져옵니다.
  // 보통 'Bearer [토큰]' 형식으로 오므로, 'Bearer ' 부분을 제외하고 실제 토큰만 추출합니다.
  const authHeader = req.headers.authorization;

  // Authorization 헤더가 없거나 'Bearer ' 형식으로 시작하지 않으면 에러 처리
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).send('인증 토큰이 제공되지 않았거나 형식이 올바르지 않습니다.');
  }

  const accessToken = authHeader.split(' ')[1]; // 'Bearer ' 다음에 오는 실제 토큰 문자열을 추출합니다.

  try {
    // `jwt.verify()`: JWT를 검증하고 페이로드(payload)를 디코딩합니다.
    // 첫 번째 인자: 검증할 JWT
    // 두 번째 인자: 비밀 키
    const payload = jwt.verify(accessToken, secretKey);
    // 페이로드에서 추출한 userId를 사용하여 사용자 배열에서 해당 사용자 정보를 찾습니다.
    const userInfo = users.find(el => el.user_id === payload.userId); 
    
    // 사용자 정보가 없으면 404 Not Found 응답을 보냅니다.
    if (!userInfo) {
      return res.status(404).send('사용자 정보를 찾을 수 없습니다.');
    }

    // 사용자 정보를 JSON 형태로 클라이언트에 응답합니다.
    return res.json(userInfo);
  } catch (error) {
    // JWT 검증 중 오류가 발생한 경우 (예: 토큰 만료, 잘못된 서명)
    if (error.name === 'TokenExpiredError') {
      return res.status(401).send('토큰이 만료되었습니다.'); // 토큰 만료 에러 처리
    }
    return res.status(401).send('유효하지 않은 토큰입니다.'); // 그 외 유효하지 않은 토큰 에러 처리
  }
})

// DELETE 요청 핸들러: 로그아웃 처리 (이 버전에서는 삭제됨)
// 이전 코드와 달리 DELETE 엔드포인트가 없습니다.
// JWT를 HTTP 헤더로 관리하는 방식에서는 클라이언트 측에서 토큰을 삭제하면 되므로,
// 서버 측에서 특별한 로그아웃 로직(예: 쿠키 삭제)이 필요 없을 수 있습니다.
// 하지만 서버 측에서 토큰 블랙리스트를 관리하는 경우에는 이 엔드포인트가 필요할 수 있습니다.

// 서버를 3000번 포트에서 실행합니다.
app.listen(3000, () => console.log('서버 실행!'))

login-header.js

const form = document.querySelector('form')
const idInput = document.querySelector('#user_id')
const passwordInput = document.querySelector('#user_password')
const loginButton = document.querySelector('#login_button')

const main = document.querySelector('main')
const userName = document.querySelector('#user_name')
const userInfo = document.querySelector('#user_info')
const logoutButton = document.querySelector('#logout_button')

// axios의 기본 설정으로, 모든 요청에 대해 자격 증명(쿠키, HTTP 인증 등)을 포함하도록 합니다.
// 이는 서버와의 세션 관리에 유용합니다.
axios.defaults.withCredentials = true;
let accessToken = '' // 로그인 성공 후 서버로부터 받은 accessToken을 저장할 변수입니다.

// 폼 제출 시 기본 동작(페이지 새로고침)을 막습니다.
form.addEventListener('submit', (e) => e.preventDefault())

// 로그인 함수: 사용자 ID와 비밀번호를 서버로 전송하여 로그인 요청을 보냅니다.
function login() {
  const userId = idInput.value // ID 입력 필드의 값
  const userPassword = passwordInput.value // 비밀번호 입력 필드의 값

  // 'http://localhost:3000'으로 POST 요청을 보냅니다.
  return axios.post('http://localhost:3000', { userId, userPassword })
  // 요청 성공 시, 서버 응답(res.data)을 accessToken 변수에 저장합니다.
  .then(res => accessToken = res.data)
}

// 로그아웃 함수: accessToken을 초기화하여 로그아웃 상태로 만듭니다.
// 실제 애플리케이션에서는 서버에도 로그아웃 요청을 보내 세션을 무효화하는 로직이 추가될 수 있습니다.
function logout() {
  accessToken = ''
}

// 사용자 정보 조회 함수: 저장된 accessToken을 사용하여 서버에 사용자 정보 요청을 보냅니다.
function getUserInfo() {
  return axios.get('http://localhost:3000', {
    // 요청 헤더에 Authorization 필드를 추가하고, Bearer 토큰 방식으로 accessToken을 전송합니다.
    headers: { 'Authorization' : `Bearer ${accessToken}`}
  })
}

// 사용자 정보를 화면에 렌더링하는 함수:
// 메인 섹션을 보이게 하고, 로그인 폼을 숨깁니다.
// 사용자 이름과 정보를 해당하는 요소에 표시합니다.
function renderUserInfo(user) {
  main.style.display = 'block' // 메인 섹션 표시
  form.style.display = 'none' // 로그인 폼 숨김
  userName.textContent = user.user_name // 사용자 이름 표시
  userInfo.textContent = user.user_info // 사용자 정보 표시
}

// 로그인 폼을 화면에 렌더링하는 함수:
// 메인 섹션을 숨기고, 로그인 폼을 보이게 합니다.
// 이전에 표시되던 사용자 이름과 정보를 초기화합니다.
function renderLoginForm() {
  main.style.display = 'none' // 메인 섹션 숨김
  form.style.display = 'grid' // 로그인 폼 표시 (CSS 그리드 레이아웃)
  userName.textContent = '' // 사용자 이름 초기화
  userInfo.textContent = '' // 사용자 정보 초기화
}

// 로그인 버튼 클릭 시 실행되는 이벤트 핸들러:
loginButton.onclick = () => {
  login() // 1. 로그인 함수 호출
    .then(() => getUserInfo()) // 2. 로그인 성공 시, 사용자 정보 조회 함수 호출
    .then(res => renderUserInfo(res.data)) // 3. 사용자 정보 조회 성공 시, 화면에 정보 렌더링
}

// 로그아웃 버튼 클릭 시 실행되는 이벤트 핸들러:
logoutButton.onclick = () => {
  logout() // 1. 로그아웃 함수 호출 (accessToken 초기화)
  renderLoginForm() // 2. 로그인 폼을 다시 화면에 표시
}
profile
UI/UX디자이너의 코딩 공부

0개의 댓글