區域網路存取權,讓你正視呼叫API的方法

區域網路存取權,阻擋跨域API

為什麼 API 權限全開,還是會封鎖連線?

開發前後端分離架構,或是串接不同環境 API 時,常發生一種情況:後端已設定 Access-Control-Allow-Origin: *(允許所有網域存取),且 Postman 測試正常,但從瀏覽器環境透過 JavaScript 發送請求給內網或私有環境的 API 伺服器時,Chrome 卻強制阻擋連線。

這是 Chrome 實施「區域網路存取權」(Local Network Access, LNA)規範所致,目的是為了保護使用者免於跨網站要求偽造 (CSRF) 攻擊。

使用者在初次進入網站時,可能會看到這2種提示訊息視窗(也可能不會看到就自動封鎖):

區域網路存取權限視窗 區域網路存取權限視窗

這會帶來什麼影響?

這項安全機制會導致即使後端 API 權限全開,瀏覽器仍攔截請求。具體的錯誤訊息如下:

Access to fetch at 'https://api.example.com/apiName' from origin 'https://www.example.com' has been blocked by CORS policy: Permission was denied for this request to access the `unknown` address space.

net::ERR_FAILED

看到 Permission was denied for this request to access the ... address space 關鍵字,即代表觸發區域網路存取限制。瀏覽器判定來源(如 www.example.com)位於公網,而目標解析後位於私有網路或無法判定的位址空間,直接封鎖。

技術債的償還:API 呼叫的正規做法

遇到此問題時,有些網站管理者會教使用者如何解除封鎖,並叫他們點擊「允許」:

  1. 點擊網址左邊的icon。
  2. 開啟存取權。
  3. 重新整理頁面。
  4. 跳出提示視窗時點擊允許。
使用者手動解除封鎖的方法


但從架構安全的角度來看,前端直接呼叫私有 API 或第三方服務,本質上就是一種「技術債」,就算寫在 .env 裡面有心人也有辦法翻出來。API 呼叫的最佳實踐本來就該是 Server-to-Server(伺服器對伺服器)

以串接 HubSpot API 為例,正常情況下,不會有人直接在前端 JavaScript 呼叫 API,因為這會導致 API Key 直接暴露在瀏覽器的原始碼或 Network Tab 中,讓任何人都能竊取並濫用。同理,企業內部的 API 即使位於內網,若依賴前端直接存取,不僅受限於瀏覽器的 CORS 與 LNA 政策,更暴露了內部網路架構細節。

Chrome 的這項限制,實則是強迫開發者修正架構,改用後端 Proxy 來處理跨網域或跨安全層級的資料請求。

解決方案:建立後端 Proxy

透過與前端同網域(或公網可存取)的後端伺服器轉發請求,將流程轉為 S2S。後端伺服器不受瀏覽器安全限制,可自由存取目標 API,同時隱藏敏感資訊(如 Token 或真實 IP)。

程式範例

1. 前端直接呼叫(技術債寫法,會失敗)

以下寫法除了觸發 Chrome 錯誤,若包含機敏參數也極不安全。

// ❌ 前端直接呼叫,觸發 LNA 限制且暴露架構
const targetApi = 'https://api.example.com/apiName';

fetch(targetApi)
  .then(response => response.json())
  .catch(error => {
    // Console 顯示 access the `unknown` address space 錯誤
    console.error('被 Chrome 封鎖:', error);
  });

2. 前端改呼叫 Proxy(正規寫法)

前端不再接觸目標 API,僅呼叫自己的中介層。

// ✅ 呼叫同網域後端,安全且合規
// 假設您的後端 Proxy 檔案位於同網域下
const proxyUrl = '/proxy.php';

fetch(proxyUrl)
  .then(response => {
    if (!response.ok) throw new Error('Proxy error');
    return response.json();
  })
  .then(data => {
    console.log('成功取得資料:', data);
  })
  .catch(error => {
    console.error('連線錯誤:', error);
  });

3. 後端 Proxy 實作範例 (PHP)

由後端代為發送請求,這是最穩健的 API 介接方式。

<?php
// proxy.php


// 舊專案 PHP 5.6 version
// 1) 驗證請求來源 host(允許 localhost / 127.0.0.1 任意 port 或正式網域)
// 2) 僅接受 POST(並處理 OPTIONS 預檢)
// 3) 以 server-side 呼叫 jwt API (https://api.example/m/login)
// 4) 將 upstream 回應與狀態回傳給前端,並處理錯誤

// (1) 基本設定
// $allowed_hosts = array('127.0.0.1', 'localhost'); // 開發用,包含任意 port
$allowed_hosts = array('www.example.com.tw');
$API_KEY = '777777777';
$upstream_url = 'https://api.example.com/m/login';

// (2) 取出 Origin(優先)或 Referer 作為來源參考
$origin_header = '';
if (!empty($_SERVER['HTTP_ORIGIN'])) {
  $origin_header = $_SERVER['HTTP_ORIGIN']; // e.g. http://127.0.0.1:53721
} elseif (!empty($_SERVER['HTTP_REFERER'])) {
  // 取 referer 的 scheme+host+port(若有)
  $origin_header = preg_replace('#^(https?://[^/]+).*$#', '$1', $_SERVER['HTTP_REFERER']);
}

// (3) 解析 host(不含 port)以比對允許清單
$origin_host = '';
if ($origin_header) {
  $parts = parse_url($origin_header);
  if ($parts !== false && isset($parts['host'])) {
    $origin_host = strtolower($parts['host']);
  }
}

// (4) 檢查來源是否在允許清單
if (!in_array($origin_host, $allowed_hosts)) {
  header('HTTP/2 403 Forbidden');
  header('Content-Type: application/json; charset=utf-8');
  echo json_encode(array('Success' => false, 'ErrorMsg' => 'Forbidden: invalid origin', 'origin' => $origin_header));
  exit;
}

// (5) CORS header:只對合法 origin 回傳 Access-Control-Allow-Origin 原樣值(含 port)
if ($origin_header) {
  header('Access-Control-Allow-Origin: ' . $origin_header);
} else {
  // 保險起見,若無 origin header 也拒絕(已在上面判斷過)
  header('HTTP/2 403 Forbidden');
  header('Content-Type: application/json; charset=utf-8');
  echo json_encode(array('Success' => false, 'ErrorMsg' => 'No origin header'));
  exit;
}

header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
header('Content-Type: application/json; charset=utf-8');

// (6) 預檢請求直接回應 (OPTIONS)
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
  // 可以回 204 或 200
  http_response_code(204);
  exit;
}

// (7) 嚴格要求 POST 方法
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
  header('HTTP/2 405 Method Not Allowed');
  header('Allow: POST, OPTIONS');
  echo json_encode(array('Success' => false, 'ErrorMsg' => 'Only POST method is allowed'));
  exit;
}

// (8) 準備向 upstream 發出請求
$payload = array(
  'system' => 'm',
  'apiKey' => $API_KEY
);
$payload_json = json_encode($payload);

// 初始化 cURL
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $upstream_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload_json);
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json', 'Content-Length: ' . strlen($payload_json)));
curl_setopt($ch, CURLOPT_TIMEOUT, 15); // 整體 timeout
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); // 連線 timeout

// 若你的環境需要,請勿關閉 SSL 驗證;下列為預設安全行為(不用設定 CURLOPT_SSL_VERIFYPEER=false)
// curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);

// (9) 執行 cURL 並取得回應、HTTP status
$response = curl_exec($ch);
$curl_errno = curl_errno($ch);
$curl_err = curl_error($ch);
$http_status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

// (10) 錯誤處理:cURL 錯誤視為 502 Bad Gateway 回傳
if ($curl_errno) {
  http_response_code(502);
  echo json_encode(array('Success' => false, 'ErrorMsg' => 'cURL error: ' . $curl_err, 'ErrorNo' => $curl_errno));
  exit;
}

// (11) 若 upstream 回傳非 2xx,將 upstream 的原始回應與狀態回傳(方便前端 debug)
if ($http_status < 200 || $http_status >= 300) {
  http_response_code(502);
  // 嘗試解析 upstream 回應為 JSON,若不是則以 raw string 包裝
  $up = json_decode($response, true);
  if ($up === null) {
    echo json_encode(array('Success' => false, 'ErrorMsg' => 'Upstream returned HTTP ' . $http_status, 'RawResponse' => $response));
  } else {
    // 若 upstream 自帶 Success 欄位,直接轉發
    echo json_encode($up);
  }
  exit;
}

// (12) 成功:直接把 upstream 的回應原封不動回傳(假設為 JSON)
echo $response;
exit;
?>

結語

當 Console 出現 access the unknown address space 錯誤時,代表 Chrome 擋下了不安全的跨層級連線。這不單是瀏覽器設定問題,更是架構設計的警訊。

解決此問題的正確途徑,並非尋找繞過瀏覽器檢查的偏門方法,而是償還技術債,建立後端 Proxy。這不僅解決了連線問題,更確保了 API Key 與內部架構的安全性。

留言

這個網誌中的熱門文章

如何產生醒目顯示文字的連結?讓使用者一目瞭然的功能(Scroll to Text Fragment)

用CSS的 min max與vw cqw,設計有極限值的RWD響應式文字

Google Search Console 網頁發現方式多了「參照網頁」