Try   HackMD

前端 CORS (使用 Fetch API) 與後端 (PHP) 設定

Tags: JavaScript FetchAPI CORS PHP

前端程式碼示例

example.js

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

# 白名單
$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;