Magento 2: Обход ошибки Refused to display ... in a frame because it set 'X-Frame-Options' to 'SAMEORIGIN'
Задача:
На странице Magento 2 есть iframe в котором нужно отобразить вебстарницу. Многие сервисы (один из них Google например) запрещают использовать контент своих вебстраниц на сторонних ресурсах. Поэтому, среди заголовков (headers) (в ответе их сервера при запросе вебстраницы) мы видим такой заголовок как 'X-frame-options'. Данный заголовок оповещает веб-браузер о том, что данную вебстраницу запрещено отображать на сторонних ресурсах (т.е. домены которых не соответствуют исходному). В результате, при попытке отобразить подобную - защищённую страницу например внутри <iframe>
Но что делать если нам всё-таки очень хочется отобразить такую страницу в iframe на странице своего веб-ресурса? Представлю один из вариантов реализации решения данной задачи в Magento 2.
Решение:
Сначала коротко опишем логику простыми словами:- Атрибуту iframe "src" задаём URL - соответствующий вашему магенто-контроллеру, например: ITStorm\ApiTest\Controller\Adminhtml\Send\GetSecurePage (а, не URL желаемой страницы которую хотите отобразить). Также, в составе этого URL указываем параметры, которые мы (в дальнейшем) будем подставлять (если это конечно нужно) в реальный URL по которому будем обращаться непосредственно к ресурсу, страницу которого нам нужно отобразить в нашем iframe. В общем выглядеть это будет примерно так:
name="iframe_a" height="500px" width="100%" title="Iframe Example"></iframe>
где: prod-id и brand это вышеупомянутые параметры.
- Далее опишем, что происходит в нашем контроллере
GetSecurePage
:- а) принимает запрос из нашего iframe, извлекает параметры (prod-id и brand);
- б) формирует URL для GET запроса к ресурсу (страница которого нам нужна), подставляя эти параметры;
- в) по сформированному URL отправляет GET запрос ресурсу и получает ответ. И что немало важно, при этом представляясь как веб-браузер (путём подстановки типичных для браузера заголовков (headers));
- г) в полученном ответе от ресурса - выделяем и сохраняем список его заголовков, среди которых будет присутствовать и блокирующий заголовок
'X-Frame-Options'
. После чего - удаляем'X-Frame-Options'
из из получившегося списка. - д) также, из полученного ответа выделяем и сохраняем в переменную тело ответа (собственно сам контент страницы).
- е) далее, средствами Magento 2 создаём заготовку - "пустой ответ" (объект Response). Устанавливаем ему заголовки из нашего списка (в котором уже отсутствует
'X-Frame-Options'
), а также тело ответа (контент страницы). - ж) и наконец - возвращаем новоиспечённый Response, который и попадёт в наш iframe.
То есть мы, по сути подменяем реальный ответ ресурса (страница которого нам нужна) - своим, который не содержит блокирующего заголовка 'X-Frame-Options'
, и возвращаем его нашему iframe, который уже спокойно отобразит контент.
declare(strict_types=1); namespace ITStorm\ApiTest\Controller\Adminhtml\Send; use Magento\Backend\App\Action; use Magento\Framework\App\ResponseInterface; use Magento\Framework\Controller\ResultFactory; use Magento\Backend\App\Action\Context; use Magento\Framework\Controller\ResultInterface; use Magento\Framework\HTTP\ZendClient; use Psr\Log\LoggerInterface; use Magento\Framework\HTTP\ZendClientFactory; class GetSecurePage extends Action { public const API_URL_MASK = 'https://www.your-api.com/sourse/product-id/%s/&brand=%s'; /** * Fake headers for request */ private const CUSTOM_HEADERS = [ 'user-agent' => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.134 Safari/537.36', 'accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 'accept-encoding' => 'gzip, deflate, br', 'accept-language' => 'en-US;q=0.8,en;q=0.7', 'cache-control' => 'max-age=0' ]; /** * @var LoggerInterface */ private LoggerInterface $logger; /** * @var ZendClientFactory */ private ZendClientFactory $httpClientFactory; /** * ApiTesting constructor. * @param Context $context * @param LoggerInterface $logger * @param ZendClientFactory $httpClientFactory */ public function __construct( Context $context, LoggerInterface $logger, ZendClientFactory $httpClientFactory ) { $this->logger = $logger; $this->httpClientFactory = $httpClientFactory; parent::__construct($context); } /** * Получаем ответ, эмитируя запрос из браузера * * @return ResponseInterface|ResultInterface */ public function execute() { $prodId = $this->getRequest()->getParam('prod-id'); $brand = $this->getRequest()->getParam('brand'); $headers = []; try { $url = sprintf(self::API_URL_MASK, $prodId, $brand); $client = $this->getHttpClient(); $client->setUri($url); $client->setMethod(); $client->setHeaders(self::CUSTOM_HEADERS); // Для эмитации браузера устанавливаем заголовки $apiResponse = $client->request(); // Выполняем запрос и получаем ответ $headers = $apiResponse->getHeaders(); // Извлекаем заголовки // Remove the blocking header (Если нужно - удаляем блокирующий заголовок 'X-frame-options') if (isset($headers['X-frame-options'])) { unset($headers['X-frame-options']); } $body = $apiResponse->getBody(); // Извлекаем тело ответа } catch (\Exception $e) { $this->logger->warning('ITStorm_ApiTest: ' . $e->getMessage()); } // Create new response (собираем новый ответ) $response = $this->resultFactory->create(ResultFactory::TYPE_RAW); foreach ($headers as $name => $value) { $response->setHeader($name, $value); // В цикле устанавливаем наш список заголовков в объект нового ответа } $response->setContents($body); // Устанавливаем контент в объект нового ответа return $response; } private function getHttpClient(): ZendClient { $client = $this->httpClientFactory->create(); $client->setConfig([ 'maxredirects' => 1, 'timeout' => 30000 ]); return $client; } }
На мой взгляд - реализация достаточно простая и понятная. Успехов! :)