谢智敏 王婷 郭倩玲
收稿日期: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—),女,汉族,河北唐山人,副研究馆员,博士,研究方向:智慧图书馆、知识产权信息服务。