واجهات GraphQL
المصادقة والتفويض
تأمين واجهة برمجة تطبيقات GraphQL
المصادقة (التحقق من هويتك) والتفويض (التحقق مما يمكنك فعله) أمران بالغا الأهمية لأي واجهة برمجة تطبيقات GraphQL إنتاجية. في هذا الدرس، سننفذ مصادقة آمنة باستخدام رموز JWT والتحكم في الوصول القائم على الأدوار.
المصادقة مقابل التفويض
- المصادقة: التحقق من هوية المستخدم (تسجيل الدخول)
- التفويض: التحقق من الإجراءات التي يُسمح للمستخدم بتنفيذها (الأذونات)
إعداد مصادقة JWT
قم بتثبيت الحزم المطلوبة:
npm install jsonwebtoken bcryptjs
npm install --save-dev @types/jsonwebtoken @types/bcryptjs
تسجيل المستخدم وتسجيل الدخول
أولاً، أنشئ محللات لتسجيل المستخدم وتسجيل الدخول:
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const typeDefs = `
type User {
id: Int!
email: String!
name: String!
role: String!
}
type AuthPayload {
token: String!
user: User!
}
type Mutation {
register(email: String!, password: String!, name: String!): AuthPayload!
login(email: String!, password: String!): AuthPayload!
}
`;
const resolvers = {
Mutation: {
register: async (parent, { email, password, name }, { prisma }) => {
// التحقق من وجود المستخدم بالفعل
const existingUser = await prisma.user.findUnique({
where: { email }
});
if (existingUser) {
throw new Error('User already exists');
}
// تجزئة كلمة المرور
const hashedPassword = await bcrypt.hash(password, 10);
// إنشاء المستخدم
const user = await prisma.user.create({
data: {
email,
password: hashedPassword,
name,
role: 'USER'
}
});
// إنشاء رمز JWT
const token = jwt.sign(
{ userId: user.id },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
return { token, user };
},
login: async (parent, { email, password }, { prisma }) => {
// البحث عن المستخدم
const user = await prisma.user.findUnique({
where: { email }
});
if (!user) {
throw new Error('Invalid credentials');
}
// التحقق من كلمة المرور
const valid = await bcrypt.compare(password, user.password);
if (!valid) {
throw new Error('Invalid credentials');
}
// إنشاء رمز JWT
const token = jwt.sign(
{ userId: user.id },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
return { token, user };
}
}
};
مهم: قم بتعيين
JWT_SECRET قوي في ملف .env الخاص بك. يُستخدم هذا السر لتوقيع رموز JWT والتحقق منها. لا تلتزم به أبدًا للتحكم في الإصدار.
المصادقة القائمة على السياق
استخراج المستخدم من رمز JWT في السياق:
const jwt = require('jsonwebtoken');
const getUserFromToken = async (token, prisma) => {
if (!token) return null;
try {
// إزالة بادئة "Bearer " إن وجدت
const cleanToken = token.replace('Bearer ', '');
// التحقق من الرمز وفك تشفيره
const decoded = jwt.verify(cleanToken, process.env.JWT_SECRET);
// جلب المستخدم من قاعدة البيانات
const user = await prisma.user.findUnique({
where: { id: decoded.userId }
});
return user;
} catch (error) {
console.error('Token verification failed:', error.message);
return null;
}
};
const server = new ApolloServer({
typeDefs,
resolvers,
context: async ({ req }) => {
const token = req.headers.authorization || '';
const user = await getUserFromToken(token, prisma);
return {
prisma,
user // متاح في جميع المحللات
};
}
});
حماية المحللات
تحقق مما إذا كان المستخدم مصادقًا عليه قبل السماح بالوصول إلى الموارد المحمية:
const resolvers = {
Query: {
me: (parent, args, { user }) => {
if (!user) {
throw new Error('Not authenticated');
}
return user;
},
myPosts: async (parent, args, { user, prisma }) => {
if (!user) {
throw new Error('Not authenticated');
}
return await prisma.post.findMany({
where: { authorId: user.id }
});
}
},
Mutation: {
createPost: async (parent, { title, content }, { user, prisma }) => {
if (!user) {
throw new Error('Not authenticated');
}
return await prisma.post.create({
data: {
title,
content,
authorId: user.id
}
});
},
deletePost: async (parent, { id }, { user, prisma }) => {
if (!user) {
throw new Error('Not authenticated');
}
// التحقق من ملكية المستخدم للمقالة
const post = await prisma.post.findUnique({ where: { id } });
if (!post) {
throw new Error('Post not found');
}
if (post.authorId !== user.id) {
throw new Error('Not authorized to delete this post');
}
return await prisma.post.delete({ where: { id } });
}
}
};
أنشئ دالة مساعدة قابلة لإعادة الاستخدام للتحقق من المصادقة وتجنب التكرار:
const requireAuth = (user) => { if (!user) throw new Error('Not authenticated'); };
التحكم في الوصول القائم على الأدوار (RBAC)
تنفيذ التفويض بناءً على أدوار المستخدم:
const resolvers = {
Query: {
users: async (parent, args, { user, prisma }) => {
// يمكن للمسؤولين فقط إدراج جميع المستخدمين
if (!user || user.role !== 'ADMIN') {
throw new Error('Not authorized');
}
return await prisma.user.findMany();
}
},
Mutation: {
deleteUser: async (parent, { id }, { user, prisma }) => {
// يمكن للمسؤولين فقط حذف المستخدمين
if (!user || user.role !== 'ADMIN') {
throw new Error('Not authorized');
}
return await prisma.user.delete({ where: { id } });
},
publishPost: async (parent, { id }, { user, prisma }) => {
if (!user) {
throw new Error('Not authenticated');
}
const post = await prisma.post.findUnique({ where: { id } });
// يمكن للمؤلفين نشر مقالاتهم الخاصة
// يمكن للمحررين والمسؤولين نشر أي مقالة
const canPublish =
post.authorId === user.id ||
user.role === 'EDITOR' ||
user.role === 'ADMIN';
if (!canPublish) {
throw new Error('Not authorized to publish this post');
}
return await prisma.post.update({
where: { id },
data: { published: true }
});
}
}
};
التفويض القائم على التوجيهات
استخدم توجيهات مخصصة لمنطق تفويض أنظف:
const { SchemaDirectiveVisitor } = require('apollo-server-express');
// تعريف التوجيه في المخطط
const typeDefs = `
directive @auth(requires: Role = USER) on FIELD_DEFINITION
enum Role {
USER
EDITOR
ADMIN
}
type Query {
me: User @auth
users: [User!]! @auth(requires: ADMIN)
myPosts: [Post!]! @auth
}
`;
// تنفيذ التوجيه
class AuthDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field) {
const { resolve = defaultFieldResolver } = field;
const { requires } = this.args;
field.resolve = async function(...args) {
const context = args[2];
const { user } = context;
if (!user) {
throw new Error('Not authenticated');
}
if (requires && user.role !== requires) {
throw new Error(`Requires ${requires} role`);
}
return resolve.apply(this, args);
};
}
}
const server = new ApolloServer({
typeDefs,
resolvers,
schemaDirectives: {
auth: AuthDirective
}
});
أفضل الممارسات الأمنية:
- قم دائمًا بتجزئة كلمات المرور (لا تخزن نصًا عاديًا أبدًا)
- استخدم أسرار JWT قوية (256 بت على الأقل)
- قم بتعيين أوقات انتهاء صلاحية رمزية معقولة
- نفذ رموز تحديث للجلسات طويلة الأمد
- تحقق من صحة جميع مدخلات المستخدم
- استخدم HTTPS في الإنتاج
أذونات على مستوى الحقل
إخفاء الحقول الحساسة بناءً على أذونات المستخدم:
const resolvers = {
User: {
email: (parent, args, { user }) => {
// إظهار البريد الإلكتروني للمستخدم نفسه أو المسؤولين فقط
if (user && (user.id === parent.id || user.role === 'ADMIN')) {
return parent.email;
}
return null;
},
password: () => {
// لا تكشف أبدًا عن تجزئة كلمة المرور
return null;
}
}
};
مثال مصادقة كامل
// استخدام العميل
const LOGIN_MUTATION = `
mutation Login($email: String!, $password: String!) {
login(email: $email, password: $password) {
token
user {
id
email
name
}
}
}
`;
// بعد تسجيل الدخول، احفظ الرمز في localStorage
localStorage.setItem('token', data.login.token);
// قم بتضمين الرمز في جميع الطلبات
const client = new ApolloClient({
uri: 'http://localhost:4000/graphql',
headers: {
authorization: `Bearer ${localStorage.getItem('token')}`
}
});
تمرين تطبيقي:
- نفذ طفرات تسجيل المستخدم وتسجيل الدخول باستخدام JWT
- أنشئ استعلامًا محميًا يتطلب المصادقة
- أضف حقل
roleللمستخدمين (USER، EDITOR، ADMIN) - نفذ التفويض لتقييد حذف المقالات للمؤلفين والمسؤولين فقط
- أنشئ إذن على مستوى الحقل يخفي رسائل البريد الإلكتروني للمستخدمين عن غير المسؤولين
- اختبر مصادقتك عن طريق إرسال استعلامات مع وبدون رموز صالحة