وصفات الوكيل العكسي وعقد ردّ النداء
كل وكيل عكسي يحتاج القطعتين نفسيهما: فحص مُخوِّل يسأل Caputchin إن كان طلب يحمل تصريحًا صالحًا، وردّ نداء على أصلك يحوّل تصريحًا محلولًا إلى كوكي cpt_gate. شرح nginx يبيّن الشكل من طرف إلى طرف؛ وهذه الصفحة تعطي الوكلاء الآخرين، وعقد ردّ النداء بالضبط، ومثال Authelia كاملًا. استبدِل YOUR_SITE_KEY بمفتاحك العامّ في كل موضع.
نقاط الوصول هي نفسها في كل مكان:
- المُخوِّل:
GET https://verify.caputchin.com/v1/gate/authz?site=YOUR_SITE_KEY، مع تمرير ترويسةCookieالخاصّة بالزائر.204تعني اسمح، و401تعني تحدٍّ. - التحدّي: أعِد توجيه المتصفّح إلى
https://verify.caputchin.com/v1/gate/challenge?site=YOUR_SITE_KEY&return=<الرابط الأصلي>.
المُخوِّل يجيب فقط 204 أو 401. وأي شيء آخر، بما فيه انتهاء المهلة، يعني أن Caputchin غير قابل للوصول، وما يحدث عندئذٍ هو وضع الفشل عندك: حجب الطلب (الفشل المغلق، أأمن لبوّابة تسجيل دخول) أو تمريره (الفشل المفتوح). اضبطه على مفتاح الموقع فيأتي المقتطف المولَّد مطابقًا. وأعطِ الطلب الفرعي للمُخوِّل مهلة قصيرة كي لا يعطّل انقطاعٌ كل طلب ريثما يقرّر.
Traefik
استخدِم وسيطة forwardAuth للمُخوِّل. يمرّر Traefik ترويسة Cookie الواردة افتراضيًّا. عند 401 من الوسيطة، أعِد توجيه المتصفّح إلى التحدّي (وسيطة صفحة خطأ موجّهة إلى مسار صغير يُصدِر 302، أو عالِج 401 عند حافّتك).
http:
middlewares:
cpt-gate:
forwardAuth:
address: "https://verify.caputchin.com/v1/gate/authz?site=YOUR_SITE_KEY"
# The 401 from forwardAuth is your signal to send the browser to:
# https://verify.caputchin.com/v1/gate/challenge?site=YOUR_SITE_KEY&return=<original-url>
routers:
portal:
rule: "Host(`auth.example.com`)"
middlewares: ["cpt-gate"]
service: your-appCaddy
استخدِم forward_auth للمُخوِّل وhandle_errors لتحويل 401 إلى إعادة توجيه. هذه نقطة بداية؛ اضبط صياغة التوجيهات لإصدار Caddy عندك.
auth.example.com {
forward_auth https://verify.caputchin.com {
uri /v1/gate/authz?site=YOUR_SITE_KEY
copy_headers Cookie
}
handle_errors {
@challenge expression {http.error.status_code} == 401
redir @challenge https://verify.caputchin.com/v1/gate/challenge?site=YOUR_SITE_KEY&return={scheme}://{host}{uri} 302
}
reverse_proxy your-app:8080
}ردّ النداء
تنتهي صفحة التحقّق المُستضافة بإرسال ذاتي لنموذج POST إلى https://<أصلك>/__cpt/callback بحقلين في الجسم: cpt_gate (التصريح) وto (إلى أين يُرسَل الزائر). وإن غيّرت مسار ردّ النداء على مفتاح الموقع، أُرسِل الطلب إلى هناك بدلًا منه. أضِف هذا المسار الواحد إلى أصلك (أو ككتلة location مخصّصة على الوكيل). يجب أن:
- يقرأ
cpt_gateوtoمن جسم POST، لا من الرابط أبدًا (تصريح في رابط يتسرّب إلى السجلّات والمُحيلين). - يؤكّد أن
toعلى أصلك أنت، ويرفض ما عداه، كي لا يُحوَّل ردّ النداء إلى إعادة توجيه مفتوحة. - يضبط الكوكي بالسمات أدناه.
- يعيد التوجيه (
303) إلىto.
// Any tiny handler on your origin works. Express shown; ~15 lines.
app.post("/__cpt/callback", express.urlencoded({ extended: false }), (req, res) => {
const pass = String(req.body.cpt_gate || "");
const to = String(req.body.to || "/");
const target = new URL(to, `https://${req.headers.host}`);
if (target.host !== req.headers.host) return res.status(400).end(); // same-origin only
res.setHeader(
"Set-Cookie",
`cpt_gate=${pass}; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=1800`
);
res.redirect(303, target.pathname + target.search);
});سمات الكوكي هي نموذج الأمان كلّه. ولأن التصريح غير مربوط بجهاز، فإن HttpOnly (يحجب سرقة السكربت)، وSecure (HTTPS فقط)، وSameSite=Lax، وMax-Age قصير (طابِقه مع مدّة السماح TTL) هي ما يحميه. وأيضًا: لا تسجّل قيمة cpt_gate في سجلّات وصولك، وأبقِ Referrer-Policy: no-referrer على ردّ النداء كي لا يركب التصريح أبدًا في ترويسة مُحيل.
وضع الفشل لكل وكيل
لا يرى Caputchin قرار وضع فشل أبدًا، فلا يظهر في إحصاءاتك أبدًا: وكيلك يتّخذه محليًّا حين لا يستطيع بلوغ المُخوِّل.
- nginx:
proxy_intercept_errors on;معerror_page 500 502 503 504 = @cpt_unreachable;، حيث تلك الكتلة المسمّاة تكونreturn 403(مغلق) أوreturn 204(مفتوح). - Traefik: تخفق وسيطة
forwardAuthعند عنوان غير قابل للوصول؛ وجّه ذلك إلى خدمة تردّ403(مغلق)، أو أسقِط الوسيطة من الموجّه خلف فحص سلامة (مفتوح). - Caddy: وسّع
handle_errorsبمطابِق على5xxيفعل إمّاrespond 403(مغلق) أوrespond 204(مفتوح).
نطاق المسارات لكل وكيل
طبّق البوّابة على هذه المسارات فقط ولا تطبّق البوّابة على هذه المسارات أبدًا تشكّلان الإعداد المولَّد؛ ومن يفرضهما هو وكيلك.
- nginx: كتلة
location ^~ <البادئة>لكل مسار مغلق، وauth_request off;داخل كل مسار مستثنى. ونمط امتداد مثل*.phpيصيرlocation ~* \.php$. - Traefik: اربط وسيطة
cpt-gateبالموجّهات التي تطابق قواعدها المسارات المغلقة فقط، واتركها عن البقية. - Caddy: لُفّ
forward_authداخل مطابِق مسار، مثلًا@gated path /admin/* /loginثمforward_auth @gated ....
لا تغلق أبدًا الطلب الفرعي للمُخوِّل ولا مسار ردّ ندائك.
مثال محلول: Authelia
بوّابة تسجيل الدخول في Authelia تطبيق أحادي الصفحة مُصرَّف بلا مكان لإدراج الأداة وبلا خطّاف إضافة، فالبوّابة هي طريقة حمايته. Authelia تجلس أصلًا خلف وكيل عكسي (هكذا يعمل ForwardAuth الخاصّ بها)، وذلك الوكيل هو موضع البوّابة. وAuthelia نفسها تُترَك دون أي تعديل بتاتًا.
ضَع البوّابة على vhost البوّابة (مثلًا auth.example.com):
- أغلِق ببوّابة HTML البوّابة ونقطة وصول تسجيل الدخول بالعامل الأول (
/api/firstfactor). - استثنِ نقطة وصول verify الخاصّة بـ ForwardAuth في Authelia (
/api/verify)، التي يناديها الوكيل لكل طلب upstream محمي. إغلاقها ببوّابة سيكسر كل تطبيق محمي خلف Authelia. - استثنِ فحوص السلامة والأصول الساكنة التي تحتاجها صفحة التحدّي نفسها.
- أضِف مسار
/__cpt/callbackكما أعلاه (أو مسار ردّ النداء الذي ضبطته).
هذه النقاط الأربع هي بالضبط ما تعبّر عنه إعدادات نطاق المسارات: / و/api/firstfactor كمسارات مغلقة، و/api/verify وفحص السلامة عندك كمسارات مستثناة.
تُبقي البوّابة البوتات بعيدة عن البوّابة؛ ويظلّ Regulation المدمج في Authelia (حدّ المحاولات والحظر) يسقّف محاولات بيانات الاعتماد لمن يمرّ، فتتراكم الطبقتان. تحمي البوّابة البوّابة التفاعلية فقط (واجهة تسجيل الدخول والموافقة)؛ ولا تُغلق، ولا ينبغي أن تُغلق، نقاط وصول الرموز غير التفاعلية.
انظر أيضًا
- أعدّ بوّابة صفحة الوكيل: شرح nginx والطرح بوضع المعاينة أولًا.
- خصّص التحدّي: نوع التحدّي، واللغة، والسمة، واسم الكوكي، ومسار ردّ النداء، ونطاق المسارات.
- نظرة عامة: الفكرة وكوكي السماح.
- الإحصاءات: أكّد التوصيل بمراقبة التحدّي مقابل المرور المباشر.