Menu
CoddyTech

로그인 세션

Coddy Node.js 여정의 인증과 보안 섹션에 포함된 레슨. 24개 중 10번째.

로그인 요청에는 200이 응답되지만, 다음 요청에서는 누가 로그인했는지 잊어버립니다. 세션이 이를 기억합니다. 로그인할 때 서버는 누구도 추측할 수 없는 ID를 만들고, 이 ID가 어느 사용자에 속하는지 저장한 다음 쿠키로 보냅니다. 이후 각 요청에서 ID가 다시 전달되면 서버는 이를 조회합니다.

ID는 무작위여야 합니다. 사용자 이름이 들어 있는 쿠키가 있으면 누구나 Cookie: sid=ada라고 입력해 Ada인 척할 수 있습니다. randomBytes(24)는 무작위 바이트 24개, 즉 192비트를 생성하므로 누구도 추측할 수 없습니다. toString('base64url')은 이를 문자 32개와 숫자, - 및 _로 나타냅니다. 이 문자들은 쿠키에 안전하게 사용할 수 있습니다. 페이지의 스크립트가 쿠키를 읽을 수 없도록 쿠키에 httpOnly를 설정하세요.

const cartId = randomBytes(24).toString('base64url');
await carts.insertOne({ _id: cartId, items: [] });
res.cookie('cart', cartId, { httpOnly: true });

// 이후 요청에서
const cart = await carts.findOne({ _id: parseCookies(req.get('cookie')).cart });
challenge icon

챌린지

쉬움

이 단계에서는 쿠키로 기억하기에서 사용하던 프로젝트 파일( server.js 포함)이 아니라, 미리 준비된 비공개 노트 API 프로젝트를 엽니다. 이전 작업은 해당 레슨에 그대로 남아 있습니다. server.js를 편집하기 전에 제공된 파일을 읽어 보세요. 이 사본에는 비공개 노트의 가입 및 로그인 경로가 들어 있습니다.

로그인은 되지만, 다음 요청을 보낼 때는 누가 로그인했는지 이미 잊어버립니다. 세션을 사용해 서버가 기억하도록 만드세요. 세션은 서버에 저장되고 쿠키로 브라우저에 전송되는 임의의 ID입니다.

  1. server.js에서 node:crypto의 randomBytes를 가져오세요. notes와 users 옆에 ./db.js의 sessions도 가져오세요. 잠겨 있는 db.js에는 이미 sessions 컬렉션이 있습니다.
  2. POST /login에서 비밀번호를 확인한 다음 세션 ID를 만드세요. 임의의 바이트 24개를 base64url 텍스트로 작성합니다. 즉, -와 _를 포함한 영문자와 숫자 32개이며, oqjRxvxmXl_nRwAwOLxgZyI2sYq9hZtN와 같은 형태입니다. 로그인할 때마다 새 ID를 만드세요.
  3. 세션을 sessions에 다음 두 필드가 있는 문서로 저장하세요.
    • _id: 세션 ID.
    • userId: 로그인한 사용자의 _id.
  4. httpOnly 옵션을 사용해 ID를 sid라는 쿠키로 브라우저에 보내세요. 그러면 페이지의 스크립트가 이 쿠키를 읽을 수 없습니다. 로그인 응답은 계속 {"username": "ada"}로 반환됩니다.
  5. GET /me를 추가하세요. 이 경로는 sid 쿠키를 읽고, 해당 _id로 세션을 찾은 다음 세션에 연결된 사용자를 찾습니다. 응답은 {"username": "ada"}입니다. sid 쿠키가 없거나 해당 ID의 세션이 없으면 상태 코드 401과 {"error": "Log in first"}를 반환합니다. sid=ada처럼 쿠키를 직접 입력한 경우에도 401을 받아야 합니다.

직접 해 보세요: ‘플레이’를 누르세요. POST /signup에 {"username": "ada", "password": "correct horse"}를 보내세요. GET /me를 보내면 상태 코드 401과 {"error": "Log in first"}가 반환됩니다. 가입할 때와 같은 본문으로 POST /login을 보내세요. 상태 코드는 200입니다. 응답의 Headers 탭에는 점 대신 임의의 ID가 들어간 Set-Cookie: sid=...; Path=/; HttpOnly가 표시됩니다. 다시 GET /me를 보내세요. 브라우저가 쿠키를 돌려보내므로 {"username": "ada"}를 받습니다. Data를 열어 보세요. sessions에는 문서 하나가 있으며, 해당 문서의 _id는 여러분의 ID이고 userId는 ada의 _id입니다.

직접 해보기

import express from 'express';
import { parseCookies } from './cookies.js';
import { notes, users } from './db.js';
import { hashPassword, verifyPassword } from './passwords.js';

const app = express();
app.use(express.json());

app.post('/signup', async (req, res) => {
    const username = String(req.body.username ?? '').trim().toLowerCase();
    const password = String(req.body.password ?? '');
    if (!username || password.length < 8) {
        return res.status(400).json({ error: 'Pick a username and a password of at least 8 characters' });
    }
    if (await users.findOne({ username })) {
        return res.status(409).json({ error: 'That username is taken' });
    }
    await users.insertOne({ username, passwordHash: await hashPassword(password) });
    res.status(201).json({ username });
});

app.post('/login', async (req, res) => {
    const username = String(req.body.username ?? '').trim().toLowerCase();
    const user = await users.findOne({ username });
    if (!user || !(await verifyPassword(String(req.body.password ?? ''), user.passwordHash))) {
        return res.status(401).json({ error: 'Wrong username or password' });
    }
    res.json({ username: user.username });
});

// 이 브라우저가 GET /visit을 호출한 횟수를 셉니다.
app.get('/visit', (req, res) => {
    const cookies = parseCookies(req.get('cookie'));
    const visits = Number(cookies.visits ?? 0) + 1;
    res.cookie('visits', String(visits));
    res.json({ visits });
});

// 메모. 현재는 누구나 읽고 쓸 수 있습니다.
app.get('/notes', async (req, res) => {
    res.json(await notes.find().toArray());
});

app.post('/notes', async (req, res) => {
    const note = { text: String(req.body.text ?? '') };
    await notes.insertOne(note);
    res.status(201).json(note);
});

app.listen(3000, () => {
    console.log('Vault is running on port 3000');
});

인증과 보안의 모든 레슨

직접 연습해 보세요: Express 플레이그라운드