التحقق من الرموز
جزء من قسم المصادقة والأمان في رحلة Node.js على Coddy. الدرس 16 من 24.
يرسل عميل API الرمز المميّز في ترويسة مع كل طلب: Authorization: Bearer <token>. إن قراءته باستخدام decodeJwt ستؤدي إلى قبول أي رمز مميّز يكتبه أي شخص. تعيد jwtVerify(token, key) حساب التوقيع باستخدام سرك، وتُصدر خطأً عندما لا يتطابق التوقيع، أو عندما تنتهي صلاحية الرمز المميّز، أو عندما لا يكون الرمز المميّز JWT أصلًا. لا تثق بـ payload إلا بعد أن تُعيد الدالة نتيجة.
async function requireDevice(req, res, next) {
const token = (req.get('authorization') ?? '').replace(/^Bearer /, '');
try {
const { payload } = await jwtVerify(token, deviceKey);
req.device = payload.sub;
} catch {
return res.status(401).json({ error: 'Unknown device' });
}
next();
}التحدي
سهلاقبل الآن الرموز التي توقّعها. يرسل التطبيق رمزه في ترويسة مع كل طلب، ويتحقق الخادم من التوقيع قبل أن يثق بأي شيء داخل الرمز.
- في
server.js، استوردjwtVerifyمنjose. - اكتب وسيطًا برمجيًا
requireUser(req, res, next). يقرأ هذا الوسيط ترويسةAuthorization. تحتوي هذه الترويسة على كلمةBearer، ثم مسافة، ثم الرمز. استخرج الرمز منها. - تحقق من الرمز باستخدام
jwtVerifyوالمفتاح نفسه الذي تستخدمه للتوقيع. تُلقيjwtVerifyاستثناءً عندما لا يتطابق التوقيع، أو عندما تنتهي صلاحية الرمز، أو عندما لا يكون النص رمزًا أصلًا. في جميع هذه الحالات، وكذلك عندما لا توجد ترويسة، أرسل استجابة بالحالة 401 و{"error": "Log in first"}. - عندما يكون الرمز صالحًا، عيّن
req.userإلى كائن يحتوي علىid، مأخوذًا من الادعاءsubفي الرمز، وعلىusername، مأخوذًا من الادعاءusernameفيه. ثم استدعِnext(). - أضف
GET /meمعrequireUser. يعيد هذا المسارreq.user:{"id": "66f1c2a9e4b0a1b2c3d4e5f6", "username": "ada"}.
جرّب ذلك: اضغط «تشغيل». أرسل GET /me: الحالة 401 و{"error": "Log in first"}. أرسل POST /signup، ثم POST /token، مع {"username": "ada", "password": "correct horse"} في كليهما، وانسخ الرمز. في حقل الترويسات، أضف ترويسة باسم Authorization، واجعل قيمتها Bearer، ثم مسافة، ثم الرمز الخاص بك. أرسل GET /me: الحالة 200، مع المعرّف الخاص بك و"username": "ada". غيّر الآن حرفًا واحدًا في الجزء الأوسط من الرمز، الواقع بين النقطتين، ثم أرسل GET /me مرة أخرى: الحالة 401، لأن التوقيع لم يعد متطابقًا.
جرّب بنفسك
import express from 'express';
import { SignJWT, decodeJwt } from 'jose';
import { users } from './db.js';
import { hashPassword, verifyPassword } from './passwords.js';
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
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 });
});
app.post('/token', 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 token = await new SignJWT({ username: user.username })
.setProtectedHeader({ alg: 'HS256' })
.setSubject(user._id.toHexString())
.setIssuedAt()
.setExpirationTime('1h')
.sign(secret);
res.json({ token });
});
// يعرض محتوى الرمز: GET /inspect?token=...
app.get('/inspect', (req, res) => {
try {
res.json(decodeJwt(String(req.query.token ?? '')));
} catch {
res.status(400).json({ error: 'Not a JWT' });
}
});
// صفحة المسؤول. لا يمكن الوثوق بأي ادعاء قبل التحقق من توقيعه.
app.get('/admin', (req, res) => {
res.status(403).json({ error: 'Admins only' });
});
app.listen(3000, () => {
console.log('Vault is running on port 3000');
});
يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.
جميع دروس المصادقة والأمان
تدرّب بنفسك: Playground لـ Express