Web

  • 单独的线程, 可以运行CPU密集型任务.
    因此无法访问一些无法保证线程安全的API.
  • 没有DOM, 相对iframe方案性能开销更小.
  • 非常有限的安全选项(仅能通过响应头应用CSP, 没有任何其他配置).
    例如, Web Workers可以访问同域的IndexedDB, 导致发生危险.
根据HTML Standard所述:
当Web Workers的URL是 data: 时, Web Workers的origin会变成opaque.
利用iframe的sandbox属性, 在iframe里创建的Web Workers的origin会变成opaque.
  • 有DOM, 相对Web Workers方案的性能开销更大.
  • 具有安全选项: sandbox和allow属性, 可基于最小权限原则逐步添加iframe的权限.
  • 尽管有一些例外, 但iframe通常会和创建它的环境共享相同的CPU线程.
    这使得iframe不适合运行CPU密集型任务.
一种在iframe里创建Web Workers的模式, 结合两者的优点.
表示资源的来源, 通常是一个URL, 具有特殊值 null.
origin的概念通用于所有的Web技术.
opaque origin表示当前资源无法追溯到一个透明的origin.
当资源具有opaque origin时, 访问 window.origin 会得到 null.
"origin为null"与"opaque origin"具有相同含义, 可以互相替代.
参考: https://stackoverflow.com/q/42239643/5462167
AMP曾经是被Google完全垄断的, 这引起很大的争议和抵制, 后来转为由OpenJS基金会主持.
AMP组件表面上是Web Components中的Custom Elements, 但事实上Bento AMP是由Preact构建的.
AMP页面曾经在Google的搜索结果里享有更高权重, 这一情况于2021年6月结束.
作为替代, Google使用新的页面体验排名算法分析每个网站的页面载入性能.
  • AMP的JavaScript文件事实上有70KB之巨.
    因此精心设计的页面会比没有缓存的AMP页面更快.
  • 有很多硬性限制, 例如CSS和JavaScript文件都被要求在一定尺寸以内.
  • 对元素事件的支持是一种DSL, 看起来简直像是重新发明了HTML的内联事件.
    这与开发人员熟知的最佳实践截然相反, 编写AMP本身就令人生畏.
可以在非AMP页面使用的AMP组件.
于2021年1月推出预览版.
由浏览器支持的可安装Web应用, 一些应用商店也支持将PWA作为应用程序提交.
PWA的标准很模糊, 本质上, 一个满足以下最低要求的网页就可以被称作一个PWA:
  • 注册Service Worker.
    这隐含了一个条件, 即网页需要使用HTTPS协议, 严格来说, PWA不支持混合内容.
  • 包含Web App Manifest: 一个填写了必要元数据的JSON文件, 通常被命名为 manifest.webmanifest.
仅仅满足PWA的最低要求通常是不够的, 因为人们对它的期望是一个应用程序, 所以还有这些软要求:
  • PWA不应该像一般网页那样出现整个页面的导航, 尤其是不能出现白屏.
  • PWA应该是可以离线访问的, 需要注重通过Service Worker将内容缓存.
  • PWA应该适合移动设备访问.
业界倾向于将现有网站改进至兼容PWA, 而不是围绕着PWA建立应用.
Twitter, Reddit, 微博都是这么做的.
实验性API, 只受Chromium系浏览器支持.
基本是在模拟移动平台上的Background API, 允许按照特定时间间隔来运行任务.
PWA的通知功能仍然相当残疾.
举例来说, 由于PWA缺乏与其他平台对等的Alarms API, 甚至不可能创建一个能够在不联网的情况下使用的带有通知提醒功能的To-do list应用.
最初, Push API在浏览器中缺乏统一标准, Chrome浏览器依赖于GCM(Google Cloud Messaging)接收推送,
这导致大部分过时文章会将Push API等同于使用GCM, 而依赖于GCM显然是一种供应商锁定.
直到2016年, 浏览器的Push API才有了一个基于VAPID的统一标准, 从而允许任何具有特定实现的服务器向浏览器推送内容.
然而, 这种基于统一标准的推送仍然被设计得很复杂, 以至于基本上不会有人想要手动实现它.
参考:
  • https://web.dev/push-notifications-subscribing-a-user/
参考: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html
CSRF令牌是一种随页面下载得到的具有不可预测性的令牌(token), 只有包含此令牌的请求会被服务器接受.
使用CSRF令牌是为了防止通过其他域名发起CSRF攻击.
常见的CSRF令牌位置:
  • 作为不可见字段隐藏在表单里, 提交表单时会一并提交令牌.
  • 通过 Set-Cookie 保存为客户端Cookie, 发送带有凭据的请求时会一并发送令牌.
    存储令牌的Cookie必须设置正确的SameSite属性, 否则仍然会留下CSRF攻击漏洞,
    因为伪造的请求能够在发送凭据时一并发送令牌.
  • 在请求中返回, 由客户端保存, 客户端之后用自定义 X-CSRF-Token 头发送令牌.
跨站请求如果可以通过CORS检查, 则意味着CSRF令牌也可以被客户端脚本读取, 客户端脚本就可能利用CSRF令牌伪造请求.
设置此属性时, Cookie将在此域名及其后代域名下的请求中被发送.
省略此属性时, Cookie将在设置此Cookie的文档的域名下的请求中被发送, 但在其后代域名下的请求中不发送.
用户代理在处理 Set-Cookie 时会按规则拒绝部分Domain属性设置:
  • 值与响应来源不同.
  • 值为响应来源的后代域名(父域名不能为后代域名设置Cookie, 但后代域名可以为父域名设置Cookie).
在早期版本的规范中(RFC 2109), 只有在域名带有前导点的情况下(例如 .example.com), 相关Cookie才会在后代域名中可用.
这一规则后来被修改(RFC 6265): 前导点被忽略, 带有前导点的域名和没有前导点的域名从此等价.
此Cookie不可通过JavaScript访问(document.cookie).
此Cookie仅可通过HTTPS协议发送.
SameSite是Cookie的一个属性, 正确设置SameSite可以防止构造带有Cookie的CSRF请求.
  • Strict: Cookie只能在相应网站上发送.
    使用Strict意味着当用户是从其他网站导航至目标网站时, 或重定向到目标网站时, 本次请求将不会发送Cookie.
  • Lax: 与 Strict 类似, 但在点击链接导航至目标网站时也会发送Cookie.
    在现代浏览器中, 如果未明确指定SameSite属性, 则 Lax 会作为默认值.
  • None: Cookie将在所有的上下文中被发送.
    在旧浏览器里, None 是SameSite的默认值.
    如果SameSite被设置为 None, 但却没有设置Secure属性(要求此Cookie只能在HTTPS请求中发送),
    控制台会打印警告, 因为Cookie可能被中间人窃取.
对CORS的一种常见误解是认为它可以阻止CSRF, 但事实并非如此,
CORS只是一种可以防止响应被客户端读取的跨站资源共享策略而已.
方法为GET, HEAD, POST, 且不包含自定义请求头的请求(有少数几个请求头是例外), 不会触发预检.
详见 https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests
因此不触发预检意味着这些请求会完整发送到目标服务器:
浏览器之后可能因为不满足CORS而在客户端层面上拒绝掉相关响应, 但请求确实被服务器接受了,
这些请求可以被用于CSRF攻击.
img元素会无视CORS, 对它src属性里的url发出的GET请求.
form提交会无视CORS, 可以用于发送GET请求和POST请求.
重点在于用form触发请求并不需要人工点击提交按钮, 仅仅只是由JavaScript调用 form.submit() 就会发出请求.
用form构造JSON请求是可能的,
enctype="text/plain" 时, 可以通过拼接input元素的 namevalue 属性构造出JSON文本.
这种攻击之所以成立的原因之一, 是API服务器没有限制客户端的 content-type 请求头.
自HTTP 1.0可用的永久重定向.
使用该状态码的重定向可能会将非GET方法的请求重定向为GET方法, 当使用GET以外的方法时, 不建议使用它.
自HTTP 1.0可用的临时重定向.
使用该状态码的重定向可能会将非GET方法的请求重定向为GET方法, 当使用GET以外的方法时, 不建议使用它.
自HTTP 1.1可用的临时重定向, 用来取代302.
重定向时会将请求改为GET方法.
自HTTP 1.1可用的临时重定向, 用来取代302.
使用该状态码的重定向不会改变请求的方法.
自HTTP 1.1可用的永久重定向, 用来取代301.
使用该状态码的重定向不会改变请求的方法.
注意: 虽然与TCP Keepalive具有类似的名称, 但两者完全没有关系.
HTTP Keppalive的功能是使接下来的HTTP请求复用上一个连接, 而不是每次请求都打开一个新连接.
当连接已结束或超时(一定时间内不再有数据包), 连接才会关闭.
需要包含请求和响应都包含 Connection: Keep-alive.
默认启用, 根据平台的不同, 具有不同的默认超时时间.
在HTTP/2里, Keepalive已经没有作用, 因为HTTP/2自身实现了多路复用.
Fetch/Beacon API的keepalive与HTTP Keepalive 不同, 启用它只能保证链接在页面关闭后仍然能够完成发送.
对于HTTP Keepalive, Fetch/Beacon API本来就会在HTTP 1.x连接上自动启用它, 所以根本不需要也无法设置它.
Fetch/Beacon API在开启keepalive后会限制正文大小, 超过64KB的内容无法被发送.
在Chrome(v105)上, 通过fetch发送正文超过64KB的请求时, 请求会立即失败, 但相关请求会错误地显示为"待处理".
在Firefox(v105)上, 通过fetch能正常发送正文超过64KB的请求.
浏览器实现相关限制是有道理的, 如果允许非常大的正文, 则连接实际上会长期开启(除非用户关闭浏览器).
即HTTP over QUIC, 是Google的QUIC协议转正的结果.
QUIC建立在UDP协议之上(使用UDP协议而不是创建IP协议下的新协议, 是考虑到中间设备的支持问题).
一组HTTP协议扩展, 目标是在HTTP协议上提供多人编辑功能.
WebDAV的一个典型用途是通过HTTP协议创建可共享的文件夹:
由于主流操作系统都内置了WebDAV客户端, 可以将WebDAV资源作为文件夹打开.
然而, WebDAV并不流行, 只有少数在线存储服务提供商支持WebDAV.
在大多数情况下, 可以将WebDAV简单地视作FTP协议等文件传输协议的替代品.
WebDAV所提供的大部分好处都来自HTTP.
对WebDAV的扩展, 以使其支持日历数据.
对WebDAV的扩展, 以使其支持联系人数据.
单工, 协议本身基于UTF-8编码的文本, 所以无法传输二进制数据.
基于HTTP实现(因此可享受到HTTP/2的多路复用优势和Node.js的事件循环模式).
是HTTP Comet技术的精神继承者.
支持使用event字段添加事件类型, 但由于所有文本数据都会完整发送至客户端, 该特性在很多场景下聊胜于无.
SSE草案提到每15秒发送一次心跳包, 这个频率可能超出了实际需求, 但也不会对服务器带来太大负担.
有几种不同的心跳包形式:
  • 草案中推荐的做法: 发送一个空白注释包, 即 :\n\n.
    尽管理论上, 也可以用 \n\n 发送心跳包(这是一个真正的空包, 连空白的注释也没有), 或者只是发送 :\n 来吊着客户端.
    这种心跳包的缺陷在于, 原生EventSource不可能使用它实现客户端心跳检测, 因为这些包对客户端来说是透明的.
  • event: hbevent: heartbeatevent: ping 发送空 data 或带有时间戳的 data
    带有时间戳的好处是当服务端和客户端中间存在奇怪的延迟问题时, 时间戳可以作为判断两次发送心跳间隔的依据.
    这种心跳包比仅发送注释需要携带更多数据量, 但原生EventSource可以检测到它.
客户端支持基于id的自动重连.
尽管协议本身支持HTTP头, 但浏览器原生的EventSource API没有提供相应的选项.
EventSource在连接断开的情况下会自动重连,
在重连时若得到不符合SSE要求的HTTP状态码时会放弃重连(视作被服务器拒绝).
服务端可以使用retry字段设置客户端重连前需要等待的时间(毫秒单位, 由浏览器决定默认值, 一般为3~5秒).
https://bugzilla.mozilla.org/show_bug.cgi?id=444328
由于SSE在应用层只提供数据的单向发送, 因此由服务端主动向客户端发送心跳包.
只能让服务器断开TCP已死的连接, 这会导致客户端的连接仍然处于半开状态.
在客户端未配置TCP Keepalive的情况下,
客户端的EventSource连接 最多需要2个小时才能发现服务器端的连接已经断开.
这使得自动重连机制在绝大多数场景下都 聊胜于无 (客户端未能配置合适的TCP Keepalive).
客户端需要能够识别服务器发送的心跳包, 在一定时间未收到心跳包时主动断开实际已经死亡的半开连接.
双工, 二进制数据或UTF-8文本数据, 基于纯TCP实现(每一个标签页都会导致一个新的TCP连接).
WebSocket的Web API没有设计流控机制, 因此会出现背压问题.
Chromium团队添加了一个非标准的WebSocketStream来解决此问题, 但相关API迟迟未能被标准化:
https://developer.chrome.com/zh/articles/websocketstream/
无法使用HTTP头, 客户端需要自行协商一些基本功能(例如身份验证).
WebSocket协程内置了ping/pong机制, 在底层ping包和pong包是作为控制帧发送的, ping为 0x9, pong为 0xA.
通常来说最佳实践是由服务端发送ping包, 以检测客户端是否存在.
浏览器端的WebSocket API没有提供与ping/pong机制有关的方法, 对于应用程序来说, 心跳机制完全是透明的.
这是非常遗憾的, 这意味着客户端没有能力主动发起ping包来自己检测与服务器的连通性.
为了实现客户端最小数据量的心跳检测, 只能定期向服务器发送空白数据.
如果客户端与服务端之间是直接连接的, 则TCP连接即使在没有数据包的情况下也会继续维持.
但假如客户端与服务端之间有防火墙等设备, 则TCP连接有可能因为长时间没有数据包经过而被中间设备删除,
这会导致下一次发包时发现TCP连接已断开.
一端发送心跳包并等待另一端的回应, 这时可以用超时来检测无效连接.
注意: 虽然与HTTP Keepalive具有类似的名称, 但两者完全没有关系.
TCP Keepalive的功能是使TCP保持活动状态, 这意味着可以检查已连接的TCP Socket, 以确认连接是否正常.
建立TCP连接时, 会将连接与计时器相关联.
当计时器归零时, 会发送Keepalive数据包, 利用TCP连接的双向性对连接进行确认(ACK).
按照公式, 一个TCP连接会在x秒后丢失:
x = KEEPALIVE_TIME + (KEEPALIVE_PROBES + 1) * KEEPALIVE_INTERVAL
从技术角度看, 这是最完美的心跳检测方式, 在任何主流TCP实现上都已实现, 但存在以下问题导致它难以使用:
  • TCP连接默认不启用Keepalive, 必须由应用调用相应的API才能支持Keepalive.
  • Keepalive的相关选项在一些平台上不受应用层的编程控制.
    例如在Windows上只能全局配置, Java不支持应用级配置.
  • 有着脱离时代的奇怪默认值, 例如默认的超时检测时间为2小时, 这是在RFC 1122里定义的.
因此, 若要使用TCP Keepalive, 则意味着:
  • 必须有配置操作系统的权限.
  • 必须修改应用以启用TCP Keepalive.
  • Keepalive选项必须适用于该操作系统上的所有应用程序.
TCP Keepalive仅适用于服务器端的半开连接检测, 如果配置TCP Keepalive, 则可以省去应用层的心跳机制.
对于客户端来说, 必须在应用层自行实现ping/pong心跳机制.
对Node.js程序来说, http和https模块可以在Agent对象中启用TCP Keepalive,
但与大多数编程平台一样, 无法在应用层提供TCP Keepalive的选项.
\HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\TCPIP\Parameters
修改/创建相应的注册表项:
  • KeepAlivetime, 单位为毫秒, 默认为7200秒.
  • KeepAliveInterval, 单位为秒, 默认为1秒.
  • TCPMaxDataRetransmissions, 默认为5.
Windows需要重新启动以使配置生效.
注1: 似乎只有ipv4具有可配置的keepalive属性.
注2: Docker容器不会继承宿主机的keepalive设置.
# 读取/写入当前keepalive超时检测时间(秒), 默认为2小时, 建议值为5分钟
cat /proc/sys/net/ipv4/tcp_keepalive_time
echo 300 > /proc/sys/net/ipv4/tcp_keepalive_time
# 使用sysctl的等价方式
sysctl net.ipv4.tcp_keepalive_time
sysctl -w net.ipv4.tcp_keepalive_time=300 # 永久修改, 在重启后仍然有效
# 读取/写入当前keepalive心跳包发送间隔(秒), 默认为75秒
cat /proc/sys/net/ipv4/tcp_keepalive_intvl
echo 30 > /proc/sys/net/ipv4/tcp_keepalive_intvl
# 使用sysctl的等价方式
sysctl net.ipv4.tcp_keepalive_intvl
sysctl -w net.ipv4.tcp_keepalive_intvl=30 # 永久修改, 在重启后仍然有效
# 读取/写入当前keepalive 未收到响应的容忍次数, 默认为9
cat /proc/sys/net/ipv4/tcp_keepalive_probes
echo 10 > /proc/sys/net/ipv4/tcp_keepalive_probes
# 使用sysctl的等价方式
sysctl net.ipv4.tcp_keepalive_probes
sysctl -w net.ipv4.tcp_keepalive_probes=10 # 永久修改, 在重启后仍然有效
一种几乎已经不再被使用的Web技术, 通常配合RSS使用.
订阅者访问发布者的站点, 发布者提供了一个推荐的Hub地址(可能是发布者自己的服务, 也可能是一个公共服务).
订阅者访问Hub(访问可能由程序自动完成), POST一个订阅请求,
希望Hub能在发布者(hub.topic)发布内容时, 发送到订阅者指定的回调URL(hub.callback).
Hub向订阅者提供的回调URL发送一个意图测试请求, 回调URL应该响应并且返回协议要求的特定信息.
当发布者发布新内容时, 发布者会通知Hub(通过hub.url属性提供更新的url, 该url应与订阅者的hub.topic相同),
Hub会访问url, 查看是否有新内容(取决于具体实现, 流行的实现似乎是检查url的RSS Feed),
再通知订阅者(标准通知仅仅是一个ping通知, 这意味着订阅者会接收到空的请求, 然后由订阅者自行访问订阅的页面).
尽管被W3C标准化, 但实际的实现还多是停留在其前身PubSubHubbub 0.4工作草案(PuSH 0.4).
该协议很明显是与Atom/RSS等协议配合使用, 在Hub不发送"Fat ping"(包含HTML正文的通知)时,
实际上仅起到了发送回调"通知"更新的作用.
WebSub的订阅方式是Request/Reply式的异步订阅, 而不是基于推送的PubSub.
Request/Reply的主要特征是订阅者在收到通知后需要主动去一个Inbox获取最新的消息
(在WebSub的例子里是RSS Feed).
文件与目录条目API建立了一个沙盒文件系统, 从而允许Web应用程序在受限的情况下访问文件系统.
尽管初衷很美好, 但现实是除了"读取文件和目录"这一用例所必需的API之外, 基本都已被标准抛弃.
这意味着Web应用程序不能在沙盒文件系统上执行文件移动/重命名, 删除, 复制, 写入, 查看元数据等操作.
https://developer.chrome.com/articles/file-system-access/
一个比File and Directory Entries新的API.
File System Access继承了File and Directory Entries访问文件系统的愿景, 试图重新标准化文件系统功能.
  • 文件系统API与不依赖于 <input> 等DOM元素, 直接由JavaScript控制.
    这意味着可以在没有DOM的环境下使用.
  • 文件系统API直接访问本地文件系统, 而不是访问沙盒文件系统.
  • 文件系统API支持文件系统的基本功能, 而非只支持读取.
    尤其值得一提的是, 文件系统API可以在本地文件系统目录中自由创建文件, 而不需要借助浏览器的下载功能.
  • 文件系统API的文件和目录句柄可以被保存供未来使用.