node.js : 쿠키와 세션

코딩로그·2025년 3월 18일
0

웹 인증 및 보안 기초

  • 클라이언트에 저장되는 짧은 텍스트
  • HTTP의 무상태성을 보완
    • 무상태성: 서버의 기억(상태)가 없음

쿠키의 역할

  • 서버는 요청과 응답을 메모
  • 쿠키 저장: 클라이언트가 담당 (cookie: 클라이언트 → 서버)
  • 쿠키 관리: 서버가 담당 (set-cookie: 서버 → 클라이언트)
    • 쿠키 저장, 삭제 등

쿠키 활용 사례

  • 사이트 모달창의 ‘오늘 하루동안 보지 않기’ → 사이트 다시 접속할 때 서버는 쿠키를 확인하여 모달창을 띄우지 않음
  • 검색창 이력
    • 접속한 사이트 외의 곳에서 쿠키를 활용 → 3자 쿠키

쿠키 생성/삭제 실습

서버 생성

  • cookie-parser : 클라이언트에서 전송된 쿠키를 해석하고 사용할 수 있도록 해주는 미들웨어
npm init -y
npm install express cors cookie-parser

실습할 html & js 파일 생성

  • index.html
    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
        <link rel="stylesheet" href="style.css">
    </head>
    
    <body>
        <div>
            <button id="set-cookie">쿠키 추가</button>
            <button id="delete-cookie">쿠키 삭제</button>
        </div>
        <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
        <script src="cookie.js"></script>
    </body>
    
    </html>
  • login.js
    const setCookieButton = document.getElementById('set-cookie');
    const deleteCookieButton = document.getElementById('delete-cookie');
    
    // 클라이언트 측에서도 credntials 설정 필수
    axios.defaults.withCredentials = true;
    
    setCookieButton.onclick = () => {
        axios.get('http://localhost:3000')
        .then(res => console.log(res))
    }
    
    deleteCookieButton.onclick = () => {
        axios.delete('http://localhost:3000')
        .then(res => console.log(res))
    }

서버 생성 및 CORS 설정

  • 쿠키는 브라우저의 보안정책 (CORS 및 SamSite 정책)으로 인해 credentials가 true여야 함
  • 개인정보 보호를 위해 자동으로 다른 도메인(크로스 오리진)에서 전송되지 않음
  • 브라우저는 CORS 요청(다른 도메인으로 보내는 요청)에서는 쿠키를 기본적으로 포함하지 않음.
    • 예를 들어, http://127.0.0.1:5500에서 http://localhost:3000으로 요청할 때, 기본적으로 쿠키가 전달되지 않음.
  • 서버에서도 CORS 설정에서 credentials: true를 활성화해야 쿠키를 클라이언트가 받을 수 있음.

  • server.js
    const express = require("express")
    const cors = require("cors")
    const cookieParser = require("cookie-parser")
    
    const app = express();
    
    app.use(cors({
        origin: ["http://127.0.0.1:5500", "http://localhost:5500"],
        methods : ["OPTIONS", "GET", "DELETE"],
        // 인증 정보 저장
        credentials: true
    }))
    
    app.use(cookieParser())

express로 get 및 delete 요청 처리하기

app.get('/', (req, res) => {
    // 첫 번째 인자는 쿠키의 이름, 두 번째 인자는 값, 세 번째 인자는 쿠키를 저장할 때 사용할 수 있는 옵션
    res.cookie('text-cookie', 'my cookie', {maxAge: 100000, httpOnly: true, secure: true})
    res.send('쿠키 생성 완료')
})

app.delete('/', (req, res) => {
    // 첫 번째 인자는 삭제하고자 하는 쿠키의 이름, 두 번째 인자는 생성 때 설정했던 옵션 (만료 옵션은 쿠키마다 다를 수 있기 때문에 생략해도 괜찮음. 배포환경에서 다르면 삭제가 안되는 경우도 있음)
    res.clearCookie('text-cookie', {httpOnly: true, secure: true})
    res.send('쿠키 생성 완료')
})

app.listen(3000, () => console.log('3000번 포트에서 서버 실행'))

프론트엔드 측 js 설정

const setCookieButton = document.getElementById('set-cookie');
const deleteCookieButton = document.getElementById('delete-cookie');

// 클라이언트 측에서도 credntials 설정 필수
axios.defaults.withCredentials = true;

setCookieButton.onclick = () => {
    axios.get('http://localhost:3000')
    .then(res => console.log(res))
}

deleteCookieButton.onclick = () => {
    axios.delete('http://localhost:3000')
    .then(res => console.log(res))
}

쿠키 확인 방법

inspection > Network > Headers 에서 http 요청과 응답 확인 가능

inspection > Application > Cookies 에서 쿠키 확인 가능

쿠키 구성 요소

속성설명
Name쿠키의 이름
Value쿠키에 저장된 값
Domain쿠키가 적용되는 도메인
Path쿠키가 유효한 경로 ('/'이면 사이트 전체에서 유효)
Expires / Max-Age쿠키의 만료 시간 (Session이면 브라우저 종료 시 삭제)
Size쿠키의 크기 (바이트 단위)
HttpOnlytrue이면 JavaScript에서 접근 불가 (보안 강화, true 권장)
Securetrue이면 HTTPS에서만 쿠키 전송 가능 (true 권장)
SameSiteCSRF 방지를 위한 설정 (Lax, Strict, None 가능)
Partition Key Site브라우저의 쿠키 격리 기능 관련 속성
Cross Site쿠키가 사이트 간 요청에서 어떻게 처리되는지
Priority쿠키의 우선순위 (Low, Medium, High)

Session

세션이란?

  • 사용자가 인증에 성공한 상태
  • 세션 기반 인증 (Session-based authentication)
    • 사용자가 인증에 성공한 상태를 서버에 저장해서 관리하는 방식

세션 기반 인증 흐름

세션 기반 인증 흐름

  1. 클라이언트가 아이디와 패스워드를 입력해 서버에 로그인 요청
  2. 서버에서 내용 확인 후 로그인/인증 성공 판단
  3. 세션 저장소에 정보 저장 후 세션 ID 발급
  4. 세션 ID를 클라이언트에게 쿠키로 전달
  5. 클라이언트는 세션 쿠키를 들고 서버에 요청
  6. 서버는 세션 저장소에서 ID를 비교하여 인증 확인
  7. 로그아웃 시, 서버에서 세션 정보 삭제

세션 기반 인증 사례

  • 유저 로그인 정보를 서버에서 직접 관리
    • → 클라이언트가 로그아웃하지 않아도 서버에서 강제 로그아웃 가능
    • → 보안 이슈 발생 시 강제 로그아웃 필요할 경우 유용
  • 자동 로그아웃 되는 사이트 예시: 은행, 공공기관 등

세션으로 로그인 기능 구현하기

  • express-session : 세션 기반 인증을 관리하는 미들웨어로, 서버에서 세션 데이터를 저장하고 관리

서버 생성 및 환경 설정

  • npm 패키지 설치
npm init -y
npm install express cors cookie-parser express-session
  • 서버 생성 및 CORS 설정
const express = require('express')
const cors = require('cors')

const app = express();

app.use(cors({
    origin: ["http://127.0.0.1:5500", "http://localhost:5500"],
    methods: ["OPTIONS", "POST", "GET", "DELETE"],
    credentials: true
}))
  • 더미데이터 생성
const users = [
    {
        user_id : 'test',
        user_password : '1234',
        user_name : '테스트 유저',
        user_info : '테스트 유저입니다.'
    }
]
  • 설치한 패키지를 변수에 할당
    • session을 생성할 때 아래와 같이 옵션을 설정해줘야 함
const cookieParser = require('cookie-parser')
const session = require('express-session')

app.use(cookieParser());
app.use(express.json());

app.use(session({
    // 클라이언트 측에 보낼 때 문자열을 암호화
    secret: 'session secrete',
    // 요청이 들어오면 다시 저장할건지 (값이 같아도 새로 저장할건지?)
    resave: false,
    // 저장할 내용이 없어도 저장할건지
    saveUninitialized : false,
    name : 'session_id'
}))
  • secret: 'session secrete' (필수)
    • 세션 ID를 암호화하는 키
    • 클라이언트에서 받은 세션 ID가 변조되지 않았음을 확인하는 용도
    • 보안상 중요하므로, 실제 서비스에서는 .env 파일에 보관하는 것이 좋음
  • resave: false (권장)
    • 세션 데이터가 변경되지 않아도 매 요청마다 다시 저장할지 여부
    • true로 설정하면 같은 데이터라도 요청마다 계속 저장됨 → 불필요한 저장이 발생
    • 로그인 등의 특정 요청이 있을 때만 세션을 갱신하려면 false가 좋음
  • saveUninitialized: false (권장)
    • 초기화되지 않은 세션을 저장할지 여부
    • 로그인 등 특정 작업이 발생할 때만 세션을 저장하려면 false가 좋음
    • true로 설정하면 사용자가 방문만 해도 세션이 저장됨 → 불필요한 저장 증가 가능성 있음
  • name: 'session_id' (선택)
    • 클라이언트의 쿠키에 저장될 세션 ID의 이름을 지정
    • 기본값은 'connect.sid'인데, 이를 그대로 사용하면 해커가 쉽게 세션 쿠키를 인식 가능
    • 보안상 기본값을 사용하지 않는 것이 권장됨
  • 서버 포트 설정
app.listen(3000, () => console.log('3000번 포트에서 서버 실행'))

서버 요청 처리

  • POST : 로그인 정보를 저장
app.post('/', (req, res) => {
		// 클라이언트가 보낸 userId와 userPassword를 req.body에서 구조 분해 할당으로 가져옴
    const {userId, userPassword} = req.body;
    // users 배열에서 **입력한 userId와 userPassword가 일치하는 사용자를 찾음**
    const userInfo = users.find(el => el.user_id ===userId && el.user_password === userPassword)
    console.log(req.body)

    if (!userInfo) {
		    // 401 Unauthorized(인증 실패) 상태 코드를 반환하여 로그인 실패를 클라이언트에게 알림
        res.status(401).send('로그인 실패')
    } else {
		    // 사용자의 user_id를 세션 객체(req.session)에 저장
        req.session.userId = userInfo.user_id;
        // 세션이 성공적으로 생성되었음을 클라이언트에게 응답
        res.send('세션 생성 완료!');
    }
})
  • GET : 사용자 정보를 호출
    • 클라이언트에서 로그인한 id 정보(req.session.userId)가 서버에 저장된 데이터의 id(el.user_id)와 일치하는 경우 userInfo 반환
app.get('/', (req, res) => {
    const userInfo = users.find(el => el.user_id === req.session.userId);
    return res.json(userInfo)
})
  • DELETE : 로그아웃
    • session에 존재하는 destroy메소드를 이용하여 세션 정보 삭제
    • 세션을 삭제해도 쿠키는 자동으로 삭제되지 않기 때문에, res.clearCookie()를 사용하여 수동으로 삭제
      • 쿠키가 남아 있으면, 다음 요청 시 삭제된 세션 ID가 전달될 수 있음 → 불필요한 요청 발생 가능
app.delete('/', (req, res) => {
    req.session.destroy()
    res.clearCookie('session_id')
    res.send('세션 삭제 완료')
})
💡

localhost에서 작업할 때 localhost와 172.x.x.x 같은 IP 주소가 다르면 인증 쿠키가 전송되지 않을 수 있으므로 localhost에서 확인

왜 localhost에서 확인해야 할까?

1️⃣ 쿠키는 SameSite 정책에 따라 전송이 제한될 수 있음

  • SameSite 정책은 CSRF(사이트 간 요청 위조) 공격을 방지하기 위해 다른 도메인 간의 쿠키 전송을 제한
  • localhost127.0.0.1은 같은 컴퓨터를 가리키지만, 도메인이 다르다고 간주
    2️⃣ localhost와 127.0.0.1이 다르게 인식될 수 있음
  • 브라우저는 localhost로컬 환경으로 특별 취급하지만, 127.0.0.1IP 주소로 인식
  • localhost에서 secure: false로 설정된 쿠키는 전송되지만, 127.0.0.1에서는 HTTPS가 아니라면 쿠키가 차단될 수 있음
    3️⃣ Secure 옵션이 활성화된 경우
  • res.cookie('session_id', 'value', { secure: true })
    • secure: true이면 HTTPS 환경에서만 쿠키가 전송됨
    • localhost에서는 HTTP에서도 작동하지만, 127.0.0.1, 172.x.x.x 같은 IP 주소에서는 HTTPS가 필요할 수도 있음
  • 따라서 개발 환경에서는 secure: false로 설정.
    4️⃣ 도메인이 다르면 쿠키가 다르게 저장됨
  • http://localhost:3000에서 로그인한 후,
    http://127.0.0.1:3000으로 접속하면 쿠키가 보이지 않을 수 있음
  • 이유: 쿠키는 도메인별로 저장되기 때문에, localhost에서
  • 개발할 때 항상 localhost에서 테스트
  • CORS 설정에서 origin: ['http://localhost:3000']을 명확하게 지정
profile
hello world!

0개의 댓글