謝智敏 王婷 郭倩玲

收稿日期:2023-04-15
基金項目:北京高校圖書館研究基金項目成果(BGT2021003)
DOI:10.19850/j.cnki.2096-4706.2023.22.014
摘? 要:文章討論了一種新的基于瀏覽器遠程訪問網絡資源的技術——Service Worker,以解決傳統基于服務器端WebVPN存在的問題。WebVPN是一種主流遠程訪問網絡資源的方式,但是傳統WebVPN技術存在難以處理動態URL和跨域訪問,以及服務器CPU占用高等問題。為了解決這些問題,文章詳細介紹了Service Worker技術的基本原理,以及如何使用Service Worker技術解決動態URL問題。同時,文章還詳細給出了解決方案的示例代碼,幫助讀者更好地理解Service Worker技術的實際應用。在WebVPN實踐中,Service Worker技術的應用有效地提升了遠程訪問網絡資源的速度和用戶體驗,該技術有望成為WebVPN領域的一項重要技術。
關鍵詞:Service Worker;Hook技術;WebVPN;動態URL;CPU占用
中圖分類號:TP393? 文獻標識碼:A? 文章編號:2096-4706(2023)22-0063-07
Application of Service Worker Technology in WebVPN Practice
XIE Zhimin, WANG Ting, GUO Qianling
(Beijing University of Chemical Technology Library, Beijing? 100029, China)
Abstract: To address the traditional problems existing in WebVPN based on server-side, the paper discusses a new technology based on browser remote access to network resources—Service Worker. WebVPN is a mainstream way to access network resources remotely, but the traditional WebVPN technology is difficult to deal with the problems of dynamic URL and cross-domain access, as well as the high occupancy of server CPU. In order to address these problems, this paper describes the basic principles of Service Worker technology in detail and how to use Service Worker technology to solve dynamic URL problems. At the same time, this paper also gives the sample code of the solution to help readers better understand the practical application of Service Worker technology. In WebVPN practice, the application of Service Worker technology effectively improves the speed and user experience of remote access to network resources. This technology is expected to become an important technology in the WebVPN field.
Keywords: Service Worker; Hook technology; WebVPN; dynamic URL; CPU usage
0? 引? 言
WebVPN是一種遠程訪問網絡資源的技術。WebVPN最初于1996年問世,旨在解決傳統VPN中的一些安全問題,隨著互聯網的普及,WebVPN技術得到了廣泛應用,已經成為遠程訪問網絡資源的一種主流方式。相比于傳統VPN,WebVPN的優勢在于使用門檻低,無須安裝客戶端軟件,更無須在軟件中設置復雜的配置,用戶只需在Web瀏覽器中登錄賬號密碼即可使用。WebVPN還提供多種認證方式,如用戶名/密碼、證書、智能卡等;同時它使用SSL/TLS協議加密傳輸數據,可以提供更好的安全性。此外,WebVPN的靈活性和便利性也是其優勢之一,它可以在不同的操作系統和設備上工作。
然而,WebVPN也存在一些缺點。首先,WebVPN連接速度通常較慢,因為SSL/TLS協議的加密過程會增加額外的延遲。其次,WebVPN不能實現某些功能,如無法實現復雜的網絡拓撲和資源共享等。再次,WebVPN對服務器CPU占用高。有以下情況:網頁中的URL有多種存在形式,既可以存在于HTML、CSS、JS文件中,也可以是動態加載的JSON、XML等格式的文本中,因此后端不僅需要處理HTML,還必須處理各種文本資源,這對服務器CPU占用較高;在對內容處理時,如解壓壓縮的數據等,也會占用較多服務器CPU[1]。
更重要的是,現在有些網站為了防止被其他服務器代理訪問,使用了.htaccess禁止反向代理、JS代碼判斷當前域名、php判斷當前域名[2],動態生成URL、動態獲取URL等技術[3],使得傳統的在線代理應用受到了一些限制。例如,當存在動態生成和獲取URL時,許多API與URL相關,字符串替換的方法無法繞過此類限制。
1? 在線代理處理動態URL的解決方案——Hook技術
Hook技術是指在程序執行過程中,動態修改或者替換原有的函數、方法或指令的技術。通過Hook技術,可以在不改變程序源代碼的情況下,對程序行為進行定制化的修改,從而實現一些特殊的功能或調試目的[4]。在WebVPN中,Hook技術可以用于實現諸如HTTP流量的攔截、篡改和重定向,從而實現各種網絡代理的功能。具體而言,Hook技術可以通過以下幾個步驟實現:
1)動態加載代碼庫:在運行時動態加載代碼庫,以便在程序執行過程中注入Hook代碼。
2)找到目標函數:根據函數名或函數地址等信息,找到目標函數所在的位置。
3)替換目標函數:通過Hook技術,將目標函數替換為自定義的函數,以便在函數執行時進行攔截和修改。
4)調用原函數:在自定義的函數中,可以通過調用原函數來實現對程序行為的監控和修改,從而達到網絡代理的目的。
Hook技術是WebVPN的關鍵技術之一,可以實現前端代理服務器和后端資源服務器之間的數據傳輸,提高了WebVPN的效率和安全性。WebVPN中常用的Hook技術有兩大類,即API Hook和DOM Hook。
1.1? API Hook
API Hook技術是指通過重寫API函數或屬性,實現對輸入參數或返回值的調整[5],從而實現WebVPN數據傳輸的目的。例如,在WebVPN中,可以通過改寫window.open()函數的方式,來攔截所有彈出窗口的請求,并加上前綴,將其重定向到代理服務器上。如在1.com的網頁里,通過改寫open函數可以讓document.domain返回2.com。改寫后的open函數在每次調用時給URL加上前綴https://1.com/proxy?,使原始網頁彈出的任何訪問請求都是站點1.com的頁面。另外還可以通過重寫document.domain的屬性,實現將原本2.com的網頁運行于1.com站點下,而腳本仍然獲得的是2.com的數據。通過API Hook技術,可以攔截和修改絕大多數與URL相關的API,從而實現數據傳輸和URL的調整。
1.2? DOM Hook
DOM Hook技術是指通過在前端注入腳本,攔截DOM元素的創建和渲染過程,將絕對路徑的URL調整成代理服務器的站點,從而實現數據傳輸和URL的調整[6]。例如,在WebVPN中,可以通過將前端腳本注入HTML代碼中,然后使用MutationObserver API來攔截DOM元素的創建和渲染過程,將絕對路徑的URL調整成代理服務器的站點。預設的前端腳本可以首先運行,通過MutationObserver,可以在DOM元素渲染前修改其屬性,將絕對路徑的URL調整為所設定的站點,以改變后續標簽的URL屬性[7]。盡管服務器返回的HTML里都是原始URL,但實際上資源是從所設定站點加載的,超鏈接指向的也是所設定的站點。這樣,前端擁有了API Hook和DOM Hook,后端則無須處理JSON、XML等資源,因為URL無論從何處獲取,最終都將傳遞給API或賦值給DOM的屬性[3]。
2? WebVPN中Service Worker技術的引入
然而有些API是無法攔截的,通常是瀏覽器本身的限制所致。例如location這一對象及其屬性均無法攔截重寫。另外由于MutationObserver只能攔截DOM元素,因此僅適用于HTML,如果CSS中也有URL地址,若想要正常顯示仍需要服務器端做特殊處理。然而即便是只處理其中一行代碼,服務器也要對流量進行解壓和再壓縮,當這種請求在并發訪問數量較高時,將會占用大量CPU資源。例如,我們在基于WebVPN開發高校電子資源訪問系統時,發現服務器的CPU資源在訪問高峰期會處于比較緊張的狀況,當并發用戶超過一定數量,會導致訪問延遲,致使用戶的使用體驗有所下降[8]。為了實現降低服務器CPU占用的目標,需要借助HTML5的一個新技術——Service Worker。
2.1? Service Worker技術簡介
Service Worker是一種基于JavaScript的瀏覽器技術,它在瀏覽器和服務器之間創建了一個中間層,可以攔截網絡請求并從緩存中提供響應,從而減少對網絡的依賴,它可以實現網頁功能的離線緩存,并在離線狀態下使用。
Service Worker可用于緩存網站的資源文件,如HTML文件、CSS文件和JavaScript文件,同時,它還支持諸如推送通知向用戶發送消息或提醒。換句話說,它充當了服務器與瀏覽器之間的中間人角色。如果網站注冊了Service Worker,它將攔截當前網站的所有請求,并根據編寫的相應判斷程序進行處理。如果需要向服務器發起請求,則將請求轉發給服務器,否則直接從緩存中返回響應,從而很大程度上提高了用戶的瀏覽體驗。
若在網站中注冊Service Worker,只需在index.html中添加下列命令:
/* 判斷瀏覽器是否支持Service Worker */
if ('ServiceWorker' in navigator) {
/* 當頁面加載完成就創建一個ServiceWorker */
window.addEventListener('load', function () {
/* 創建并指定對應的執行內容 */
navigator.serviceWorker.register('./serviceWorker.js', {scope: './'})
.then(function (registration) {
console.log('ServiceWorker registration successful with scope: ', registration.scope);
})
.catch(function (err) {
console.log('ServiceWorker registration failed: ', err);
});
});
}
注冊后,在指定的處理程序serviceWorker.js中書寫對應的安裝及攔截邏輯,即可安裝Service Worker,相關指令如下[9]:
/* 監聽安裝事件,install一般是被用來設置瀏覽器的離線緩存邏輯 */
this.addEventListener('install', function (event) {
/* 通過這個方法可以防止緩存未完成,就關閉service Worker */
event.waitUntil(
/* 創建一個名叫V1的緩存版本 */
caches.open('v1').then(function (cache) {
/* 指定要緩存的內容,地址為相對于跟域名的訪問路徑 */
return cache.addAll([
'./index.html'
]);
})
);
});
/* 注冊fetch事件,攔截全站的請求 */
this.addEventListener('fetch', function(event) {
event.respondWith(
// magic goes here
/* 在緩存中匹配對應請求資源直接返回 */
caches.match(event.request)
);
});
目前,主流瀏覽器如Edge、Chrome、Safari、Samsung Internet均已經支持Service Worker技術[10]。這為在WebVPN中廣泛運用該技術提供了良好的前提條件。
2.2? Service Worker在線代理訪問的實現
Service Worker是一個瀏覽器后臺運行的服務進程,它可以攔截當前站點產生的所有HTTP請求(甚至包括瀏覽器地址欄的訪問請求),并能控制返回結果,相當于瀏覽器內部的反向代理。Service Worker本質上充當Web瀏覽器與網絡(可用時)之間的代理服務器。這個API會攔截網絡請求并根據網絡是否可用來采取適當的動作、更新來自服務器的資源。它還提供入口以推送通知和訪問后臺同步API[11]。這樣就可以統一捕獲流量,無論URL是絕對路徑還是相對路徑,無論出現在HTML還是CSS,都能在Service Worker層進行攔截。Service Worker本身需要通過腳本安裝,服務端則不再需要往HTML中插入腳本。而且,Service Worker是后臺進程,一旦安裝可長期運行,即使網頁關閉,它仍在運行。所以只需讓用戶安裝一次就可以。當用戶首次訪問時,無論訪問什么路徑,服務端始終返回安裝頁面。之后,整個站點所有流量都被Service Worker接管。最終,所有的內容處理都可以由JavaScript來實現,服務端只需純粹轉發數據,甚至不用考慮解壓縮,從而大幅降低服務器CPU占用。
從運作原理不難知道,Service Worker在瀏覽器端即可實現API Hook、DOM Hook,而傳統方法需要在服務器端來完成,無論加載URL還是調用API,都可被Service Worker攔截和代理。然而,由于各種限制,仍然有特殊情況無法采用Hook技術簡單解決,可以通過下列方案加以解決。
2.2.1? 無法實現Hook的API解決方案
由于瀏覽器限制,有些API是無法重寫的,其中最典型的就是location,無論window.location還是document.location以及location對象中的成員,都是無法重寫的。然而location API使用頻率非常高,不少網站通過它檢測當前域名是否合法,例如:
if (location.host != '2.com') {
location.href = 'http://2.com';
}
JavaScript代碼檢測當前窗口的URL地址,如果不是2.com,就自動跳轉到2.com去。所以如果不考慮這個接口,網站跳轉后,就離開WebVPN設定的沙盒了,理論上它又無法重寫。同時因為location是全局變量,很容易跟其他局部變量混淆,造成不必要的麻煩。所以在實踐中一般都避免直接用location變量,通常的辦法是將JavaScript代碼中出現的location進行重命名,例如修改成_location,這樣就能將操作轉移到所定義的對象上。
因為Service Worker掌控所有流量,所以修改JS資源并不困難。此外,網頁中的內聯腳本也可通過MutationObserver攔截和修改。可以直接使用正則替換,將同名的函數、屬性、字符串也進行修改,但為了避免出現錯誤的修改或替換,更好的方案是在語法樹層面進行修改,但會引起較大的CPU占用。
2.2.2? 無法實現Hook的DOM解決方案
當然,也有些請求無法被Service Worker攔截,例如不同域的框架頁面。這時需要借助MutationObserver修改元素的URL屬性,將跨域的頁面變成同域,從而可攔截子頁面的所有請求。修改元素URL屬性的示例代碼如下:
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
const target = mutation.target;
if (target.tagName === "IFRAME" && target.src) {
target.src = newURL(target.src);
} else if (target.tagName === "OBJECT" && target.data) {
target.data = newURL(target.data);
} else if (target.tagName === "LINK" && target.href) {
target.href = newURL(target.href);
} else if (target.tagName === "A" && target.href) {
target.href = newURL(target.href);
}
}
});
observer.observe(document.documentElement, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ["src", "data", "href"],
});
function newURL(url) {
// 替換成新代碼
return url;
}
不過MutationObserver也仍然有一些問題。例如瀏覽器為了提高速度,有一個預加載的機制,原始URL在HTML解析階段就開始加載,那我們所做的修改會導致預加載取消,但實際請求其實已經產生,在瀏覽器的控制臺里仍然可看到這個處于cancel狀態的請求。此時也可以設置一個內容安全策略,讓網頁只允許加載自己域名下的內容,從而阻止預加載請求。所以MutationObserver并不是最佳方案,更完善的方案仍然是替換HTML里的URL地址。例如,可以使用DOM Hook技術來攔截和修改頁面中的form表單提交操作。在攔截到提交操作后,再將提交的地址修改為所設代理服務器的地址,從而實現跨域訪問。使用DOM Hook技術攔截和修改form表單提交操作的代碼示例如下:
let myForm = document.getElementById('myForm');
myForm.addEventListener('submit', function(event) {
event.preventDefault();
let xhr = new XMLHttpRequest();
xhr.open('POST', 'https://proxy.example.com?url=' + encodeURIComponent(myForm.action));
xhr.send(new FormData(myForm));
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
console.log(xhr.responseText);
}
};
});
表單被攔截和修改后,使用addEventListener方法為表單的submit事件綁定了一個回調函數。在回調函數中,首先調用event.preventDefault方法,阻止表單的默認提交操作。然后創建一個XMLHttpRequest對象,將請求的地址修改為所設定的代理服務器地址,將表單數據封裝為FormData對象并發送請求。最后在XMLHttpRequest對象的onreadystatechange回調函數中處理響應結果。相關代碼如下:
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.attributeName === 'src') {
const element = mutation.target;
const newUrl = new URL(element.src, location.href);
if (newUrl.hostname === 'example.com') {
element.src = '/proxy?url=' + encodeURIComponent
(element.src);
}
}
});
});
observer.observe(document, {
attributes: true,
attributeFilter: ['src']
});
上述代碼將在文檔中觀察所有帶有src屬性元素的變化。如果元素的src屬性指向example.com,則將其替換為代理地址/proxy?url=,并附加原始URL。
2.2.3? 無法Hook的資源解決方案
當使用Data URI時,瀏覽器會將數據URI轉化為一個Blob URL,然后再將其作為一個單獨的資源來請求。因此,即使使用Service Worker攔截這些請求,也無法捕獲數據URI的內容。下列代碼示例簡單說明了如何創建Data URI以及如何在Service Worker中捕獲其他資源的請求,但無法捕獲數據URI的請求:
//創建一個Data URI
const dataURI = 'data:text/plain;base64,SGVsbG8sIFdvcmxkIQ%3D%3D';
//在頁面中插入一個使用Data URI的圖像
const img = document.createElement('img');
img.src = dataURI;
document.body.appendChild(img);
//注冊Service Worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').then(function(registration) {
console.log('Service Worker registered:', registration);
}).catch(function(error) {
console.log('Service Worker registration failed:', error);
});
}
//Service Worker腳本
self.addEventListener('fetch', function(event) {
console.log('Fetch intercepted:', event.request.url);
//此處可以對其他資源的請求進行攔截和處理,但無法捕獲Data URI的請求
});
在上述示例中,創建了一個Data URI,并將其用作一個圖像的src屬性。當頁面加載時,瀏覽器會將Data URI轉換為Blob URL,然后像加載普通資源一樣請求該URL。在Service Worker中,可以攔截其他資源的請求并進行處理,但是無法捕獲Data URI的請求。因此,還需要借助API Hook和DOM Hook來覆蓋這類資源的加載。其他例如WebSocket協議ws和wss,雖然也不會經過Service Worker,但其本質仍是HTTP,因此通過API Hook即可解決。使用API Hook的示例代碼如下:
const originalFetch = window.fetch;
window.fetch = function(url, options) {
if (url.startsWith('data:') || url.startsWith('about:') ||
// handle the URL
}
return originalFetch(url, options);
};
2.3? Service Worker在線代理訪問的應用實例
文章作者在基于WebVPN開發高校電子資源訪問系統時,發現部分網站由于安全方面的考慮,對爬蟲等可能危害數據安全的技術做了較為全面的防范,網站需要在三個域之間來回切換,有多重身份校驗機制,使在線代理的實現受到了影響。例如某網站W,在未使用Service Worker技術時,無法正確顯示表單,WebVPN訪問的顯示結果如圖1所示。
但通過Service Worker使用DOM Hook技術來攔截和修改頁面中的form表單提交操作。在攔截到提交操作后,再將提交的地址修改為所設代理服務器的地址,從而實現了跨域訪問。正確的訪問頁面如圖2所示。
3? 結? 論
通過使用Service Worker技術,可以實現前后端分離,讓前端只提供靜態資源,負責展示網頁以及Service Worker的安裝和自身腳本,而后端只提供代理接口,負責數據轉發。在WebVPN中應用這項技術,還可以將各大網站的常用靜態資源預先部署到本地CDN上,當用戶訪問這些資源時,可以直接從CDN加載,從而大幅加快訪問速度,減少代理服務器的流量,大幅降低服務器的流量開銷。這一過程在瀏覽器的Service Worker中實現,不僅用戶毫無感知,連上層業務也一樣毫無感知。例如在高校圖書館的資源訪問系統中使用Service Worker技術,可以將讀者下載的文獻PDF文件緩存在本地服務器上,后續如果有其他用戶提出下載請求時,即可直接從緩存內發給用戶,這將大大提高用戶的下載體驗。
前后端分離的架構雖然大幅增加了開發的難度,例如Cookie的增刪修改、同源策略的模擬等,原本由瀏覽器底層實現的功能,現在需要在Service Worker中通過代碼來實現。但由于Service Worker的超高靈活性,它甚至自帶數據庫indexDB,可以把Cookie保存在數據庫中,按照需求進行修改。這大大拓展了在線代理的發揮空間。
盡管WebVPN本身技術并不復雜,但需要使用各種黑科技和巧妙的思路來改進它,完善其功能,擴展其應用場景和范圍。目前WebVPN已被廣泛用于高校圖書館的遠程資源訪問業務,方便了高校用戶在校外訪問電子資源,將Service Worker技術應用于WebVPN改進高校校外訪問系統,可以很好地解決服務器CPU占用問題,獲得更高的訪問速度和更好的用戶體驗。
參考文獻:
[1] 楊洋,李金祥,龔致.基于JavaScript hook的新型WEB在線代理方法:202010662640.4 [P].2020-07-10.
[2] Z4.防止網站被反代(禁止反向代理)的方法 [EB/OL].
(2020-04-21).https://cloud.tencent.com/developer/article/1617650.
[3] mb5fcf3d80e40fa.使用JS Hook技術,打造最先進的在線代理 [EB/OL].(2021-07-15).https://blog.51cto.com/u_15050720/
3438720.
[4] Zeus_龍.Hook(鉤子技術)基本知識講解,原理 [EB/OL].(2018-04-16).https://blog.csdn.net/qq_36381855/article/details/79962673.
[5] bobopeng.Hook技術之API攔截(API Hook) [EB/OL].
(2014-06-02).https://blog.csdn.net/junbopengpeng/article/details/28142669.
[7] MDN Web Docs 社區.DOM概述 [EB/OL].[2022-04-12].https://developer.mozilla.org/zh-CN/docs/Web/API/Document_Object_Model/Introduction.
[8] 謝智敏,王婷,趙英,等.高校圖書館電子資源訪問智能網關研究——以北京化工大學為例 [J].現代信息科技,2023,7(2):57-61+68.
[9] 林除夕.service worker是什么?看這篇就夠了 [EB/OL].
(2022-06-15).https://zhuanlan.zhihu.com/p/115243059.
[10] 深度開源.Is Service Worker Ready? [EB/OL].[2022-04-12].https://jakearchibald.github.io/isserviceworkerready/.
[11] MDN Web Docs 社區.Service Worker API指南 [EB/OL].[2022-04-12].https://developer.mozilla.org/zh-CN/docs/Web/API/Service_Worker_API.
作者簡介:謝智敏(1979—),男,漢族,安徽宣城人,館員,碩士,研究方向:智慧圖書館、知識產權信息服務;王婷(1975—),女,漢族,山東青島人,副研究館員,碩士,研究方向:智慧圖書館;通訊作者:郭倩玲(1971—),女,漢族,河北唐山人,副研究館員,博士,研究方向:智慧圖書館、知識產權信息服務。