استفاده از لاراول به عنوان یک سرویس پروکسی/دروازه

هفته گذشته در روز کاری خود، در حال بررسی گزینههایی برای پیادهسازی یک نقطه پایانی پروکسی برای یکی از سرویسهای خارجی خود بودم. یکی از پشته های مورد استفاده در برنامه ما لاراول است، بنابراین طبیعتاً من بررسی کردم که چگونه یک نقطه پایانی پروکسی را با استفاده از آن پیاده سازی کنم. به نظر می رسد بسیار ساده است، یک لاراول قبلاً شامل بسیاری از موارد مورد نیاز برای ساخت آن است. این رویکرد باید برای بسیاری از موارد استفاده ساده مناسب باشد تا از اضافه کردن سرویس پروکسی اختصاصی جدید مانند یک دروازه API کامل مانند Traefik یا Kong جلوگیری شود، یا اگر به منطق سفارشی نیاز دارید که ممکن است با استفاده از راهحلهای موجود به سختی به دست آورید. .
چرا
هنگام ساخت برنامه با استفاده از معماری میکروسرویس/سرویس گرا، معمولاً نیاز به اتصال به سرویس های خارجی است. این سرویسها ممکن است برخی از APIهای فروشنده مانند ارائهدهنده ایمیل، درگاه پرداخت، یا صرفاً سرویس داخلی باشد که بهصورت اتفاقی در شبکههای متفاوتی وجود دارد. برخی موارد استفاده رایج:
- محدود کردن دسترسی به اعتبار با استفاده از یک سرویس پروکسی واحد، سایر سرویسها نیازی به ذخیره اعتبار برای سرویسهای خارجی نداشتند، بنابراین کنترل بهتری از نظر نگهداری و امنیت فراهم میکردند.
- به اشتراک گذاری منبع برای مثال: oauth token. هر سرویس نیازی به درخواست توکن خود نداشت، اما در عوض میتوانست همان توکنی را که توسط سرویس پروکسی اداره میشود به اشتراک بگذارد. مثال دیگر ذخیره کردن منابع است، بنابراین منابعی که به طور مکرر دسترسی دارند فقط باید در چند بار دسترسی داشته باشند.
چرا لاراول
در retropect، لاراول ممکن است یک انتخاب عجیب و غریب برای ساخت یک سرویس پروکسی سفارشی / دروازه API باشد. اگر قصد دارید این سرویس را به عنوان یک سرویس مستقل بسازید، ممکن است استفاده از برخی ریزفریمورکها مانند Lumen یا فقط استفاده از Symphony منطقیتر باشد. گزینه های دیگر استفاده از پشته های دیگر مانند Golang یا NodeJs است. با این حال، در مورد من، سیستم در واقع در سرویس لاراول موجود تعبیه شده است (به دلایلی)، و به همین دلیل من می خواهم تجربه خود را به اشتراک بگذارم، اگر کسی با وضعیت مشابهی مانند من مواجه شود.
مزایا در مقایسه با سرویس پروکسی اختصاصی / دروازه api
- آسان برای پیاده سازی منطق سفارشی. هنگام استفاده از سرویس اختصاصی، ممکن است لازم باشد از مقداری DSL مبهم استفاده کنید یا اسکریپت خود را به روشی عجیب وصل کنید. بدیهی است که این ممکن است ذهنی باشد، اما IMHO با استفاده از پشته موجود یک مزیت واضح است زیرا بار شناختی در ساخت و نگهداری سرویس را کاهش می دهد.
- بدون هزینه اضافی تعمیر و نگهداری: هیچ فناوری جدیدی نیاز به یادگیری ندارد
منفی
- افزایش کار اضافی در مقایسه با دسترسی مستقیم، اگرچه بستگی به موقعیت دارد. کش در واقع ممکن است اتصال شبکه را بهبود بخشد
- مقیاس پذیری محدود
چگونه
پیش نیازها
برای این آموزش، از کتابخانه Guzzle به عنوان سرویس گیرنده HTTP استفاده می کنیم. برخی از شما ممکن است تعجب کنید، “چرا ما از کلاینت HTTP داخلی لاراول استفاده نمی کنیم؟”. خوب، استفاده از کلاینت لاراول راحت تر است، اما برای نیازهای ما انعطاف پذیری کمتری دارد. HTTP Client لاراول در واقع فقط یک بسته بندی در اطراف Guzzle است، بنابراین منطقی است که در واقع کمتر انعطاف پذیر است.
برای نصب guzzle کافیست اجرا کنید
composer require guzzlehttp/guzzle
همچنین برای اختصار، در اینجا نقطه پایانی را درست در فایل مسیر خود پیاده سازی می کنیم. در عمل، جدا کردن منطق به فایل کنترلر اختصاصی تمیزتر خواهد بود. در اینجا می توانید یکی را انتخاب کنید routes/web.php
یا routes/api.php
، یا رویداد در صورت تمایل یک فایل مسیر جدید ایجاد کنید.
استفاده اولیه
بیایید ساده شروع کنیم. در اینجا نقطه پایانی جدیدی ایجاد می کنیم که با توجه به روش مسیر و HTTP، httpbin.org را فراخوانی می کند
use GuzzleHttp\Client as HttpClient;
Route::any('/proxy/{path}', function(Request $req, $path) {
$client = new HttpClient([
'base_uri' => 'https://httpbin.org'
]);
return $client->request($req->method(), $path);
});
کد باید کاملاً ساده باشد. برای هر درخواست به /proxy/{path}
، آن را به درخواست httpbin.org/{path}
با همان روش HTTP می توانید با استفاده از روش های مختلف، نقطه پایانی جدید را درخواست کنید تا اثر آن را ببینید. در اینجا من از HTTPie برای آزمایش نقطه پایانی استفاده کردم:
http POST localhost:8000/proxy/post
http PUT localhost:8000/proxy/put
http DELETE localhost:8000/proxy/delete
مسیرهای فرعی پروکسی
اگر متوجه شده باشید، متغیر path در واقع فقط می تواند مسیر را بازیابی کند، اما مسیر فرعی را نه. برای مثال دریافت localhost:8000/proxy/get کار میکند، اما localhost:8000/proxy/get/subpath ناموفق خواهد بود، زیرا لاراول قادر به مسیریابی بعدی نخواهد بود. راه حل این است که روش “where” را اضافه کنید تا به متغیر مسیر اجازه دهید همه مسیرهای فرعی را بگیرد. بنابراین فقط اضافه کنید:
Route::any(
//...
)->where('path', '.*');
اضافه کردن بدنه درخواست، پارامترها و کد پاسخ
همچنین ممکن است متوجه شوید که اجرای فعلی ما بدنه درخواست، پارامترهای پرس و جو و کد وضعیت پاسخ ما را ارسال نمی کند. پس بیایید آن را تغییر دهیم:
//...
$resp = $client->request($req->method(), $path, [
'query' => $req->query(),
'body' => $req->getContent(),
]);
return response($resp->getBody()->getContents(), $resp->getStatusCode());
//...
ارسال سرصفحه های لازم
در نهایت، ممکن است متوجه شوید که اجرای ما سرصفحههای درخواست و پاسخ را ارسال نمیکند. هدرها در واقع بسیار مشکل هستند زیرا ممکن است بر درخواست و پاسخ ما تأثیر بگذارند و آنها را نامعتبر کنند. من شخصا دریافتم که بهتر است فقط فیلدهای هدر لازم را فوروارد کنم و بقیه را نادیده بگیرم. این نیز امنیت ما را بهبود می بخشد.
برای انجام این کار، ما یک تابع کمکی برای فیلتر کردن هدرها آماده خواهیم کرد و فقط هدرهای «نوع محتوا» و «قبول» را استخراج می کنیم. البته شما همچنین می توانید آن را با توجه به نیاز خود تغییر دهید:
// simple helper function to filter header array on request & response
function filterHeaders($headers) {
$allowedHeaders = ['accept', 'content-type'];
return array_filter($headers, function($key) use ($allowedHeaders) {
return in_array(strtolower($key), $allowedHeaders);
}, ARRAY_FILTER_USE_KEY);
}
و سپس می توانیم از آن در نقاط پایانی خود استفاده کنیم. نهایی ما می تواند این باشد:
<?php
// you could use either routes/web.php or routes/api.php
// simple helper function to filter header array on request & response
function filterHeaders($headers) {
$allowedHeaders = ['accept', 'content-type'];
return array_filter($headers, function($key) use ($allowedHeaders) {
return in_array(strtolower($key), $allowedHeaders);
}, ARRAY_FILTER_USE_KEY);
}
Route::any('/proxy_example/{path}', function(Request $request, $path) {
$client = new GuzzleHttp\Client([
// Base URI is used with relative requests
'base_uri' => 'https://pie.dev', // public dummy API for example
// You can set any number of default request options.
'timeout' => 60.0,
'http_errors' => false, // disable guzzle exception on 4xx or 5xx response code
]);
// create request according to our needs. we could add
// custom logic such as auth flow, caching mechanism, etc
$resp = $client->request($request->method(), $path, [
'headers' => filterHeaders($request->header()),
'query' => $request->query(),
'body' => $request->getContent(),
]);
// recreate response object to be passed to actual caller
// according to our needs.
return response($resp->getBody()->getContents(), $resp->getStatusCode())
->withHeaders(filterHeaders($resp->getHeaders()));
})->where('path', '.*'); // required to allow $path to catch all sub-path
بعد
این پیاده سازی باید 80-90٪ موارد استفاده را پوشش دهد. با این حال، همانطور که در ابتدای این مقاله بیان شد، می توانید این کد را به گونه ای گسترش دهید که قابلیت های بیشتری را شامل شود. به عنوان مثال میتوانید مکانیسم احراز هویت را در اینجا اضافه کنید، یا مقداری حافظه پنهان برای کاهش تعداد درخواستهای شبکه اضافه کنید.