로그인 세션
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 });챌린지
쉬움이 단계에서는 쿠키로 기억하기에서 사용하던 프로젝트 파일( server.js 포함)이 아니라, 미리 준비된 비공개 노트 API 프로젝트를 엽니다. 이전 작업은 해당 레슨에 그대로 남아 있습니다. server.js를 편집하기 전에 제공된 파일을 읽어 보세요. 이 사본에는 비공개 노트의 가입 및 로그인 경로가 들어 있습니다.
로그인은 되지만, 다음 요청을 보낼 때는 누가 로그인했는지 이미 잊어버립니다. 세션을 사용해 서버가 기억하도록 만드세요. 세션은 서버에 저장되고 쿠키로 브라우저에 전송되는 임의의 ID입니다.
server.js에서node:crypto의randomBytes를 가져오세요.notes와users옆에./db.js의sessions도 가져오세요. 잠겨 있는db.js에는 이미sessions컬렉션이 있습니다.POST /login에서 비밀번호를 확인한 다음 세션 ID를 만드세요. 임의의 바이트 24개를 base64url 텍스트로 작성합니다. 즉,-와_를 포함한 영문자와 숫자 32개이며,oqjRxvxmXl_nRwAwOLxgZyI2sYq9hZtN와 같은 형태입니다. 로그인할 때마다 새 ID를 만드세요.- 세션을
sessions에 다음 두 필드가 있는 문서로 저장하세요._id: 세션 ID.userId: 로그인한 사용자의_id.
httpOnly옵션을 사용해 ID를sid라는 쿠키로 브라우저에 보내세요. 그러면 페이지의 스크립트가 이 쿠키를 읽을 수 없습니다. 로그인 응답은 계속{"username": "ada"}로 반환됩니다.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 플레이그라운드