Menu
CoddyTech

로그아웃

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

쿠키를 지우는 것은 브라우저에 ID를 잊어 달라고 요청할 뿐입니다. 다른 기기에서 복사했거나 쿠키를 훔친 사람이 여전히 유효한 키를 가지고 있습니다. 로그아웃하려면 서버에서 세션을 삭제해야 합니다.

res.clearCookie(name)은 브라우저에 쿠키를 삭제하라고 알립니다. 상태 코드 204 No Content는 요청이 성공했고 반환할 내용이 없다는 뜻입니다: res.status(204).end().

다음은 관련 경로로, 모든 기기에서 사용자를 한 번에 로그아웃합니다:

app.post('/logout-everywhere', requireUser, async (req, res) => {
    await sessions.deleteMany({ userId: new ObjectId(req.user.id) });
    res.clearCookie('sid');
    res.status(204).end();
});
challenge icon

챌린지

쉬움
  • POST /logout은 sid 쿠키의 세션을 삭제하고, 쿠키를 지운 뒤 204로 응답합니다.
  • 세션이 없어도 204로 응답합니다.

로그인한 다음 POST /logout을 보내고, 이어서 GET /me를 보내세요.

직접 해보기

import express from 'express';
import { randomBytes } from 'node:crypto';
import { parseCookies } from './cookies.js';
import { notes, sessions, 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' });
    }
    const sid = randomBytes(24).toString('base64url');
    await sessions.insertOne({ _id: sid, userId: user._id, createdAt: new Date() });
    res.cookie('sid', sid, { httpOnly: true });
    res.json({ username: user.username });
});

// 세션 쿠키에 해당하는 사용자를 찾거나 401로 응답합니다.
async function requireUser(req, res, next) {
    const { sid } = parseCookies(req.get('cookie'));
    const session = sid && (await sessions.findOne({ _id: sid }));
    const user = session && (await users.findOne({ _id: session.userId }));
    if (!user) {
        return res.status(401).json({ error: 'Log in first' });
    }
    req.user = { id: user._id.toHexString(), username: user.username };
    next();
}

app.get('/me', requireUser, (req, res) => {
    res.json({ username: req.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', requireUser, async (req, res) => {
    res.json(await notes.find({ owner: req.user.id }).toArray());
});

app.post('/notes', requireUser, async (req, res) => {
    const note = { owner: req.user.id, 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');
});

인증과 보안의 모든 레슨