有時候想寫個簡單的小工具來拉取或查詢 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 資料時,必須達成下面三個條件

  1. 相同協定 protocol
  2. 相同主機位置 domain
  3. 相同埠號 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 的處理方式

  1. 後端伺服器站台調整支援特定 Origin 或是 Header 的”跨來源請求”,這是最正規的方式,也是最適當的。
  2. 關閉瀏覽器的安全性設置,似乎有些招式可以關閉但可能不太安全 - Run Chrome browser without CORS
  3. 使用代理站台,在後端呼叫 API 後再加上符合 CORS Policy 的 Header,回傳原始資料即可。
  4. 找尋他人已建立的代理站台,通常會有請求限制或是付費方案,例如: 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) => {

// 解析url
console.log("🔵 Receive request: "+req.url);
let urlObject = url.parse(req.url,true);
console.log(urlObject);
let queryObject = urlObject.query;
console.log(queryObject);

// 這裡只示範 GET,其他方法自行擴充
if (req.method == 'GET' && urlObject.pathname == '/get') {

http.get(queryObject["target"], (resp) => {
let data = '';
resp.on('data', (chunk) => {
data += chunk;
});
resp.on('end', () => {
// 轉打成功後,自行加上 CORS 允許的 Header
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) // 使用 NodeJS 的站台轉打 API
.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'; //'192.168.110.147';
const port = 3000; //80;

// 網站
// http://localhost:3000/abc.html

// Api
// http://localhost:3000/redirect/https://httpbin.org/get

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{

// req.url = /redirect/https://httpbin.org/get
let url = req.url.slice(10);
// url = https://httpbin.org/get
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);
//console.log("🔵 Request Content-Type: "+req.headers["content-type"]);
//console.log("🔵 Request Authorization: "+req.headers["authorization"]);

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);
//console.log('🟢 response.body: '+ response.body);

res.statusCode = response.statusCode;

//如果網站站台與轉跳站台是不同的站台(不符合 Same-Origin Policy),就需要加上下面這兩條
//res.setHeader('Access-Control-Allow-Origin', '*');
//res.setHeader('Access-Control-Allow-Credentials', false);

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) // 使用 NodeJS 的站台轉打 API
.then(response => response.text())
.then(result => {
console.log(result);
})
.catch(error => {
console.log('error', error);
});
}
</script>
</body>
</html>