التعامل مع الـ Bearer Tokens وتجديد الصلاحيات آلياً
وش سالفة الـ Bearer Tokens؟
يا هلا والله بالشباب! اليوم بنتكلم عن شيء أساسي في عالم الـ APIs وتأمينها: الـ Bearer Tokens. باختصار، الـ Bearer Token هو مفتاح مؤقت تعطيك إياه السيرفر بعد ما تسجل دخولك أو تسوي توثيق، وظيفته يثبت هويتك لكل طلب ترسله للسيرفر بعد كذا. يعني، بدل ما ترسل اسم المستخدم وكلمة المرور في كل طلب (وهذا شيء خطير ومو كويس أبد)، ترسل هذا التوكن.
لما ترسل التوكن، يكون في الـ Authorization header، وغالباً شكله يكون كذا: Authorization: Bearer YOUR_ACTUAL_TOKEN_HERE. السيرفر يشوف هذا التوكن، يتأكد إنه صالح، ويسمح لك بالوصول للموارد اللي عندك صلاحية عليها.
ليه نحتاج Refresh Token؟
طيب، إذا التوكن يشتغل تمام، ليش نوجع راسنا بشيء اسمه Refresh Token؟ الجواب بسيط: الأمان. الـ Bearer Tokens (أو الـ Access Tokens زي ما يسمونها أحياناً) صلاحيتها قصيرة، ممكن تكون ساعة، نص ساعة، أو حتى أقل. هذا الشيء كويس عشان لو أحد قدر يسرق التوكن حقك، ما يقدر يستخدمه لفترة طويلة.
لكن المشكلة هنا، لو صلاحية التوكن خلصت، يروح المستخدم يسوي لوق أوت بالغصب، أو لازم يسجل دخول من جديد، وهذا شيء يطفش المستخدمين. هنا يجي دور الـ Refresh Token.
ملاحظة: الـ
Refresh Tokenصلاحيته أطول بكثير من الـAccess Token(ممكن أيام أو أسابيع أو حتى شهور)، ويستخدم بس عشان تجيبAccess Tokenجديد لما القديم تنتهي صلاحيته. هذا يعني إنك ما ترسله مع كل طلب، ترسله بس لـ Endpoint مخصص لتجديد التوكن.
كيف تشتغل العملية؟
السيناريو العام يكون كذا:
- المستخدم يسجل دخول (يرسل اسم المستخدم وكلمة المرور).
- السيرفر يتأكد من البيانات ويرجع له
Access TokenوRefresh Token. - الـ Client (تطبيقك) يخزن التوكنين هذي (غالباً الـ
Access Tokenفي الذاكرة أوlocalStorage، والـRefresh TokenفيlocalStorageأوhttpOnly cookiesلأمان أعلى). - مع كل طلب للسيرفر، الـ Client يرسل الـ
Access Tokenفي الـAuthorizationheader. - إذا السيرفر رجّع خطأ
401 Unauthorized(يعني الـAccess Tokenانتهت صلاحيته أو غير صالح)، الـ Client يوقف الطلب الأصلي. - يرسل الـ Client طلب جديد لـ Endpoint تجديد الصلاحية (مثلاً
/refresh-token)، ويرسل معاه الـRefresh Tokenاللي خزّنه. - إذا الـ
Refresh Tokenكان صالح، السيرفر يرجعAccess Tokenجديد (وممكنRefresh Tokenجديد بعد). - الـ Client يخزّن الـ
Access Tokenالجديد، وبعدين يعيد إرسال الطلب الأصلي اللي كان وقفه في الخطوة 5، بس هالمرة بالـAccess Tokenالجديد.
يلا نكتب كود! (مثال JavaScript)
خلنا نشوف كيف ممكن نطبق هذا السيناريو في JavaScript باستخدام fetch API. بنسوي دالة ترسل الطلبات، وإذا جاها 401، تحاول تجدد التوكن وتعيد الطلب.
const API_BASE_URL = 'https://api.yourdomain.com';
let accessToken = localStorage.getItem('accessToken');
let refreshToken = localStorage.getItem('refreshToken');
async function refreshAccessToken() {
try {
console.log('Attempting to refresh access token...');
const response = await fetch(<code dir="ltr" style="background:#f3f4f6; color:#0056b3; padding:2px 6px; border-radius:4px; font-family:monospace; direction:ltr !important; display:inline-block;">${API_BASE_URL}/auth/refresh-token</code>, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ refreshToken: refreshToken })
});
if (response.ok) {
const data = await response.json();
accessToken = data.accessToken;
localStorage.setItem('accessToken', accessToken);
// Optionally, if the server returns a new refresh token
if (data.refreshToken) {
refreshToken = data.refreshToken;
localStorage.setItem('refreshToken', refreshToken);
}
console.log('Access token refreshed successfully!');
return true; // Token refreshed
} else if (response.status === 401 || response.status === 403) {
console.error('Refresh token invalid or expired. Logging out...');
// Handle logout, e.g., redirect to login page
logoutUser();
return false; // Refresh failed, need to log out
} else {
console.error('Failed to refresh token:', response.statusText);
return false; // Refresh failed
}
} catch (error) {
console.error('Network error during token refresh:', error);
return false; // Refresh failed due to network error
}
}
async function authenticatedFetch(url, options = {}) {
let headers = options.headers || {};
// Add Authorization header with current access token
if (accessToken) {
headers['Authorization'] = <code dir="ltr" style="background:#f3f4f6; color:#0056b3; padding:2px 6px; border-radius:4px; font-family:monospace; direction:ltr !important; display:inline-block;">Bearer ${accessToken}</code>;
}
options.headers = headers;
let response = await fetch(url, options);
if (response.status === 401) {
console.warn('Access token expired or unauthorized. Trying to refresh...');
const isRefreshed = await refreshAccessToken();
if (isRefreshed) {
console.log('Retrying original request with new access token...');
// Retry the original request with the new access token
headers['Authorization'] = <code dir="ltr" style="background:#f3f4f6; color:#0056b3; padding:2px 6px; border-radius:4px; font-family:monospace; direction:ltr !important; display:inline-block;">Bearer ${accessToken}</code>;
options.headers = headers;
response = await fetch(url, options);
} else {
// If refresh failed, the user is already logged out via refreshAccessToken()
// Or we could redirect here if not handled there.
return response; // Return the 401 response
}
}
return response;
}
function logoutUser() {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
accessToken = null;
refreshToken = null;
// Redirect to login page or update UI
window.location.href = '/login';
}
// Example usage:
async function fetchData() {
try {
const response = await authenticatedFetch(<code dir="ltr" style="background:#f3f4f6; color:#0056b3; padding:2px 6px; border-radius:4px; font-family:monospace; direction:ltr !important; display:inline-block;">${API_BASE_URL}/data/protected</code>);
if (response.ok) {
const data = await response.json();
console.log('Protected data:', data);
} else {
console.error('Failed to fetch data:', response.status, response.statusText);
}
} catch (error) {
console.error('Error fetching protected data:', error);
}
}
// Call this on app start to try and fetch data
// fetchData();
// Example for initial login (this would typically be on a login form submission)
async function login(username, password) {
try {
const response = await fetch(<code dir="ltr" style="background:#f3f4f6; color:#0056b3; padding:2px 6px; border-radius:4px; font-family:monospace; direction:ltr !important; display:inline-block;">${API_BASE_URL}/auth/login</code>, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, password })
});
if (response.ok) {
const data = await response.json();
accessToken = data.accessToken;
refreshToken = data.refreshToken;
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
console.log('Logged in successfully!');
// Redirect to dashboard or update UI
// window.location.href = '/dashboard';
} else {
console.error('Login failed:', response.status, response.statusText);
}
} catch (error) {
console.error('Network error during login:', error);
}
}
// Dummy login call (replace with actual form submission)
// login('testuser', 'password123');
ملاحظات مهمة على الطاير
- أمان الـ Refresh Token: تخزينه في
localStorageممكن يكون خطر إذا موقعك فيه ثغرات XSS. الحلول الأكثر أماناً هيhttpOnly cookies(ممكن السيرفر يرسلها لك) أو تخزينها في الذاكرة إذا كان تطبيقك SPA وما يحتاج يحافظ على الجلسة بعد تحديث الصفحة. - Race Conditions: لو عندك أكثر من طلب يرسل في نفس الوقت، وكلهم ياخذون
401، ممكن كلهم يحاولون يجددون التوكن في نفس الوقت. هذا بيسبب مشاكل. لازم تسوي آلية تخلي طلب واحد بس هو اللي يجدد التوكن، وباقي الطلبات تنتظر لين يخلص التجديد، بعدين تعيد إرسال نفسها. هذا يتطلب شوي تعقيد في الكود (ممكن تستخدمPromiseعشان تنتظر). - إدارة الحالة: كيف تدير حالة التوكنات في تطبيقك (هل هي موجودة، صالحة، منتهية)؟ مكتبات إدارة الحالة زي Redux أو Vuex أو Context API في React ممكن تساعدك كثير.
هذا كان شرح سريع ومبسط عن الـ Bearer Tokens وكيف تجدد صلاحيتها آلياً. طبقها صح، وتطبيقك بيكون آمن وسلس للمستخدمين. بالتوفيق يا بطل!