1531 字
8 分鐘
[HiPKI Local Server] /popupForm 解決 JavaScript CORS 問題 | 深入淺出自然人憑證
2024-01-03
無標籤

首頁 > 系列文 > 自然人憑證開發筆記

當我想透過前端存取 HiPKI Local Server 的時候,會遇到跨域存取問題。本篇將說明如何來解決。


跨來源資源共用(CORS)#

前幾篇提到要取得卡片資訊可以到 http://127.0.0.1:61161/pkcs11info?withcert=true。在自己網站透過 fetch 該路徑會出現下圖的錯誤,因為我們的網站與 localhost 是不同網域,沒辦法做跨域存取,被瀏覽器擋下來。

原因是因為 CORS,CORS 代表跨來源資源共用(Cross-Origin Resource Sharing)。它是一種瀏覽器的安全性機制,用於控制網頁應用程式中的網頁來自不同源的資源是否可以被請求。網頁中的 JavaScript 通常僅能夠請求與包含該 JavaScript 檔案的網頁相同源的資源。「相同源」是指兩個網址的協議(通常是 http 或 https)、域名和埠(如果有指定埠)都相同。這是出於安全性考慮,以防止惡意網站盜用用戶在其他網站上的資源。


常見的 CORS 解決辦法#

解決 CORS(Cross-Origin Resource Sharing)的問題有幾種方法:

  1. 伺服器端處理: 在伺服器端進行相應的配置,以允許跨來源請求。你可以在 HTTP 回應標頭中添加特定的 CORS 相關標頭,例如 Access-Control-Allow-Origin
  2. 代理伺服器: 在你的伺服器和外部伺服器之間設置一個代理伺服器。客戶端只和代理伺服器通信,而代理伺服器負責轉發請求到外部伺服器。這樣,由於同源政策僅適用於瀏覽器,伺服器間的通信不受影響。
  3. 使用 JSONP(僅限 GET 請求): JSONP 是一種繞過同源政策的方法,但僅適用於支持 JSONP 的 API 和僅支持 GET 請求的情況。請確保你的 API 支持 JSONP 並遵從安全最佳實踐。

上述方法各有優勢和限制,選擇取決於你的應用程式需求和伺服器設置。伺服器端處理是最常見和最具靈活性的方法,但可能需要你有控制伺服器的權限。


postMessage 解決 CORS#

除了上面幾種辦法,還可以在前端使用 postMessage 來解決 CORS。postMessage 是一種 HTML5 提供的跨窗口通訊機制,可以用來解決前端的跨來源請求(CORS)問題。這種方法通常被用於在不同的瀏覽器窗口或 iframe 之間進行通訊。

基本思想是,一個窗口(或 iframe)向另一個窗口(或 iframe)發送訊息,並且在接收訊息的窗口中處理相應的邏輯。這樣,不同源的窗口之間可以協同工作而無需依賴同源政策。

以下是使用 postMessage 解決 CORS 問題的基本步驟:

1. 在發送窗口(Sender Window)中:

  • 使用 postMessage 方法將訊息發送到目標窗口。這個方法接受兩個參數:訊息內容和目標窗口的來源(Origin)。
const targetWindow = window.open("https://target-domain.com")
// targetWindow 或者指定特定 iframe 的 window
targetWindow.postMessage('Hello, other window!', '*');

2. 在接收窗口(Receiver Window)中:

  • 在目標窗口中,需要添加一個監聽器以處理從發送窗口接收到的訊息。
  • 監聽 message 事件,檢查訊息的來源,確保它是來自預期的窗口。如果是,則執行相應的邏輯。
window.addEventListener('message', (event) => {
// 檢查訊息的來源是否符合預期的來源
if (event.origin === 'https://sender-domain.com') {
// 處理接收到的訊息
console.log('Received message:', event.data);
}
});

這種方法可以用於解決跨域的問題,但要確保在發送和接收窗口之間建立了安全的通信。因此,確保在發送訊息時指定正確的目標窗口的來源,並在接收訊息時驗證訊息的來源,以防止潛在的安全漏洞。


HiPKI Local Server 的 popupForm#

HiPKI Local Server 的 popupForm 是以 postMessage 解決跨域存取問題,溝通流程如下:

  1. 在網站會需要先呼叫 window.open 彈出 popupForm 視窗(http://localhost:61161/popupForm)
  2. popupForm 視窗開啟後,會回傳 getTbs 訊息到網站,代表呼叫成功
  3. 網站收到 getTbs 後,將要執行的指令傳送到 popupForm 視窗
  4. popupForm 視窗將執行結果與資料傳送回網站


popupForm 程式碼#

以下是 popupForm 程式碼,可以了解業務邏輯,與交換資料的方法。由於 popupForm 在回傳資料使用 window.opener,因次只能用在彈跳小視窗,沒辦法把 popupForm 放在 iframe 內。

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
</head>
<body>
<h2><span id="actionText"></span></h2>
<img src="waiting.gif" />
<form name="DemoForm" action="sign" method="POST">
<input type="hidden" name="tbsPackage" />
</form>
<script type="text/javascript">
function receiveMessage(event) {
var matchstr = "(.+)($|:\\d+$)";
if (event.origin.match(matchstr) == null) {
var ret = {};
ret["func"] = "sign";
ret["ret_code"] = 0x7600000f;
ret["message"] = "禁止存取的網域";
window.opener.postMessage(JSON.stringify(ret), "*");
window.close();
return;
}
var ret = JSON.parse(event.data);
var actionText = document.getElementById("actionText");
if (ret.func) {
var action = null;
var actionFun = null;
if (ret.func == "MakeSignature") {
action = "sign";
actionFun = "sign";
actionText.innerHTML = "簽章中";
} else if (ret.func == "umakeSig") {
action = "umakeSig";
actionFun = "umakeSig";
actionText.innerHTML = "簽章中";
} else if (ret.func == "GetUserCert") {
action = "pkcs11info?withcert=true";
actionFun = "pkcs11info";
actionText.innerHTML = "憑證讀取中";
} else if (ret.func == "DecryptData") {
action = "decrypt";
actionFun = "decrypt";
actionText.innerHTML = "解密中";
} else if (ret.func == "writecert") {
action = "writecert";
actionFun = "writecert";
actionText.innerHTML = "寫憑證中";
} else if (ret.func == "CheckEnvir") {
action = "pkcs11info";
actionFun = "pkcs11info";
actionText.innerHTML = "資料讀取中";
} else if (ret.func == "changeUserPINCode") {
action = "CardManagement/sendCardCommand";
actionFun = "changeUserPINCode";
actionText.innerHTML = "密碼變更中";
} else if (ret.func == "makeCsr") {
action = "makeCsr";
actionFun = "makeCsr";
actionText.innerHTML = "製作憑證請求檔中";
} else if (ret.func == "signDetached") {
action = "signDetached";
actionFun = "signDetached";
actionText.innerHTML = "簽章中";
}
if (action != null) {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function () {
if (xhttp.readyState == 4) {
if (xhttp.status == 200) {
window.opener.postMessage(xhttp.responseText, "*");
window.close();
} else {
window.opener.postMessage(
'{"func":"' +
actionFun +
'","ret_code":' +
xhttp.status +
"}",
"*"
);
window.close();
}
}
};
xhttp.open("POST", action, true);
xhttp.send("tbsPackage=" + event.data);
} else {
window.opener.postMessage(
'{"func":"' + actionFun + '","ret_code":' + 0x7600000e + "}",
"*"
);
window.close();
}
}
}
window.addEventListener("message", receiveMessage, false);
var tbsData = {};
tbsData["func"] = "getTbs";
var msg = JSON.stringify(tbsData);
var postTarget = window.opener;
postTarget.postMessage(msg, "*");
</script>
<br />
</body>
</html>

結語#

透過 postMessage 可以與 HiPKI Local Sever 進行交換資料,解決 CORS 問題,下一篇將說明如何與透過 postMessage 與 popupForm 操作自然人憑證。

▶ 閱讀更多:深入淺出自然人憑證