Menu
CoddyTech

Sesi Login

Termasuk bagian Autentikasi dan Keamanan dalam perjalanan Node.js di Coddy. Pelajaran 10 dari 24.

Login memberikan jawaban 200, lalu permintaan berikutnya tidak ingat siapa yang login. Sesi mengingatnya. Saat login, server membuat id yang tidak dapat ditebak siapa pun, menyimpan pengguna pemiliknya, lalu mengirimkannya sebagai cookie. Setiap permintaan berikutnya membawa id itu kembali, lalu server mencarinya.

Id harus acak. Cookie yang menyimpan nama pengguna memungkinkan siapa pun mengetik Cookie: sid=ada dan menjadi Ada. randomBytes(24) memberikan 24 byte acak, 192 bit, yang tidak dapat ditebak siapa pun. toString('base64url') menuliskannya sebagai 32 huruf, angka, -, dan _, karakter yang aman digunakan dalam cookie. Atur cookie httpOnly agar skrip di halaman tidak dapat membacanya.

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

// pada permintaan berikutnya
const cart = await carts.findOne({ _id: parseCookies(req.get('cookie')).cart });
challenge icon

Tantangan

Mudah

Langkah ini membuka proyek API catatan pribadi yang disiapkan untuk kursus ini, bukan file proyekmu dari Mengingat dengan Cookies (termasuk server.js). Pekerjaanmu sebelumnya tetap berada di pelajarannya sendiri. Baca file yang disediakan sebelum mengedit server.js. Salinan ini berisi rute pendaftaran dan login untuk catatan pribadi.

Login berhasil, tetapi permintaan berikutnya sudah lupa siapa yang login. Buat server mengingatnya dengan sesi: id acak yang disimpan di server dan dikirim ke browser sebagai cookie.

  1. Di server.js, impor randomBytes dari node:crypto. Impor sessions dari ./db.js bersama notes dan users. db.js yang terkunci sudah memiliki koleksi sessions.
  2. Di POST /login, setelah kata sandi diperiksa, buat id sesi: 24 byte acak yang ditulis sebagai teks base64url. Panjangnya 32 karakter, terdiri dari huruf, angka, - dan _, seperti oqjRxvxmXl_nRwAwOLxgZyI2sYq9hZtN. Buat id baru setiap kali login.
  3. Simpan sesi di sessions sebagai dokumen dengan dua kolom:
    • _id: id sesi.
    • userId: _id pengguna yang login.
  4. Kirim id ke browser sebagai cookie bernama sid, dengan opsi httpOnly, agar skrip di halaman tidak dapat membacanya. Login tetap memberikan respons {"username": "ada"}.
  5. Tambahkan GET /me. Rute ini membaca cookie sid, mencari sesi dengan _id tersebut, lalu mencari pengguna sesi itu. Rute ini memberikan respons {"username": "ada"}. Jika tidak ada cookie sid, atau tidak ada sesi dengan id tersebut, rute ini memberikan status 401 dan {"error": "Log in first"}. Siapa pun yang mengetik cookie secara manual, seperti sid=ada, harus mendapat status 401 itu.

Coba: Tekan Run. Kirim POST /signup dengan {"username": "ada", "password": "correct horse"}. Kirim GET /me: status 401 dan {"error": "Log in first"}. Kirim POST /login dengan isi yang sama seperti saat mendaftar: status 200. Tab Headers pada respons menampilkan Set-Cookie: sid=...; Path=/; HttpOnly, dengan id acakmu sebagai pengganti titik-titik. Kirim GET /me lagi. Browser mengirimkan cookie itu kembali, jadi kamu mendapat {"username": "ada"}. Buka Data: sessions berisi satu dokumen, dengan id-mu sebagai _id dan _id milik ada sebagai userId.

Coba sendiri

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

// Menghitung berapa kali browser ini mengakses 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 });
});

// Catatan. Untuk saat ini, siapa pun bisa membaca dan menulisnya.
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');
});

Semua pelajaran di Autentikasi dan Keamanan

Latihan sendiri: Playground Express