<style> body, p, li, table { font-family: "Arial", "MingLiU"; } h1, h2, h3, h4, h5, h6 { font-family: "Arial", "Microsoft YaHei"; } h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { font-family: "Consolas"; } pre, code { font-family: "MingLiU"; white-space: pre-wrap !important; word-break: break-all !important; } .markdown-body pre { margin-bottom: 3px; padding: 10px 12px 6px 12px; } .markdown-body h6 code { font-size: 9pt; font-family: 'Arial'; font-weight: normal;; } .div-font { font-family: "Arial", "MingLiU"; } pre.fit { margin-top: -1em; margin-bottom: 0.5em; } pre.inline { display: inline; padding: 0.2em 0; margin: 0; line-height: 1.5; } pre.inline::before, pre.inline::after { content: ""; display: inline-block; width: 0.3em; } pre.code-block { margin: -1em 0em 0.25em; font-size: 0.85em; } .markdown-body p+pre { margin-top: -12px; padding-top: 6px; margin-bottom: 0px; } code.inpara { display: inline-block; margin: 0.5em 0; } span.spnote { font-size: 0.85em; color: #f43; } span.spnote.add { color: #43f; } span.spnote a { color: #912; } span.refsrc { display: block; margin-top: -0.5em; margin-left: 1.5em; font-size: 0.75em; font-family: "Arial", "MingLiU"; } .sp { color: #FF4000 !important; } a.private-note { color: indianred; } a.private-note:hover { color: firebrick; } .date-notes { font-size: 9pt; font-style: italic; color: gray; margin-bottom: 1em; } .lfis { margin-left: 1.5rem; } .lfid { margin-left: 2rem; } .smn { font-size: 0.8em; color: mediumslateblue; margin-right: 0.33em; } .ano { font-size: 0.9em; color: #f43; } i.emph { color: #d66; } i.empho { color: #93c; } .left-indent { margin-left: 2em; } strong, .emphasis { font-family: "Arial", "Microsoft YaHei"; font-weight: bold; } strong code { font-family: "Consolas", "Microsoft YaHei"; } .alert { margin-bottom: 0; } .alert-success a { color: #40b142; } .alert-warning a { color: #c19953; } .alert-danger a { color: #dc625f; } h6 div.timestamp { margin-top: 1rem; } </style> # 前端 CORS (使用 Fetch API) 與後端 (PHP) 設定 ###### Tags: `JavaScript` `FetchAPI` `CORS` `PHP` ## 前端程式碼示例 example.js ```javascript fetch('http://example.url/example/route', { method: 'POST', credentials: 'include', headers: new Headers({ 'Custom-Header': encodeURIComponent('A custom message'), // 可傳輸非 ISO-8859-1 字元 'Custom-Cookie': document.cookie }) }) .then(response => { if (response.ok) return response.json(); throw new Error(`${response.url} (${response.statusText})`); }) .then(async data => { console.log(data); // 將後端回應的 Base64 資料解碼再轉回 UTF-8 let message = decodeURIComponent(window.atob(data.message)); console.log(message); }) .catch(error => { console.warn(error); }); ``` ## 後端程式碼示例 http://example.url/example/route ```php <?php # 白名單 $allowedHost = [ '61.62.63.64', '101.102.103.104', '185.186.187.188' ]; # 取得請求發送者的 IP $clientIP = (function() { $ip = null; if (isset($_SERVER['HTTP_CLIENT_IP']) && $_SERVER['HTTP_CLIENT_IP'] !== '') { $ip = $_SERVER['HTTP_CLIENT_IP']; } else if (isset($_SERVER['HTTP_X_FORWARDED_FOR']) && $_SERVER['HTTP_X_FORWARDED_FOR'] !== '') { $ip = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0]; } else if (isset($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] !== '') { $ip = $_SERVER['REMOTE_ADDR']; } return trim($ip); })(); # 帶微秒的請求時間 $requestData = (function() { $time = explode('.', $_SERVER['REQUEST_TIME_FLOAT']); $date = date('Y-m-d H:i:s', $time[0]); return "{$date}.{$time[1]}"; })(); # 限定接受的 HTTP 方法為 OPTIONS(預檢請求)及 POST(自定義),其他方法回 404 switch ($_SERVER['REQUEST_METHOD']) { case 'OPTIONS': case 'POST': { # 宣告 CORS 預檢請求(Preflight Request)所需要的 header if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { header("Access-Control-Request-Method: GET, POST, PUT, PATCH, DELETE, OPTIONS"); header('Access-Control-Allow-Headers: Content-Type, Custom-Header, Custom-Cookie'); } # 允許 CORS 認證 header('Access-Control-Allow-Credentials: true'); # 連入 IP 不在白名單內,返回 401 if (!in_array($clientIP, $allowedHost)) { header("{$_SERVER['SERVER_PROTOCOL']} 401 Unauthorized"); exit; } # 請求來自瀏覽器時,在回應 header 中加入允許來源 if (isset($_SERVER['HTTP_ORIGIN'])) { header("Access-Control-Allow-Origin: {$_SERVER['HTTP_ORIGIN']}"); } # 預檢 OPTIONS 動作完畢,跳出 switch case if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') break; # 回應資料陣列 $data = []; # 請求 header 中有 Custom-Header 時,將其值 base64 編碼處理後加入為回應資料的 message 欄位 if (isset($_SERVER['HTTP_CUSTOM_HEADER'])) { $data['message'] = base64_encode($_SERVER['HTTP_CUSTOM_HEADER']); } # 取得請求 header 中帶的 Custom-Cookie $customCookie = []; if (isset($_SERVER['HTTP_CUSTOM_COOKIE'])) { $customCookiePair = explode(';', $_SERVER['HTTP_CUSTOM_COOKIE']); foreach ($customCookiePair as $cookiePair) { $cookieSlice = explode('=', $cookiePair); if (trim($cookieSlice[0]) !== '') { $customCookie[trim($cookieSlice[0])] = trim($cookieSlice[1]); } } } $data['customCookie'] = $customCookie; # 通知瀏覽器建立 cookie(須指定 SameSite=None; Secure 選項) setcookie('lastAccessDate', $requestDate, [ 'samesite' => 'None', 'secure' => true ]); # 將 SERVER 資訊加入回應資料以便檢視 $data = array_merge($data, ['server' => $_SERVER]); # 將 COOKIE 資訊加入回應資料以便檢視 $data = array_merge($data, ['cookie' => $_COOKIE]); header('Content-Type: application/json'); echo json_encode($data, 320); break; } default: header("{$_SERVER['SERVER_PROTOCOL']} 404 Not Found"); break; } exit; ```