Menu
CoddyTech

로그인 세션

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

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

ID는 무작위여야 합니다. 사용자 이름이 들어 있는 쿠키를 사용하면 누구나 Cookie: sid=ada라고 입력해 Ada인 것처럼 가장할 수 있습니다. randomBytes(24)는 무작위 비트 192개를 제공합니다. 페이지의 스크립트가 쿠키를 읽을 수 없도록 쿠키에 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

챌린지

쉬움
  • POST /login은 sessions에 { _id: sid, userId }를 저장하고, randomBytes(24).toString('base64url')을 사용해 sid 쿠키를 httpOnly로 설정합니다.
  • GET /me는 해당 세션의 경우 { username }을 반환하고, 그렇지 않으면 401과 { "error": "Log in first" }를 반환합니다.

가입하고, 로그인한 다음, GET /me를 보내고 Data를 확인하세요.

직접 해보기

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');
});

인증과 보안의 모든 레슨