Girişlerde Hız Sınırlaması
Coddy'nin Node.js Journey'sinin Kimlik Doğrulama ve Güvenlik bölümünün bir parçası. Ders 23 / 24.
Bir betik, tek bir hesaba karşı dakikada binlerce parola deneyebilir. Her kullanıcı adı için başarısız oturum açma denemelerini sayın ve bir dakika içinde beş denemeye ulaşıldıktan sonra dakika bitene kadar başka denemeleri kontrol etmeyi reddedin.
429 Too Many Requests durum kodu bunun nedenini söyler, Retry-After başlığı ise kaç saniye beklenmesi gerektiğini belirtir. Yalnızca başarısız denemeleri sayın: doğru parolayı yazan bir kişi kendisini asla hesabına erişemez duruma düşürmemelidir. Bir Map, değerleri anahtarlara göre saklar: map.get(key) bir değeri okur, map.set(key, value) ise bir değer yazar. Bellekte tutulur; bu, tek bir sunucu için yeterlidir ve sunucu yeniden başlatıldığında içindeki veriler silinir. Bu örnek dakikada bir parola sıfırlama e-postasına izin verir, bu yüzden yalnızca bir zaman bilgisi tutar. Oturum açma sisteminiz başarısız denemeleri sayar; bu nedenle kaydı hem bir sayı hem de bir zaman bilgisi tutar.
const lastSent = new Map();
app.post('/reset', (req, res) => {
const email = String(req.body.email ?? '').toLowerCase();
const wait = (lastSent.get(email) ?? 0) + 60_000 - Date.now();
if (wait > 0) {
res.set('Retry-After', String(Math.ceil(wait / 1000)));
return res.status(429).json({ error: 'One reset email a minute' });
}
lastSent.set(email, Date.now());
// ... e-postayı gönder
});Bu öğrenme amaçlı sürüm, tek bir sunucu sürecinde tamamlanan başarısız denemeleri sayar. Parola kontrolleri beklemedeyken eşzamanlı istekler sınırın aşılmasına neden olabilir; üretim ortamında kullanılacak bir sınırlayıcı, parolayı kontrol etmeden önce bir denemeyi saymalı ve tüm sunucuların paylaştığı bir depoda sayaçları tutmalıdır.
Görev
KolayBu adım, Security Headers bölümündeki proje dosyaların yerine, kurs için hazırlanmış özel notlar API projesini açar (server.js dâhil). Önceki çalışmaların kendi dersinde kalır. server.js dosyasını düzenlemeden önce verilen dosyaları oku. Bu kopyada özel notların sahip filtreleri, rol kontrolleri ve güvenlik başlıkları bulunur.
Bir betik, tek bir hesaba karşı dakikada binlerce parola deneyebilir. Bunu yavaşlat: bir kullanıcı adı için bir dakika içinde 5 yanlış parola girildikten sonra, bir dakika dolana kadar o ad için parola denetimini durdur.
server.jsiçinde,POST /loginsatırının üstünde, her kullanıcı adına ait başarısız girişleri birMapiçinde kaydet. Her ad için kaç başarısız giriş olduğunu ve ilk başarısız girişin ne zaman gerçekleştiğini sakla.POST /loginiçinde, parolayı denetlemeden önce kullanıcı adına ait kaydı bul. İlk başarısız girişin üzerinden en az bir dakika geçmişse bu kayıt yok sayılır. Ad için bir dakika içinde 5 başarısız giriş varsa, 429 durum koduyla ve{"error": "Too many failed logins. Try again later."}yanıtını ver. Parola doğru olsa bile bunu yap. 429 Too Many Requests, “yavaşla” anlamına gelir.- Bu 429 yanıtında, ilk başarısız girişten itibaren bir dakika dolana kadar kalan tam saniye sayısını
Retry-Afterbaşlığına ayarla; örneğinRetry-After: 42. - Parola yanlış olduğunda, o kullanıcı adının başarısız giriş sayısını 1 artır ve ardından her zamanki 401 yanıtını ver. İlk başarısız girişte veya eski bir kayıttan sonraki ilk başarısız girişte, geçerli zamanla birlikte 1'den başlayan yeni bir sayım başlat.
- Başarılı girişleri asla sayma. Bir kullanıcı adına ait başarısız girişler başka bir kullanıcı adını asla kısıtlamaz.
Denemek için: “Run” düğmesine bas.
POST /signupisteğini{"username": "ada", "password": "correct horse"}ile gönder.POST /loginisteğini{"username": "ada", "password": "guess"}ile beş kez gönder: her seferinde durum kodu 401 olur.- Altıncı kez gönder: durum kodu 429 olur. Headers sekmesinde, en fazla 60 olan saniye sayısıyla birlikte
Retry-Aftergörünür. - Doğru parolayı gönder:
{"username": "ada", "password": "correct horse"}; yanıt yine 429 olur. POST /signupisteğini, ardındanPOST /loginisteğini{"username": "bob", "password": "correct horse"}ile gönder: yalnızca ada kısıtlandığı için durum kodu 200 olur. Bir dakika sonra ada tekrar giriş yapabilir.
Kendin dene
import express from 'express';
import { ObjectId } from 'mongodb';
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.disable('x-powered-by');
app.use(express.json());
// Tarayıcıya her yanıtta dikkatli olmasını söyleyen başlıklar.
app.use((req, res, next) => {
res.set({
'X-Content-Type-Options': 'nosniff',
'Referrer-Policy': 'no-referrer',
'Content-Security-Policy': "default-src 'self'",
});
next();
});
// Tek yönetici hesabı. Parolası ortamdan alınır.
await users.insertOne({ username: 'admin', role: 'admin', passwordHash: await hashPassword(process.env.ADMIN_PASSWORD) });
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, role: 'member', 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,
sameSite: 'lax',
secure: true,
maxAge: 24 * 60 * 60 * 1000,
});
res.json({ username: user.username });
});
// Oturum çerezine ait kullanıcıyı bulur; bulamazsa 401 yanıtını verir.
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, role: user.role };
next();
}
// Oturum açmış kullanıcı bu role sahip değilse 403 yanıtını verir.
function requireRole(role) {
return (req, res, next) => {
if (req.user.role !== role) {
return res.status(403).json({ error: 'Not allowed' });
}
next();
};
}
app.get('/me', requireUser, (req, res) => {
res.json({ username: req.user.username });
});
app.post('/logout', async (req, res) => {
const { sid } = parseCookies(req.get('cookie'));
if (sid) {
await sessions.deleteOne({ _id: sid });
}
res.clearCookie('sid');
res.status(204).end();
});
// Notlarınızın sayfa olarak gösterimi.
app.get('/', requireUser, async (req, res) => {
const list = await notes.find({ owner: req.user.id }).toArray();
const items = list.map((note) => `<li>${note.text}</li>`).join('');
res.send(`<!doctype html><title>Vault</title><h1>${req.user.username}'s notes</h1><ul>${items}</ul>`);
});
// Notlar: Her kullanıcı yalnızca kendi notlarını okur ve yazar.
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.get('/notes/:id', requireUser, async (req, res) => {
if (!ObjectId.isValid(req.params.id)) {
return res.status(404).json({ error: 'Note not found' });
}
const note = await notes.findOne({ _id: new ObjectId(req.params.id), owner: req.user.id });
if (!note) {
return res.status(404).json({ error: 'Note not found' });
}
res.json(note);
});
app.delete('/notes/:id', requireUser, async (req, res) => {
if (!ObjectId.isValid(req.params.id)) {
return res.status(404).json({ error: 'Note not found' });
}
const { deletedCount } = await notes.deleteOne({ _id: new ObjectId(req.params.id), owner: req.user.id });
if (!deletedCount) {
return res.status(404).json({ error: 'Note not found' });
}
res.status(204).end();
});
// Yöneticiler için: Kaç hesap olduğu.
app.get('/admin', requireUser, requireRole('admin'), async (req, res) => {
res.json({ users: await users.countDocuments() });
});
app.listen(3000, () => {
console.log('Vault is running on port 3000');
});
Kimlik Doğrulama ve Güvenlik bölümündeki tüm dersler
1Şifreler
Baytlar ve Onaltılık MetinParola Anahtarı TüretmeYeni Bir TuzParolaları Asla SaklamayınAnahtar Baytlarını KarşılaştırmaParolayı DoğrulamaKayıt Kuralları4Sahiplik ve Güvenlik
Yalnızca NotlarınızYönetici RolleriKendi başına pratik yap: Express playground