הרשמה והתחברות (bcrypt ו-JWT)
כמעט כל אפליקציה צריכה משתמשים: הרשמה, התחברות, ונתיבים שרק משתמש מחובר יכול לגשת אליהם. בשיעור הזה נבנה את זה נכון, עם שני כלים: bcrypt לשמירת סיסמאות, ו-JWT לזיהוי המשתמש בבקשות הבאות.
לעולם לא לשמור סיסמה כמו שהיא
אם המסד דולף, כל הסיסמאות חשופות, ורוב האנשים משתמשים באותה סיסמה בכמה אתרים. לכן שומרים רק hash: תוצאה של פונקציה חד-כיוונית, שאי אפשר לשחזר ממנה את הסיסמה. bcrypt גם מוסיף "מלח" אקראי לכל סיסמה, ומכוון להיות איטי, כדי שניחוש בכוח יהיה יקר.
npm install bcryptjs jsonwebtoken
import bcrypt from 'bcryptjs';
const hash = await bcrypt.hash('my-secret-password', 12); // 12 = כמה עבודה (עלות)
// מחרוזת שמתחילה ב-$2, עם המלח והעלות בתוכה - זה מה שנשמר במסד
const isCorrect = await bcrypt.compare('my-secret-password', hash); // true
הרשמה
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
// src/auth/auth.routes.js
import { Router } from 'express';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import { pool } from '../db.js';
import { AppError } from '../errors.js';
export const authRouter = Router();
authRouter.post('/register', async (req, res) => {
const email = String(req.body.email ?? '').trim().toLowerCase();
const password = String(req.body.password ?? '');
if (!email.includes('@')) throw new AppError('אימייל לא תקין', 400);
if (password.length < 10) throw new AppError('סיסמה חייבת לפחות 10 תווים', 400);
const passwordHash = await bcrypt.hash(password, 12);
try {
const [result] = await pool.query(
'INSERT INTO users (email, password_hash) VALUES (?, ?)', [email, passwordHash]
);
res.status(201).json({ id: result.insertId, email });
} catch (error) {
if (error.code === 'ER_DUP_ENTRY') throw new AppError('האימייל כבר רשום', 409);
throw error;
}
});
מה זה JWT
אחרי התחברות, השרת צריך לזהות את המשתמש בכל בקשה בלי לבקש שוב סיסמה. JWT (JSON Web Token) הוא מחרוזת עם שלושה חלקים: מידע (למשל מזהה המשתמש), תוקף, וחתימה שרק השרת יודע לייצר עם מפתח סודי. אם מישהו משנה אפילו תו אחד, החתימה לא תתאים.
שימו לב: התוכן של JWT קריא לכל אחד (הוא לא מוצפן, רק חתום). לא שמים בו סיסמאות או מידע רגיש.
התחברות
# .env - מחרוזת אקראית ארוכה. לייצר, למשל, עם:
# node -e "console.log(crypto.randomBytes(48).toString('hex'))"
JWT_SECRET=...
authRouter.post('/login', async (req, res) => {
const email = String(req.body.email ?? '').trim().toLowerCase();
const [rows] = await pool.query('SELECT * FROM users WHERE email = ?', [email]);
const user = rows[0];
// אותה הודעה לשני המקרים: לא מגלים אם האימייל קיים במערכת
const valid = user && await bcrypt.compare(String(req.body.password ?? ''), user.password_hash);
if (!valid) throw new AppError('אימייל או סיסמה שגויים', 401);
const token = jwt.sign({ sub: String(user.id) }, process.env.JWT_SECRET, { expiresIn: '1h' });
res.json({ token });
});
Middleware שמגן על נתיבים
// src/auth/require-auth.js
import jwt from 'jsonwebtoken';
import { AppError } from '../errors.js';
export function requireAuth(req, res, next) {
const header = req.get('Authorization') ?? '';
const token = header.startsWith('Bearer ') ? header.slice(7) : null;
if (!token) throw new AppError('נדרשת התחברות', 401);
try {
const payload = jwt.verify(token, process.env.JWT_SECRET);
req.userId = Number(payload.sub);
next();
} catch (error) {
throw new AppError('ההתחברות פגה או לא תקינה', 401);
}
}
// src/app.js
app.use('/api/auth', authRouter);
app.use('/api/todos', requireAuth, todosRouter); // כל נתיבי המשימות דורשים התחברות
// בתוך הנתיבים: רק המשימות של המשתמש המחובר
todosRouter.get('/', async (req, res) => {
const [rows] = await pool.query('SELECT * FROM todos WHERE user_id = ?', [req.userId]);
res.json(rows);
});
כדי שלכל משתמש יהיו משימות משלו, מוסיפים לטבלה עמודה שמקשרת אותה למשתמש:
ALTER TABLE todos ADD COLUMN user_id INT NOT NULL,
ADD FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
הלקוח שולח את הטוקן בכל בקשה: Authorization: Bearer eyJhbGciOi....
איפה הדפדפן שומר את הטוקן
| אפשרות | יתרון | סיכון |
|---|---|---|
Cookie עם httpOnly | JavaScript לא יכול לקרוא אותו, ולכן XSS לא גונב אותו | צריך הגנת CSRF (sameSite) |
localStorage | פשוט לשימוש מ-React | כל סקריפט בדף (גם זדוני) יכול לקרוא אותו |
לאפליקציית ווב, cookie עם httpOnly, secure ו-sameSite: 'lax' הוא הבחירה הבטוחה יותר. Authorization header מתאים לאפליקציות מובייל ולקוחות שאינם דפדפן.
רשימת בדיקה
- סיסמאות: רק hash (bcrypt או argon2), אף פעם לא טקסט רגיל, ואף פעם לא בלוג.
JWT_SECRETארוך ואקראי, ב-.envולא בקוד.- תוקף קצר לטוקן, והודעת שגיאה זהה לאימייל שגוי ולסיסמה שגויה.
- HTTPS בפרודקשן, תמיד.
- הגבלת ניסיונות התחברות, כדי שאי אפשר יהיה לנחש סיסמאות. על זה בשיעור הבא.
בדקו את עצמכם
נסו לענות לבד לפני שאתם פותחים את התשובה.
-
איך שומרים סיסמאות במסד?
הצגת התשובה
תשובה א. hash חד-כיווני עם מלח: אי אפשר לשחזר ממנו את הסיסמה.
-
האם התוכן של JWT מוצפן?
הצגת התשובה
תשובה ד. לכן לא שמים ב-JWT מידע רגיש.
-
למה להחזיר אותה הודעה לאימייל שגוי ולסיסמה שגויה?
הצגת התשובה
תשובה ב. הודעות שונות מאפשרות לאסוף רשימת משתמשים.