因為blog標題原因, 本文重新發布了.
Title/ 瀏覽器跨域(CrossOrigin)請求的原理, 以及解決方案詳細指南 #flight.Archives011
序:
最近看到又有一波新的創作活動了, 官方給出的話題中有一個"為什么XHR不能跨域請求資源", 看起來像一道很簡單的面試題哈哈哈
作為前端愛好者, 不妨借此機會研究一下跨域, 總結一篇文章出來. 於是我就安排了flight.A011的FocusList#寫作計划.簡介:
一篇最簡潔高效的跨域請求指南 made with ❤ by忘我思考
注:
- 本文中XHR表示
XMLHttpRequest
對象.- CORS表示
Cross-origin resource sharing
, 譯為跨域資源共享.- 請求頭即 Request Headers, 響應頭即 Response Headers.
Tag/ 瀏覽器跨域(CORS)限制介紹: 為什么要限制XHR跨域請求, 如何觸發跨域限制
大佬們可以直接看下一章解決方案, 這里會解釋得詳細一點方便新手入門(誰都曾是萌新嘛), 不了解CORS的來這補補課吧~
跨域, 就是站A的請求要訪問站B的服務器.
如果 協議
(protocol), 域名
(domain), 端口
(port) 三者有任意一處不同, 瀏覽器就認為這是兩個不同的網站, 會觸發跨域安全限制.
詳細的說, 就是:
[https://][example.com][:80]/path?query#heading-1
只有打方括號內的內容完全一致才是"同源", 也就是說同源請求URL中只有端口后的內容是可以不一致的, 否則就是跨域
請注意: https協議的默認端口是443, http協議默認端口是80. 默認端口可以省略不寫.
也就是說 https://example.com:443/... 和 https://example.com/... 是同源, http同理.
->> 為什么瀏覽器要有跨域限制
設想這樣的場景:
我是不知名小Blog主, 你現在在訪問我的博客網站, 然后我突發奇想, 想要用自動JS程序發送請求到 weibo 的服務器, 讓你自動關注我的微博.
如果這能實現, 那瀏覽器就太不安全了, 網站主可以在后台做很多危險請求(如獲取你在 weibo 的登錄信息等).
所以瀏覽器都有一個跨域限制, 阻止不安全的第三方內容請求.
舉個例子:
如果我在Segmentfault官網的Console訪問我的博客園首頁, 就會觸發XHR跨域限制, 禁止讀取返回內容.
而另一個情景:
你是一個小站長, 在網站使用Vue.js, 於是添加了一個CDN腳本(如 unpkg
等)
結果瀏覽器錯誤的因為跨域限制自動攔截請求導致你無法使用CDN腳本.
這顯然是不合理的, 所以現在的瀏覽器都會智能根據對方(網站B)的 響應頭
中的 Access-Control-Allow-Origin
屬性, 來判斷你是否可以讀取對方返回的內容.
比如 unpkg
就把 Access-Control-Allow-Origin
屬性設為了 *
, 方便外域調用CDN.
像React也建議你驗證CDN的請求頭屬性, 避免使用出現跨域限制問題.
也就是說, 你能不能訪問第三方資源, 其實是由對方(響應頭)決定的(這其實也是合理的, 按道理請求別人的東西就要經過允許呀).
對方服務器可以通過你的 cookie
referer
等 請求頭
參數來判斷請求的安全性, 決定是否返回對應內容.
注意: 純前端是不能纂改 referer
請求頭的, 否則會報錯.
->> 瀏覽器對請求的詳細划分方法: 簡單請求和復雜請求
不過瀏覽器的跨域限定其實有詳細的划分, 這里有必要提到一下:
請求分為 簡單請求
和 復雜請求
兩種.
對於 簡單請求
, 要滿足以下所有條件("與"關系).
- 請求方法是
GET
,POST
,HEAD
之一. - 人為設置的請求頭不超過以下屬性:
Accept
,Accept-Language
,Content-Language
. Content-Type
屬性值是text/plain
,multipart/form-data
,application/x-www-form-urlencoded
之一.- 請求中的任意
XMLHttpRequestUpload
對象均沒有注冊任何事件監聽器;XMLHttpRequestUpload
對象可以使用XMLHttpRequest.upload
屬性訪問. - 請求中沒有使用
ReadableStream
對象.
其中前3點比較常用. 如果請求不滿足以上5個條件的任意之一, 則是復雜請求.
瀏覽器區分 簡單請求
和 復雜請求
是為了兼容表單(form), 因為歷史上表單一直可以發出跨域請求.
->> XHR請求在瀏覽器里是如何被發出去的? 瀏覽器對簡單請求和復雜請求的不同處理方式
這里我覺得阮一峰的文章已經寫得很好了(在文末有鏈接), 所以, 如有雷同(不對, 就是雷同)... 不要覺得我是在抄襲, 只是按照他的
創意共享3.0許可證
合法借鑒哈.
-
瀏覽器對簡單請求的處理方式!
如果一個XHR滿足簡單請求的所有判定, 那它會被這樣發出去:// 直接發送包含Origin請求頭的XHR請求到對方服務器 瀏覽器會自動在頭信息之中, 添加一個Origin字段. 如: Origin: http://api.gold.xitu.io (現在掘金被字節收取就變味了, 好懷念Ming在的時間啊...) Origin是"起源"的意思, 包含協議 + 域名 + 端口, 會告訴對方請求是從哪個源發出的. 如果Origin指定的源不在許可范圍內(比如在cnblogs中添加訪問Bilibili的代碼), 對方會返回一個不包含Access-Control-Allow-Origin的響應頭, 瀏覽器就知道出錯了, 從而拋出一個錯誤, 被XMLHttpRequest的onerror回調函數捕獲. 注意: 這種錯誤無法通過狀態碼識別, 因為HTTP回應的狀態碼有可能是200. 如果Origin指定的域名在許可范圍內, 服務器返回的響應, 會多出幾個頭信息字段: Access-Control-Allow-Origin: http://api.bbb.com Access-Control-Allow-Credentials: true Access-Control-Expose-Headers: FooBar 上面這些以 Access-Control- 開頭的字段, 都表示對方服務器對請求的訪問限制. 下面介紹一下這三個屬性: 1. Access-Control-Allow-Origin(必須屬性): 可以是URL, 表示對請求時Origin字段的值的限制, 也可以是 *, 表示接受任意域名的請求, 像CDN代碼存儲服務之類的. 2. Access-Control-Allow-Credentials(可選): 一個布爾值, 只能為true, 表示是否允許發送Cookie. 默認情況下(不填該屬性), 則Cookie不包括在跨域請求之中. 3. Access-Control-Expose-Headers(可選): 跨域請求時, XHR對象的getResponseHeader()方法只能拿到6個基本字段: Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma. 如果想拿到其他字段, 必須在Access-Control-Expose-Headers中指定. 上面的例子指定getResponseHeader('FooBar')可以返回FooBar字段的值. ->> Details: withCredentials 屬性 上面說到, 跨域請求默認不發送Cookie和HTTP認證信息. 如果要把Cookie發到服務器, 一方面要服務器同意, 指定Access-Control-Allow-Credentials字段為true. 另一方面, 網站主必須在AJAX請求中打開withCredentials屬性. 像這樣: var xhr = new XMLHttpRequest(); xhr.withCredentials = true; 否則, 即使服務器同意發送Cookie, 瀏覽器也不會發送. 這種情況下服務器即使要求設置Cookie, 瀏覽器也不會處理. 但是, 如果省略withCredentials設置, 有的瀏覽器還是會一起發送Cookie. 這時, 可以顯式關閉withCredentials: xhr.withCredentials = false; 需要注意的是, 如果要發送Cookie, Access-Control-Allow-Origin就不能設為星號, 必須指定明確的, 與請求網頁一致的域名. 同時, Cookie依然遵循同源政策, 只有用服務器域名設置的Cookie才會上傳, 其他域名的Cookie並不會上傳, 且(跨域的)原網頁代碼中的document.cookie也無法讀取對方服務器域名下的Cookie.
-
瀏覽器對復雜請求的處理方式!
// 先發送預檢請求(preflight), 請求通過后, 發送XHR到對方服務器 瀏覽器會先詢問服務器, 當前網頁所在的域名是否在服務器的許可名單之中, 以及可以使用哪些HTTP動詞和頭信息字段. 只有得到肯定答復, 瀏覽器才會發出正式的XMLHttpRequest請求, 否則就報錯. 來了, 這是一段JS代碼! var url = 'http://api.aaa.com/cors'; var xhr = new XMLHttpRequest(); xhr.open('PUT', url, true); xhr.setRequestHeader('X-Custom-Header', 'value'); xhr.send(); 可以看到使用了 PUT 方法, 還人為設置了X-Custom-Header請求頭, 所以這是個復雜請求qwq... 所以瀏覽器會發送一個預檢請求, 預檢請求的請求頭像這樣: OPTIONS /cors HTTP/1.1 Origin: http://api.bbb.com Access-Control-Request-Method: PUT Access-Control-Request-Headers: X-Custom-Header Host: api.aaa.com ...(以及一些更多的無關請求頭) 預檢請求使用了 OPTIONS 方法, 表示這個請求是用來詢問的. 請求頭中, 關鍵字段是Origin, 表示請求來自哪個源. 而 Access-Control-Request-Method 和 Access-Control-Request-Headers 你一看就明白是什么了, 分別是請求方法和人為設置了哪些屬性. 服務器受到預檢請求后, 判斷請求頭是否OK, 如果OK就可以做出回應啦! 響應頭中關鍵信息是這幾個: Access-Control-Allow-Origin: http://api.bbb.com Access-Control-Allow-Methods: GET, POST, PUT Access-Control-Allow-Headers: X-Custom-Header 解釋一下~ 1. Access-Control-Allow-Origin: 表示允許的請求源, 也可以是*表示隨便什么網站. 2. Access-Control-Allow-Methods: 就是允許的方法啊! 3. Access-Control-Allow-Headers: 允許設置的請求頭參數. 好了, 那如果服務器不同意預檢請求怎么辦呢~ 會返回一個正常的HTTP回應, 但是沒有任何CORS相關的頭信息字段. 這時,瀏覽器就認為, 服務器不同意預檢請求. 因此觸發一個錯誤, 被XMLHttpRequest對象的onerror回調函數捕獲. 控制台會打印出如下的報錯信息: XMLHttpRequest cannot load http://api.aaa.com. Origin http://api.bbb.com is not allowed by Access-Control-Allow-Origin. 服務器回應的其他CORS相關字段如下: Access-Control-Allow-Methods: GET, POST, PUT //所有支持的方法 Access-Control-Allow-Headers: X-Custom-Header //如果瀏覽器請求包括Access-Control-Request-Headers字段, 則Access-Control-Allow-Headers字段是必需的, 表示支持的所有請求頭字段. Access-Control-Allow-Credentials(可選): true //也是表示Cookie Access-Control-Max-Age(可選): 1728000 //表示本次預檢請求的有效期, 單位為秒. 比如這個就設定了本次請求有效期是20天(1728000秒), 期間不再次發送請求.
->> 案例: 解決一個博客音樂播放器的跨域問題
瀏覽器的安全限制其實也不是完美無缺的, 前端的話如果不涉及驗證碼, 登錄, 加密之類的復雜操作, 通過一些技術手段想要突破跨域限制, 也不是沒有可能.
無后端支持也是可以實現跨域的, 不過一般要在用戶知情的情況下配合操作.
其實就是你可以實現一些跨域請求, 但是是否能夠獲取請求內容, 本來是由對方服務器請求頭決定的, 現在決定權在於用戶.
比如你想要在你的博客中添加一個音樂播放器, 播放QQ音樂的內容.
而眾所周知, 騰訊那邊的盜鏈(可以理解為跨域)限制是很強的!
所以一般我的實現是配合后端, 也就是訪問QQ音樂網址, 把音樂通過瀏覽器控制台Network欄獲取請求資源URL下載保存到自己的服務器.
然后直接訪問自己的服務器獲取音樂資源(QQ音樂一般是m4a, 網易一般是mp3, 不過順便一提網易取消了盜鏈保護, 所以一般可以直接用網易服務器, 只是有的音樂有版權保護就不能外域播放了).
而如果要純前端實現跨域音樂播放則可以使用 <iframe>
, Chrome參數纂改, Chrome插件等方法來實現盜鏈.
這些《神奇》的方法也就是下一章會詳細介紹的內容啦~ [奸笑]
Tag/ 重頭戲開始了! 突破跨域限制的9種解決方案
-
使用
iframe
實現
iframe
標簽可以將跨域內容嵌入你的網頁, 比如在博客中添加一個Bilibili視頻 或者 Codepen Demo的iframe
.
不過iframe
有兩個缺點:- 很多服務器會直接拒絕跨域
iframe
請求, - 跨域訪問
iframe
中的內容會遭到限制qwq.
放幾張圖體會一下:
如果在阮一峰的Blog中插入 iframe(cnblogs博文), 會被拒絕qwq...
而如果在站內插入iframe則沒有問題, 看來博客園為了安全考慮目前關閉了跨域請求.
而Bilibili就比較開放了, 目前可以插入iframe.
不過如果你想通過iframe讀取或者篡改Bilibili網頁的內容也是不可以的啦~
所以這終究只是一個向用戶"展示內容"的方法而已, 限制很多, 如果要讀取/篡改內容, 還是得同源才行~
- 很多服務器會直接拒絕跨域
-
使用CORS方法(跨域資源共享)
這個方法也就是第一章中介紹的, 很詳細了. 不用再重復一遍啦.但是這種方法也是受到對方服務器的種種限制, 不能為所欲為qwq...
-
搞一個瀏覽器插件配合(終極大招, 直接無視跨域限制)
嘿嘿嘿! 沒想到吧! 竟然還有這種方法?!
網上這方面的文章一般都不會想到有這種神奇也是非常強大的方法, 不過這也是非常實用的.
如果要我來, 很可能會采用這種方法, 可以不用后端支持進行跨域!
如果你的網站面向的對象主要是愛好破解的極客/有探索精神的玩家, 可以向他們推薦你的Chrome插件, 或者是一段油猴腳本.
比如讓用戶簡單的copy&paste添加一段你的油猴(TemperMonkey)腳本, 就可以實現跨域請求啦!
(現在油猴/Chrome插件在極客/開發者手里也已經很普及了吧~, 想想你裝了多少插件...
(當然一個都沒裝的勿噴!不過現在Chrome插件那么多, 可是你... 能不能獨立開發一個擴展插件呢? 恐怕很難吧...
(其實如果你懂前端, 插件開發也不難, 只要了解Chrome插件專門的一些方法就行了.
那么這里就簡單介紹一下實現方法, 如果你想開發一個完整實用的插件, 網上也有不少文檔了. (Chrome Developer中也有詳細的介紹不如先學習一下別人是怎么玩的. 我們隨便打開一個別人開發的Chrome插件的文件夾(我打開的是Tempermonkey), 是這幅景象:
其中有一個
manifest.json
文件, 和你的插件支不支持跨域密切相關, 比如油猴的長這樣:{ "background": { "page": "background.html" }, "browser_action": { "default_icon": { "16": "images/icon_grey16.png", "19": "images/icon_grey19.png", "24": "images/icon_grey24.png", "32": "images/icon_grey32.png", "38": "images/icon_grey38.png" }, "default_popup": "action.html", "default_title": "Tampermonkey" }, "commands": { "open-dashboard": { "description": "Open dashboard" }, "open-dashboard-with-running-scripts": { "description": "Open dashboard with the current tab's URL used as filter" }, "open-new-script": { "description": "Open new script tab" }, "toggle-enable": { "description": "Toggle enable state" } }, "content_scripts": [ { "all_frames": true, "js": [ "rea/common.js", "content.js" ], "matches": [ "file:///*", "http://*/*", "https://*/*" ], "run_at": "document_start" } ], "content_security_policy": "script-src 'self'; object-src 'self';", "default_locale": "en", "description": "The world's most popular userscript manager", "differential_fingerprint": "1.2ae827c5b75f9e167b3ed0bf65d0c330028dd0acf93a84a8f0f2d7e096024ba3", "icons": { "128": "images/icon128.png", "32": "images/icon.png", "48": "images/icon48.png" }, "incognito": "split", "key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCwuYtg7kY2YyNieOkV9pK/qcXwUXu0CFUO0zU6DLAGAJZK7zxrHlwg9a+zFH7CXqgH7zSfRce9KiYOHJaLPBXM66uPCliiQ6Q+bFaNZx1FxLXkZTFnlyPh8kkwwohLJeSQ9NQXqEfeTepDj5BRufAR48az0MC5aUTEj+fFXbzX7QIDAQAB", "manifest_version": 2, "minimum_chrome_version": "64.0.0.0", "name": "Tampermonkey BETA", "offline_enabled": true, "optional_permissions": [ "downloads" ], "options_page": "options.html", "options_ui": { "chrome_style": false, "open_in_tab": true, "page": "options.html" }, "permissions": [ "notifications", "unlimitedStorage", "tabs", "idle", "webNavigation", "webRequest", "webRequestBlocking", "storage", "contextMenus", "chrome://favicon/", "clipboardWrite", "cookies", "declarativeContent", "\u003Call_urls>" ], "short_name": "TM BETA", "update_url": "https://clients2.google.com/service/update2/crx", "version": "4.14.6142" }
這個是Chrome插件的靈魂, 設置了各種屬性, 包括Chrome插件的圖標, 點擊Chrome插件圖標打開的popup頁面等...
這里和跨域相關的只有一個: 就是permissions
屬性.這個屬性設置了插件有哪些權限, 比如可以隨意訪問哪些網站.
比如油猴的, 就有一個
"\u003Call_urls>"
在permissions
屬性里.
其中\u003C
其實就是<
的Unicode編碼, =<all_urls>
, 也就是這個插件定義的JS代碼可以隨意訪問各個網站而在用戶眼中就是這幅景象:
關於Chrome插件配合網站實現跨域的文檔, 可以參考這篇 https://wizardforcel.gitbooks.io/chrome-doc/content/23.html, 總結得很完整了.
而我搜索的時候, 驚喜的發現, 還有人開發了這樣一個插件(不是我開發的啊):
名字叫 "Allow CORS", 挺好用的, 初衷是幫助開發者跨域調試網站.用戶評價也挺不錯的.
網址就是 https://chrome.google.com/webstore/detail/allow-cors-access-control/lhobafahddgcelffkeicbaginigeejlf
用途當然就是和名字一樣, 可以解除跨域限制. 所以你只要讓用戶輕輕一點去裝上這個插件, 就可以無視跨域限制啦!
不過Chrome因為Google原因, 在國內有限制, 面對一般的用戶Chrome也支持上傳本地包來添加插件, 也很簡單,下載->一點打開開發者模式->一拖添加插件
就OK啦!好了這個方法是真的tql.
-
配合客戶端本地代理VPN(終極大招)
看完第三個方法, 我覺得其他的都要退下了, 畢竟第3個方法應該是最簡單的了.
第四個原理差不多, 不過開發起來是真的麻煩啊.
要讓用戶裝一個系統應用, 本地VPN, 就像Charles, Fiddler這樣.
然后在本地自動修改 Referer 和 Origin 等請求頭, 假裝一個正常的用戶請求.
總之不是一個優秀的解決方案吧. -
配合后端(終極大招)
本文着重純前端開發, 我也不太懂后端(基本的php除外, 用來建博客要用到)
這個方法也是很實用的, 但是不詳細介紹了.
就是針對不涉及用戶在瀏覽器中cookie的情況, 通過后端語言(如Java)正常請求來獲取資源.
再返回到前端, 請求一個本站資源(其實轉自對方站).比如第一章中提到的音樂播放器, Github上就有人用配合后端實現了! 還有1.9k個star!
不過這個解析下載音樂因為侵權了, 所以2018年起暫停維護了, 不過下載下來依然可以使用.不過只能下載免費音樂, 付費聽的音樂會ErrorHappensQwQ.
因為只是后端搜索解析一下, 音樂資源依然是來自對方服務器(所以QQ音樂盜鏈保護還是生效, 不能聽啊... 網易就沒事, 畢竟網易一向開放)
想了解一下的, 可以啪的點進去: https://github.com/maicong/music -
JSONP方法
對於script
標簽的資源, 瀏覽器是沒有跨域限制的! (對方按照referer/origin屬性不返回數據的情況除外)比如網上就有這樣的實現:
<!-- 原生JS實現 --> <script type="text/javascript"> window.jsonpCallback = function(res) { console.log(res); }; </script> <script src="http://localhost:80/api/jsonp?&cb=jsonpCallback" type="text/javascript" ></script> <!-- JQuery Ajax實現 --> <script src="https://cdn.bootcss.com/jquery/3.5.0/jquery.min.js"></script> <script> $.ajax({ url: "http://localhost:80/api/jsonp", dataType: "jsonp", type: "get", data: { msg: "hello" }, jsonp: "cb", success: function(data) { console.log(data); } }); </script>
嗯, 就是這樣用的, 很簡單獲取Script內容, 不過也只支持Script qwq...
-
WebSocket實現
WebSocket是HTML5的一個特性, 可以讓瀏覽器化被動為主動發送內容, 適合實時直播等場景(比如字節的掘金直播就用到了這種方法)WebSocket其實雖然有用(本來就沒有CORS的跨域限制), 但其實也沒有辦法獲取HTTP資源QAQ.
所以這個方法也不推薦吧!
-
window.postMessage() 方法
這個方法可以安全地實現跨源通信!不了解這個方法, 可以去這里補補課~ ->> https://developer.mozilla.org/zh-CN/docs/Web/API/Window/postMessage
在《HTML5CSS3權威指南》中也有介紹, 但是在實戰中這個方法使用也比較少哈~
這個方法可以進行不同界面的消息傳遞:
比如多窗口之間, 網頁和其iframe之間.比如QQ音樂就用到了這種方法.
如果你開啟了多個播放頁面, QQ音樂會通過這個方法多界面之間通信, 自動暫停之前的播放頁, 避免同時播放多個音樂.當然正因為它可以 "安全地" 實現跨源通信, 所以這個通信方法也是需要對方服務器認可的. 所以如果要做不正經的事(對方不允許的情況下強行獲取資源)也沒有用啊...
-
使用"允許跨域"方法打開瀏覽器
既然是瀏覽器搞的限制, 能不能關閉這個限制呢?
是可以的!就以這種方法打開(Windows, xxx是你的data目錄):
C:/.../.../path/chrome.exe --disable-web-security --user-data-dir=xxxx
Mac也可以! 也是一樣的.
--disable-web-security --user-data-dir=~/Downloads/chrome-data
就是這樣, 不過要重啟Chrome才行, 用戶可能不願意, 所以還是插件這個方法我最推薦.
當然, 如果不正當獲取了第三方內容是可能會有律師函警告的~
所以說, 本文只是對跨域的介紹和技術上的探索與研究, 僅此而已. (你懂的[doge]
->> Details
附錄 - CORS請求與JSONP的比較
CORS與JSONP的使用目的相同, 但是比JSONP更強大.
JSONP只支持GET請求, CORS支持所有類型的HTTP請求. JSONP的優勢在於支持老式瀏覽器, 以及可以向不支持CORS的網站請求數據.
->> Reference link
MDN中文文檔 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS
MDN 英文文檔 https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
阮一峰 https://www.ruanyifeng.com/blog/2016/04/cors.html
秋風的筆記 https://segmentfault.com/a/1190000022398875
思否 - 安靜de沉淀 https://segmentfault.com/a/1190000011145364
掘金 - 小銘子 https://juejin.cn/post/6844903882083024910
掘金 - JackySummer https://juejin.cn/post/6861553339994374157
Chrome Developers https://developer.chrome.com/docs/extensions/mv3/xhr/
Html5Rocks https://www.html5rocks.com/en/tutorials/cors//
博客園 - 小火柴的藍色理想 https://www.cnblogs.com/xiaohuochai/p/6036475.html
知乎 - 單純前端能否解決跨域問題? https://www.zhihu.com/question/302245173
MDN - XHR介紹 https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest
MDN - XHR介紹(英文) https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest
MDN - Access-Control-Allow-Origin 屬性介紹 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
MDN - Access-Control-Allow-Origin 屬性介紹(英文) https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
StackOverflow - How does Access-Control-Allow-Origin header work? https://stackoverflow.com/questions/10636611/how-does-access-control-allow-origin-header-work
StackOverflow - CORS with XMLHttpRequest not working https://stackoverflow.com/questions/25296455/cors-with-xmlhttprequest-not-working
W3 - CORS Enabled https://www.w3.org/wiki/CORS_Enabled
->> Version History
現在版本為V1.0 (對了博客園的自定義CSS我沒有好好研究, 所以最后渲染結果(UI)可能不盡人意, 所以下一版應該會添加自定義CSS美化一下的!)
詳見 Github(@flightmakers)2021.8.23 發布V1.0