有時候想寫個簡單的小工具來拉取或查詢 API 資料,可能是開源資料、可能是公司內部站台用的資料,當然有很多前端平台可以選擇(網頁、手機、PC),為了方便隨時開啟,有時會選擇簡單的靜態網頁,但是使用網頁呼叫 API 可能會遇到 CORS Policy 的問題?那該如何解決呢?
前端網頁呼叫 API 使用網頁呼叫 API 有很多方式,而且各有優缺點,可以參考其他大大寫的 AJAX與Fetch API :
AJAX 與 XMLHttpRequest
HTML5 的 Fetch
…
1 2 3 4 5 6 7 8 9 10 11 12 13 var requestOptions = { method : 'GET' , redirect : 'follow' };fetch("http://192.168.99.222:12345/api/Member/infos" , requestOptions) .then(response => response.text()) .then(result => { console .log(result); document .querySelector("#error" ).innerHTML = `` ; jsonToResultTable(result); }) .catch(error => { console .log('error' , error); document .querySelector("#error" ).innerHTML = `${error} ` ; window .clearInterval(timeoutID); });
上面的程式碼在執行時會發生錯誤,打開 Condole 看,發現是關於 CORS Policy 的問題。
1 Access to fetch at 'http://....' from origin 'null' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
CORS Policy 的用途 提及 CORS Policy,先理解一下什麼是同源政策 (Same-Origin Policy) 同源政策指的是說網頁透過 AJAX 或是 Fetch 技術存取 API 資料時,必須達成下面三個條件
相同協定 protocol
相同主機位置 domain
相同埠號 port
如果不滿足就會是一個跨來源請求 (Cross-origin http request)!! 而 CORS Policy 全名為 Cross-origin resource sharing Policy,叫做”跨來源資源共享政策”, 它就是用來訂定跨來源的請求和伺服器該如何回應。
如果是跨來源請求,網頁會自動補上 Access-Control-Request 相關參數,成為 Preflight Request。
1 2 3 4 5 OPTIONS /api/Member/infos Host: 192.168.99.222:12345 Origin: ... Access-Control-Request-Method: GET Access-Control-Request-Headers: Content-Type
伺服器端需要在回應的 Header 加上 Access-Control-Allow-Origin、Access-Control-Request-Method、Access-Control-Request-Headers 等設定, 用來規定伺服器能接受的來源 Origin、可用方法 Method、Header, 規則也很多,可以自行參考其他資料 CORS 。
CORS Policy 的處理方式
後端伺服器站台調整支援特定 Origin 或是 Header 的”跨來源請求”,這是最正規的方式,也是最適當的。
關閉瀏覽器的安全性設置,似乎有些招式可以關閉但可能不太安全 - Run Chrome browser without CORS
使用代理站台,在後端呼叫 API 後再加上符合 CORS Policy 的 Header,回傳原始資料即可。
找尋他人已建立的代理站台,通常會有請求限制或是付費方案,例如: netnr 。
在無法調整後端伺服器站台的前提下,感覺第三種可以玩玩看!
[範例一]作法流程: 這裡是使用較簡易的建立站台方式,用 Node.js 單一腳本完成, 最後再使用前端網頁測試看看,是否可以轉打資料回來。
建立代理站台 建立 App.js,並在終端機執行 node app.js
, 應該會看到 🟢 Server running at http://127.0.0.1:3000/
,就代表成功建立站台。
App.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 const http = require ('http' );const url = require ('url' );const hostname = '127.0.0.1' ;const port = 3000 ;const server = http.createServer((req, res ) => { console .log("🔵 Receive request: " +req.url); let urlObject = url.parse(req.url,true ); console .log(urlObject); let queryObject = urlObject.query; console .log(queryObject); if (req.method == 'GET' && urlObject.pathname == '/get' ) { http.get(queryObject["target" ], (resp ) => { let data = '' ; resp.on('data' , (chunk ) => { data += chunk; }); resp.on('end' , () => { res.statusCode = resp.statusCode; res.setHeader('Access-Control-Allow-Origin' , '*' ); res.setHeader('Access-Control-Allow-Credentials' , false ); res.setHeader('Content-Type' , 'text/plain; charset=utf-8' ); res.end(data); }); }).on("error" , (err ) => { console .log("🔴 Error: " + err.message); }); } else { res.statusCode = 200 ; res.setHeader('Content-Type' , 'text/plain; charset=utf-8' ); res.end('Hello World' ); } }); server.listen(port, hostname, () => { console .log(`🟢 Server running at http://${hostname} :${port} /` ); }); process.on('uncaughtException' , function (err ) { console .log('🔴 Caught exception: ' , err); });
打開網頁 要記得先編輯此網頁,把 fetch 的目標改成代理站台的 API, 例如上面規定的 path 為 /get,parameter 為 target = 某個API ( CORS 是被禁止或是被限制的 )http://localhost:3000/get?target=http://.....
編輯好,就可以打開網頁有沒有資料回來。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 <html > <head > <title > AppInfo Table</title > <meta charset ="utf-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1" > <link rel ="stylesheet" href ="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" > <script src ="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js" > </script > <script src ="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js" > </script > <script src ="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" > </script > </head > <body style ="background-color:WhiteSmoke;" > <div class ="setting" style ="margin:10px 10px;" > <button class ="btn btn-dark" onclick ="onGetData()" > Get All Data!!</button > </div > <script > function onGetData ( ) { var requestOptions = { method : 'GET' , redirect : 'follow' }; fetch("http://localhost:3000/get?target=http://....." , requestOptions) .then(response => response.text()) .then(result => { console .log(result); }) .catch(error => { console .log('error' , error); }); } </script > </body > </html >
[範例二]作法流程: 這裡嘗試將”網站站台”與”轉跳站台”是相同的站台,就不用加上 CORS 允許的 Header 並且此範例可以通吃所有 method 的請求。
建立代理站台 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 const http = require ('http' );const request = require ('request' );const fs = require ('fs' );const hostname = 'localhost' ; const port = 3000 ; var requestCount = 0 ;const server = http.createServer((req, res ) => { if ( !req.url.startsWith('/redirect' )){ showGeneral(res,req.url); return ; } let date = new Date (); console .log(`⚪ [${date.toString()} ]` ); requestCount += 1 ; console .log("⚪ Request Count: " +requestCount); let data = '' ; req.on('data' , chunk => { data += chunk; }); req.on('end' , () => { try { let url = req.url.slice(10 ); url = decodeURIComponent (url); var options = { 'method' : req.method, 'url' : url, 'headers' : { 'Content-Type' : `${req.headers["content-type" ]} ` , 'authorization' : `${req.headers["authorization" ]} ` }, body : data }; console .log("🔵 Send Request" ); console .log("🔵 Request FixUrl: " +url); console .log("🔵 Request Data: " +data); request(options, function (error, response ) { let date = new Date (); console .log(`⚪ [${date.toString()} ]` ); if (error) console .log('🔴 Caught exception: ' , error); console .log('🟢 Response StatusCode: ' + response.statusCode); res.statusCode = response.statusCode; res.setHeader('Content-Type' , 'text/plain; charset=utf-8' ); res.end(response.body); }); }catch (err) { console .log('🔴 Caught exception: ' , err); } }); }); server.listen(port, hostname, () => { console .log(`🟢 Server running at http://${hostname} :${port} /` ); }); process.on('uncaughtException' , function (err ) { console .log('🔴 Caught exception: ' , err); }); function showGeneral (res,url ) { let type = "" ; if (url.endsWith('html' )){ type = 'text/html' ; } else if (url.endsWith('css' )){ type = 'text/css' ; } else if (url.endsWith('ico' )){ res.end(); return ; } else { type = 'text/file' ; }; res.writeHead(200 , { 'Content-Type' : type }); fs.readFile('.' +url, null , function (error, data ) { res.write(data); res.end(); }); }
打開網頁 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 <html > <head > <title > AppInfo Table</title > <meta charset ="utf-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1" > <link rel ="stylesheet" href ="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" > <script src ="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js" > </script > <script src ="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js" > </script > <script src ="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" > </script > </head > <body style ="background-color:WhiteSmoke;" > <div class ="setting" style ="margin:10px 10px;" > <button class ="btn btn-dark" onclick ="onGetData()" > Get All Data!!</button > </div > <script > function onGetData ( ) { var requestOptions = { method : 'GET' , redirect : 'follow' }; fetch("http://localhost:3000/redirect/https://httpbin.org/get" , requestOptions) .then(response => response.text()) .then(result => { console .log(result); }) .catch(error => { console .log('error' , error); }); } </script > </body > </html >