0%

HTTP(Hyper Text Transfer Protocol,超文本传输协议)是 Web 开发中最重要的协议之一。本文将从基础到进阶,全面讲解 HTTP 协议的核心知识点。

阅读全文 »

垃圾回收机制是由引擎来负责的。JS 引擎有很多种(各个浏览器都不同),其垃圾回收机制在一些细节及优化上也略有不同。本文以一些通用的回收算法作为切入,再由市场占有率大的 V8 引擎发展至今对该机制的优化为例。JavaScript 是门魅力无限的语言,关于它的 GC(垃圾回收)方面,你了解多少呢?想来大部分人是因为面试才去看一些面试题从而了解的垃圾回收,那在正式开始之前,给大家列几个小问题,大家可以先想一下答案,带着问题及答案再去看文章。

  • 什么是垃圾回收机制?
  • 垃圾是怎样产生的?
  • 为什么要进行垃圾回收?
  • 垃圾回收是怎样进行的?
  • V8 引擎对垃圾回收进行了哪些优化?
    阅读全文 »

本文全面介绍了前端安全的各个方面,包括加密算法、常见攻击方式及其防御措施,以及现代前端安全最佳实践。

阅读全文 »

目录

  1. 开篇:一个故事看懂鉴权全貌
  2. 前端鉴权概览:四种方式的横向对比
  3. Cookie:浏览器的小纸条
  4. localStorage & sessionStorage:前端本地存储
  5. Cookie + Session 认证:经典的登录方案
  6. Token 认证:现代 API 的首选
  7. JWT:自带信息的令牌
  8. OAuth 2.0 / SSO:第三方登录与单点登录
  9. 面试高频问题汇总
  10. 总结回顾

开篇:一个故事看懂鉴权全貌

想象你去一家公司拜访朋友:

  1. 前台登记(认证):你出示身份证,前台核实你的身份,确认”你真的是你”
  2. 领取临时工牌(凭证):前台给你一张临时工牌,上面有你的照片和有效期
  3. 刷卡进出门(授权):不同门禁需要不同的权限——你能进大厅但不一定能进机房
  4. 集团分公司通用(SSO):如果你有集团总部的工牌,旗下子公司也认——不需要每家都登记一次

对应到计算机世界:

现实世界计算机世界
出示身份证输入用户名密码
前台核实Authentication(认证)
临时工牌Token / Session ID
不同门禁权限Authorization(授权)
集团工牌通用SSO(单点登录)
你把工牌借给朋友会话劫持 / Token 泄露

❗ 面试常问:认证(Authentication)和授权(Authorization)的区别?
认证是”你是谁”,授权是”你能做什么”。认证在前,授权在后。很多系统只需要认证(如公开 API 的限流),但授权一定依赖认证。


前端鉴权概览:四种方式的横向对比

HTTP 是无状态协议——每次请求都是独立的,服务器记不住”上次和你聊过”。要让前后端互信,必须主动维护一个状态。前端常见的鉴权方式有四种:

方式状态存放是否跨域CSRF 防护服务端开销典型场景
Cookie + Session服务端(Session 内存/Redis)❌ 需额外防护传统 MPA(多页应用)
Token(含 JWT)客户端(localStorage/内存)✅ 是✅ 天然防御低(无状态)SPA、移动端 API
OAuth 2.0第三方认证服务器✅ 是中等第三方登录(Google、微信)
SSO统一认证中心跨站点中等公司内部多系统打通

这四种方式并非互斥,实际项目中常常组合使用:

  • 内部系统用 SSO + Cookie Session
  • 对外 API 用 JWT
  • 第三方登录用 OAuth 2.0,内部仍用 Session 或 JWT 管理登录态

Cookie:浏览器的小纸条

Cookie 是服务端塞给浏览器的一小块数据,浏览器会把它存起来,下次请求同一域名时自动带上。

1
2
3
4
5
6
7
8
sequenceDiagram
participant B as 浏览器
participant S as 服务端
B->>S: 首次请求
S->>B: 响应 Set-Cookie: sessionId=abc123
Note over B: 浏览器保存 Cookie
B->>S: 后续请求自动带上 Cookie
S->>S: 解析 Cookie 识别身份
属性说明面试频率
Expires / Max-Age过期时间。不设置则为会话 Cookie(关闭浏览器即失效)⭐⭐
Domain指定哪些域名可以访问。如 .example.com 则子域名都可用⭐⭐
Path指定路径生效范围,默认 /
Secure仅在 HTTPS 下传输⭐⭐⭐
HttpOnlyJS 无法读取(document.cookie 拿不到),防 XSS⭐⭐⭐⭐
SameSite现代 CSRF 防护核心,见下方详解⭐⭐⭐⭐⭐

Cookie 由服务端通过 HTTP 响应头 Set-Cookie 设置:

1
Set-Cookie: sessionId=abc123; Expires=Wed, 21 Oct 2026 07:28:00 GMT; Path=/; Domain=.example.com; Secure; HttpOnly; SameSite=Lax

配置说明:

  • 服务端返回响应时,在 Set-Cookie 中指定属性,多个属性用 ; 分隔
  • 浏览器收到后,会按照这些属性规则存储和携带 Cookie
  • 前端无法主动设置 HttpOnly、Secure、SameSite 等安全属性——这些必须由服务端在响应头中指定。如果前端想在客户端创建 cookie,只能用 document.cookie,且无法添加 HttpOnly 等安全属性

JS 操作 Cookie

1
2
3
4
5
6
7
8
9
// 设置(只能设置 name=value、Domain、Path、Expires,无法设置 HttpOnly / Secure / SameSite)
document.cookie = 'token=abc123; Path=/; Domain=.example.com'

// 读取
console.log(document.cookie) // "token=abc123; otherKey=otherValue"
// ⚠️ 如果 cookie 设置了 HttpOnly,这里不会显示该 cookie

// 删除:把 Expires 设为过去的时间
document.cookie = 'token=; Expires=Thu, 01 Jan 1970 00:00:00 GMT'

✅ SameSite 属性(现代标准,必知)

SameSite 是 Chrome 51+ 引入的 Cookie 安全属性,用于限制第三方请求是否携带 Cookie,是目前最有效的 CSRF 防御手段之一。

行为使用场景
Strict完全禁止第三方请求携带,即使是点击链接跳转也不带银行转账等极高安全场景
Lax(默认)允许 GET 方式的顶级导航携带(点击链接、GET 表单),POST 等方式不携带大多数网站推荐
None不限制,但必须配合 Secure(仅 HTTPS)跨域请求需要携带 Cookie 时

CSRF 的攻击原理是:攻击者在自己的页面构造请求,让浏览器带着你的 Cookie 向目标网站发起请求(比如转账)。因为 Cookie 自动携带,服务端无法区分这个请求是用户主动发起的还是被诱骗发起的。

防御手段(从推荐到兜底):

  1. SameSite Cookie(首选):设置 SameSite=Lax。现代浏览器默认行为,阻止 POST 等危险请求跨站携带 Cookie。配置简单、覆盖面广,后端加一行响应头即可——这是当前最推荐的防御方案。

  2. 校验 Referer / Origin 头:服务端检查请求来源是否为合法域名。缺点:某些场景(如 HTTPS→HTTP 跳转)浏览器可能不发送 Referer 头。

  3. CSRF Token:服务端在页面中嵌入一个随机 token,提交请求时携带。攻击者无法获取这个 token,因此请求被拒绝。缺点:SPA 需要额外的后端配合。

  4. 关键操作加验证码:转账、改密等操作强制用户输入验证码,从根本上阻止 CSRF。

localStorage 如何避免 XSS 风险?

XSS(跨站脚本攻击)是指攻击者在页面中注入恶意脚本,从而读取页面中的一切数据,包括 localStorage。

防御手段:

  1. 永远不要在 localStorage 中存敏感信息(如身份证号、银行卡号)。如果必须存 token,要接受 XSS 会导致 token 泄露的风险。

  2. 严格防范 XSS(这是根本):

    • 对用户输入进行转义和过滤(避免 <script> 标签被直接渲染)
    • 使用 Content-Security-Policy(CSP)头限制脚本来源
    • React / Vue 等现代框架默认对插值做转义,但 v-html / dangerouslySetInnerHTML 仍存在风险
  3. 使用 HttpOnly Cookie 替代 localStorage 存 token(但如果你的 SPA 需要跨域 API,这条路走不通)

  4. 最佳实践:Access Token 放内存,Refresh Token 放 HttpOnly Cookie——XSS 能读到内存中的 Access Token,但有效期短(15 分钟);拿不到 Refresh Token 就无法续期,损失可控。

  • 大小限制:单个域名下约 4KB
  • 数量限制:每个域名约 20~50 个(因浏览器而异)
  • 同源限制:不可跨域,子域名通过 Domain 可共享
  • 自动携带:同域名的每个请求都会带上,影响性能(尤其静态资源多时)
  • 不安全:明文传输(除非 Secure),可被 XSS 读取(除非 HttpOnly)

现代浏览器默认开启 Cookie,禁用的情况极少。如果真禁用了,使用 URL 参数传递 Session ID 是最常见的替代方案。但要注意:URL 传递 Session ID 容易被截取或泄露(通过 Referer 头),所以主流做法是要求浏览器开启 Cookie,否则提示用户。

✅ 过时内容清理:Flash 沙箱、IE6 时代的 Cookie 问题已全部删除。


localStorage & sessionStorage:前端本地存储

⚠️ 重要区分Session(服务端会话)sessionStorage(浏览器存储) 除了名字相似,没有任何关系!Session 存在服务器内存/Redis 中,sessionStorage 存在浏览器磁盘中。Session 的过期由服务端控制,sessionStorage 关闭即清理。

三兄弟对比表(面试必考)

特性CookielocalStoragesessionStorage
容量~4KB~5MB+~5MB+
生命周期可设置,默认关闭浏览器即失效永久保存(除非手动清除)当前标签页会话,关闭即清除
HTTP 携带✅ 每次请求自动带上❌ 不参与通信❌ 不参与通信
跨域❌ 绑定域名❌ 同源策略❌ 同源策略
可访问性同域名所有标签页同源所有标签页仅当前标签页
XSS 风险HttpOnly 可防御⚠️ JS 直接读取,风险高⚠️ JS 直接读取,风险高
API 易用性需要自行封装原生接口友好原生接口友好

Storage 的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 存
localStorage.setItem('token', 'eyJhbGciOiJIUzI1NiIs...');
sessionStorage.setItem('tempData', JSON.stringify({ id: 1 }));

// 取
const token = localStorage.getItem('token');
const temp = JSON.parse(sessionStorage.getItem('tempData'));

// 删
localStorage.removeItem('token');
sessionStorage.clear(); // 清空全部

// storage 事件:不同标签页间通信
window.addEventListener('storage', (e) => {
console.log('新的 key:', e.key);
console.log('旧值:', e.oldValue, '新值:', e.newValue);
});

❗ 面试题:localStorage 如何跨标签页通信?
通过 storage 事件。当同一源下的某个页面修改 localStorage 时,其他页面会触发该事件。

Token 存哪最安全?(面试高频)

存储位置XSS 防护CSRF 防护跨域场景
localStorage❌ 直接暴露SPA 常用,需做好 XSS 防护
sessionStorage❌ 直接暴露标签页隔离需求
Cookie(HttpOnly + SameSite)✅(SameSite=Lax)最安全但限制跨域
内存变量刷新即丢失

最佳实践:将 Access Token 放在内存中,Refresh Token 放在 HttpOnly Cookie 中。这样即使 XSS 也只能拿到短期有效的 Access Token,拿不到可刷新的 Refresh Token。


⚠️ 名称澄清:此处的 Session(会话) 是服务端概念,和浏览器的 sessionStorage 没有任何关系。Session 存在服务器内存/Redis 中,由服务端管理其生命周期。

工作原理

1
2
3
4
5
6
7
8
9
10
11
sequenceDiagram
participant B as 浏览器
participant S as 服务器
B->>S: 1. POST /login (username + password)
S->>S: 2. 验证账号密码
S->>S: 3. 创建 Session (内存/Redis)
S->>B: 4. Set-Cookie: "JSESSIONID=xxx; HttpOnly;Secure;SameSite=Lax"
Note over B: 5. 浏览器保存 Cookie
B->>S: 6. 后续请求自动携带 Cookie
S->>S: 7. 根据 SessionID 查找 Session
S->>B: 8. 返回业务数据

关键概念

  • Session:服务端内存中的一块空间,存储用户信息(ConcurrentHashMap / Redis)
  • Session ID:唯一标识,通过 Cookie 传给客户端
  • Cookie:浏览器端的存储载体,自动携带 Session ID

Session 的生命周期

Session 存在于服务端,它的有效期由服务端配置决定,与”浏览器是否关闭”没有直接关系:

  • 超时过期:最常见的策略。用户一段时间无操作(如 Tomcat 默认 30 分钟),服务端自动销毁 Session。Node.js (express-session) 默认也是 10 分钟无操作即过期。
  • 主动销毁:用户点击”退出登录”,服务端删除 Session。
  • Cookie 中的 Session ID 是会话 Cookie(默认),浏览器关闭后丢失。但**丢失的是”钥匙”而不是”锁”**——Session 数据在服务端还会保留一段时间,直到 GC(垃圾回收)清理。

举例:你用手机浏览器登录了知乎,Session 数据存在知乎服务器。你关掉浏览器,知乎服务端的 Session 并不会立即消失——它还会保留十几分钟到半小时,等超时后再被清理。这就是为什么”关浏览器”不等于”退出登录”。

Session 的分布式问题(面试高频)

当服务扩展到多台服务器时,Session 存在不同机器上怎么办?

方案说明优缺点
粘性会话(Sticky Session)负载均衡将同一用户始终转发到同一台机器简单但有单点风险,某台挂掉则 Session 丢失
Session 复制各服务器间同步 Session消耗网络带宽,节点多时性能差
集中式 Session(推荐)所有 Session 存入 Redis✅ 主流方案,弹性伸缩,节点故障不影响
无状态 JWT不再需要服务端存 Session彻底解决分布式问题,但引入 JWT 的其他问题

❗ 面试题:多服务器部署时 Session 怎么处理?
标准答案是用 Redis 集中存储 Session,所有服务节点从同一个 Redis 读取 Session 数据。

  • 安全性:Session 服务端存储 > Cookie 客户端存储

  • 存储类型:Session 支持任意对象 > Cookie 仅支持字符串

  • 存储大小:Session 远大于 Cookie 的 4KB

  • 生命周期

    • Session:存在服务端,生命周期由服务端控制。最常见的超时策略是”一段时间无操作则过期”(如 30 分钟),而非”浏览器关闭”。虽然 Cookie 中的 Session ID 默认是会话 Cookie(关浏览器就没了),但Session 本身的有效期和服务端配置相关(比如 Tomcat 的 session-timeout、PHP 的 session.gc_maxlifetime)。浏览器关闭只丢失了 Session ID 这个”钥匙”,Session 数据本身还会在服务器保留一段时间直到 GC 回收。
    • Cookie:由客户端控制过期,可设置为长期有效(对应用户的”记住我”功能)

Token 认证:现代 API 的首选

Token 解决了什么问题?

  1. CSRF 防护:Token 由前端手动携带(放在 Authorization Header),浏览器不会自动带,从根本上避免 CSRF
  2. 跨域友好:Token 不存在同源限制,适合 SPA 和移动端
  3. 服务器无状态:Token 自身包含用户信息,服务器不需要存储会话
  4. 适合分布式:无状态意味着任何服务器节点都能验证 Token

Access Token + Refresh Token 机制

这是现代 Token 认证的标准模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
sequenceDiagram
participant C as 客户端
participant I as IdP(认证服务)
participant S as 业务服务

Note over C,I: 1. 登录阶段
C->>I: POST /login (username + password)
I->>C: { access_token, refresh_token }

Note over C,S: 2. 正常请求
C->>S: GET /api/data (Authorization: Bearer access_token)
S->>C: 200 OK + data

Note over C,S: 3. Access Token 过期
C->>S: GET /api/data (Authorization: Bearer access_token)
S->>C: 401 Unauthorized

Note over C,I: 4. 刷新 Token
C->>I: POST /refresh (refresh_token)
I->>C: { access_token: new, refresh_token: new }
C->>S: GET /api/data (Authorization: Bearer new_token)
S->>C: 200 OK + data

为什么需要两个 Token? 职责分离:

  • Access Token:短期有效(15 分钟~2 小时),用于请求资源,丢失了影响有限
  • Refresh Token:长期有效(天级别),仅用于获取新的 Access Token,通常存在服务端以增加安全性

面试重点:无痛刷新 Token(前端实战)

当用户同时发起多个请求时,如果 Access Token 恰好过期,不能每个请求都去刷新 Token。标准做法是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import axios from 'axios'

let isRefreshing = false // 是否正在刷新 Token
let pendingRequests = [] // 等待重试的请求队列

const request = axios.create({
baseURL: '/api',
timeout: 30000,
})

// 请求拦截器:自动带 Token
request.interceptors.request.use((config) => {
const token = localStorage.getItem('access_token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})

// 响应拦截器:Token 过期自动刷新
request.interceptors.response.use(
(response) => response.data,
(error) => {
if (!error.response) return Promise.reject(error)

const { config, response: { status, data } } = error

// Access Token 过期(根据后端约定判断)
if (status === 401 && data?.code === 'TOKEN_EXPIRED') {
if (!isRefreshing) {
isRefreshing = true
return refreshToken()
.then(({ access_token }) => {
// 更新本地 Token
localStorage.setItem('access_token', access_token)
// 重试所有等待的请求
pendingRequests.forEach(cb => cb(access_token))
pendingRequests = []
// 重试当前请求
config.headers.Authorization = `Bearer ${access_token}`
return request(config)
})
.catch(() => {
// Refresh Token 也失效了,跳登录页
redirectToLogin()
})
.finally(() => {
isRefreshing = false
})
} else {
// 正在刷新,把当前请求加入队列等待
return new Promise((resolve) => {
pendingRequests.push((newToken) => {
config.headers.Authorization = `Bearer ${newToken}`
resolve(request(config))
})
})
}
}
return Promise.reject(error)
}
)

function refreshToken() {
const refreshToken = localStorage.getItem('refresh_token')
return axios.post('/auth/refresh', { refresh_token: refreshToken })
.then(res => res.data.data)
}

通俗理解:这相当于”排队机制”——第一个请求发现过期了,就先去”取新令牌”;其他请求在窗口等着,等第一个拿回新令牌后,大家继续办业务。

Token 的存储位置取舍(面试高频)

方案安全风险实际应用
localStorageXSS 直接泄露最常见但也最危险
Cookie(HttpOnly + SameSite)XSS 安全 ✅,但跨域麻烦同域下的 SPA
内存(变量)+ Refresh Cookie✅ 刷新丢失 Access Token,但 Refresh Token 在 Cookie 中安全最佳实践

JWT:自带信息的令牌

JWT(JSON Web Token,读作 “jot”)是一种规范化的 Token 格式,定义在 RFC 7519。相当于”会自己说话的工牌”——不用去后台查数据库,看工牌本身就知道你是谁。

JWT 的结构

JWT 由三个用 . 分隔的 Base64URL 字符串组成:

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

三部分分别是:

1. Header(头部)

1
2
3
4
{
"alg": "HS256",
"typ": "JWT"
}
  • alg:签名算法(HS256、RS256 等)
  • typ:类型,固定为 JWT

2. Payload(载荷)

1
2
3
4
5
6
7
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516242622,
"role": "admin"
}
  • 标准字段:iss(签发人)、sub(主题)、aud(受众)、exp(过期时间)、iat(签发时间)等
  • 自定义字段:可按需添加业务信息
  • ⚠️ Payload 是明文 Base64 编码,不是加密!不要存敏感信息

3. Signature(签名)

1
2
3
4
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)

签名的作用:防止数据篡改。服务端收到 JWT 后,用同样的 secret 对 header+payload 重新签名,对比是否一致。

通俗理解:JWT 像一张”防伪标签”——你可以看到上面的字(payload),但你想改上面的字,签名就对不上了,会被发现。

JWT 工作流程

1
2
3
4
5
6
7
8
9
10
11
12
sequenceDiagram
participant C as 客户端
participant A as 认证服务
participant B as 业务服务

C->>A: 用户名密码登录
A->>A: 验证身份,生成 JWT(签名)
A->>C: 返回 JWT
Note over C: 保存 JWT(localStorage / Cookie)
C->>B: 请求 API(Authorization: Bearer JWT)
B->>B: 验证签名 → 从 Payload 读用户信息
B->>C: 返回数据

HS256 vs RS256 签名算法选择

算法类型密钥场景
HS256对称加密同一密钥签名和验签单体应用、认证和业务服务在一起
RS256非对称加密私钥签名,公钥验签微服务(认证服务独立)、第三方信任

JWT 面试高频问题

1. JWT 如何注销?

JWT 无状态=无法从服务端主动使其失效。解决方案:

方案说明优缺点
客户端删除前端删除本地 JWT简单,但 JWT 在有效期内仍然可用(游离态)
黑名单维护一个”提前注销”的 JWT 列表✅ 实用,黑名单通常很小(只维护未过期但需要注销的)
短有效期设置短的过期时间(如 15 分钟)配合 Refresh Token,即使无法注销,影响窗口也小
修改用户密钥用户注销时修改其在服务端的 secret使之前签发的所有 JWT 失效,但会使所有”已登录”用户重新登录

最佳实践:短有效期 + 黑名单(只维护未过期的已注销 Token)+ Refresh 联动。

2. JWT 被盗怎么办?

防御措施说明
HTTPS传输层加密,防止中间人窃取
短有效期即使被盗,窗口期很短
客户端指纹生成 JWT 时绑定客户端特征(如 User-Agent、IP),被盗后无法在其他环境使用
存储在 Cookie 中并设置 HttpOnly防止 XSS 窃取
Refresh Token 轮换(Rotation)每次使用 Refresh Token 都发放一个新的,旧的立即失效

3. JWT 的安全攻击(现代必知)

攻击类型原理防御
“none” 算法攻击攻击者将 alg 改成 none,服务端若未禁用 none 算法则会跳过签名验证服务端必须禁用 none 算法
算法混淆攻击服务端期望 RS256(非对称),但攻击者用 HS256(对称),用已知的公钥作为 secret 签名固定算法类型,或始终使用非对称加密
密钥暴力破解使用弱密钥的 HS256 可能被暴力破解使用强密钥,优先 RS256

4. JWT 适合所有场景吗?

不适合的场合

  • 需要服务端主动控制的场景(如强制下线、踢人)
  • payload 过大会影响请求效率
  • 敏感信息不能放 payload

适合的场合

  • ✅ 分布式微服务(无状态,任意节点可验证)
  • ✅ 移动端(不支持 Cookie 的场景)
  • ✅ 第三方 API(携带用户身份)
  • ✅ 单点登录
对比维度Session CookieJWT
状态存放服务端客户端
伸缩性需统一 Session 存储(Redis)天然支持,任意节点可验签
注销能力✅ 服务端可直接删除 Session❌ 需黑名单或短有效期
CSRF❌ 需额外防护✅ 天然防御
跨域❌ 受 Cookie 同源限制✅ 不受限
实时性✅ 可随时修改或撤销❌ 修改权限后需等待 Token 过期
额外查询每次需要查 Session❌ 不需要,但载荷过大影响传输效率

OAuth 2.0 / SSO:第三方登录与单点登录

SSO(单点登录)的核心思想

一次登录,全网通行。你在公司内部系统中只需登录一次,就可以访问 HR 系统、代码仓库、日志平台等所有内部系统。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
sequenceDiagram
participant U as 用户
participant A as 系统A
participant B as 系统B
participant SSO as SSO 认证中心

U->>A: 访问系统A(未登录)
A->>SSO: 重定向到 SSO 中心
U->>SSO: 输入账号密码登录
SSO->>SSO: 创建全局会话(Cookie)
SSO->>A: 返回 token,A 创建本地会话
A->>U: 系统A首页

U->>B: 访问系统B(未登录)
B->>SSO: 重定向到 SSO 中心
SSO->>SSO: 检测到已登录(全局 Cookie)
SSO->>B: 直接返回 token(无需再输密码)
B->>U: 系统B首页

OAuth 2.0(面试重点)

OAuth 2.0 的本意是授权(Authorization)——允许第三方应用在用户授权下访问用户在其他平台的数据。最常见的应用场景是”使用微信/Google 账号登录”。

四种角色

角色现实类比例子
Resource Owner(资源所有者)你本人用户
Client(客户端)想借你书的同学第三方应用(”来记账” App)
Authorization Server(授权服务器)图书馆管理员微信/Google 认证服务
Resource Server(资源服务器)图书馆书库API 服务器(存储用户数据)

四种授权模式

模式安全等级适用场景
授权码模式(Authorization Code)⭐⭐⭐⭐⭐最常用,有后端的 Web 应用
隐式模式(Implicit)— 已弃用⭐⭐纯前端 SPA(不再推荐,用 PKCE 替代)
密码模式(Password)⭐⭐高度信任的第一方应用(如自家 App)
客户端凭证模式(Client Credentials)⭐⭐⭐⭐服务间调用(无用户参与)

授权码流程详解(标准版)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
sequenceDiagram
participant U as 用户(浏览器)
participant A as 第三方应用
participant G as Google 认证服务器

U->>A: 点击"使用 Google 登录"
A->>U: 重定向到 Google 授权页
U->>G: 询问"是否授权给该应用?"
U->>G: 点击"同意"
G->>U: 重定向回第三方应用(附带授权码 code)
U->>A: 将 code 交给前端 → 后端
A->>G: 后端用 code + client_secret 换取 access_token
G->>A: 返回 access_token
A->>G: 用 access_token 请求用户信息
G->>A: 返回用户信息(姓名、邮箱等)
A->>U: 登录成功

为什么需要授权码这个中间步骤?
因为 Access Token 不能暴露在浏览器——授权码是一次性的,由后端服务器凭 client_secret 去换取 Token。这样即使恶意脚本截取了授权码,没有 client_secret 也换不到 Token。

PKCE(Proof Key for Code Exchange)— 移动端必用

纯前端应用(SPA、移动 App)无法安全保存 client_secret,因此有了 PKCE 扩展:

1
2
3
4
5
6
7
8
9
10
11
sequenceDiagram
participant A as 移动 App
participant G as 认证服务器

A->>A: 生成随机 code_verifier(长字符串)
A->>A: code_challenge = SHA256(code_verifier)
A->>G: 请求授权(附带 code_challenge)
G->>A: 返回授权码 code
A->>G: 用 code + code_verifier 换 Token
G->>G: 验证 SHA256(code_verifier) == code_challenge
G->>A: 返回 access_token

OAuth 2.0 vs OpenID Connect (OIDC)

对比维度OAuth 2.0OpenID Connect (OIDC)
目的授权(允许访问资源)认证(确认身份)+ 授权
产出Access TokenAccess Token + ID Token(JWT 格式)
用户身份不标准化,需要额外 API 获取标准化的 JWT 包含用户信息
协议层级基础协议OAuth 2.0 之上的认证层

通俗理解:OAuth 2.0 是”我给你一把钥匙,你可以进我的储物间”;OIDC 是”我不仅给你钥匙,还给你一张身份证,证明你确实是你”。

OAuth 2.0 vs OpenID

  • OpenID 1.0/2.0:只做认证,不授权。允许用同一身份登录不同网站,但无权访问你的数据
  • OAuth 2.0:做授权。允许第三方访问你的数据(如读取你的 Google 日历)
  • OIDC:在 OAuth 2.0 之上加了一层认证,是目前的主流方案

你可以理解为:OAuth 解决”你能做什么”(授权),OpenID 解决”你是谁”(认证)。


面试高频问题汇总

按面试出现频率排列,方便快速复习:

⭐⭐⭐⭐⭐(必考)

  1. Cookie、localStorage、sessionStorage 的区别?

    • 容量:4KB vs 5MB vs 5MB
    • 生命周期:可设置 vs 永久 vs 关闭即清理
    • HTTP 携带:自动带 vs 不参与通信 vs 不参与通信
    • 安全性:HttpOnly 可防 XSS vs JS 直接读取风险高
  2. Session 和 JWT 的区别?各有什么优缺点?

    • Session:有状态,服务端存 → 易注销,需统一存储(Redis),CSRF 需防护
    • JWT:无状态,客户端存 → 难注销,天然防 CSRF,跨域友好,但无法主动失效
    • 见 JWT vs Session Cookie 对比表
  3. JWT 由哪几部分组成?如何保证不被篡改?

    • Header + Payload + Signature
    • 服务端用 secret 重新对 header+payload 签名,对比是否一致

⭐⭐⭐⭐(高频)

  1. Token 过期如何处理?Refresh Token 的作用?

    • 401 → 用 Refresh Token 换新 Access Token → 重试原请求
    • Refresh Token 长期有效,负责身份认证;Access Token 短期有效,负责资源访问
    • 职责分离:Access 丢失影响小,Refresh 通常存在服务端
  2. 如何防止 Token 被盗?

    • HTTPS 传输、短有效期、客户端指纹绑定、HttpOnly Cookie 存储、Refresh Token 轮换
  3. SSO 的实现原理?

    • 一个统一的认证中心,各子系统不再各自认证
    • 同域 SSO:共享 Cookie;跨域 SSO:CAS(Central Authentication Service)/ OAuth
    • 关键点:全局会话 vs 局部会话

⭐⭐⭐(中等频率)

  1. OAuth 2.0 授权码流程是怎样的?为什么需要授权码?

    • 用户同意 → 返回 code → 后端用 code+secret 换 Token
    • 授权码一次性的,防止 Token 暴露在浏览器端
  2. SameSite Cookie 解决了什么问题?

    • 解决 CSRF 攻击。Lax 允许 GET 导航携带,Strict 完全禁止,None 不限(需 HTTPS)
  3. 前端如何安全地存储 Token?

    • Access Token 放内存变量,Refresh Token 放 HttpOnly Cookie
    • 纯 localStorage 方案必须做好 XSS 防御

⭐⭐(了解即可)

  1. OIDC 和 OAuth 2.0 的关系?

    • OIDC = OAuth 2.0 + 身份认证层(ID Token),解决 OAuth 不标准化用户身份的问题
  2. 什么是 PKCE?为什么移动端需要它?

    • 纯前端应用无法安全保存 client_secret,用 code_verifier 证明请求合法性
  3. JWT 中的 “none” 算法攻击?

    • 攻击者将 alg 改为 none 绕过签名验证,服务端必须禁用 none

总结回顾

用一句话串联全文:

Cookie 是浏览器”帮你带”纸条,所以有 CSRF 风险;localStorage 是你”自己放”纸条,所以有 XSS 风险;Session 是服务端记小本本,有状态但好注销;JWT 是把小本本的内容印在工牌上,无状态但难回收;它们组合起来加上OAuth 2.0 和 SSO,就构成了前端鉴权的完整拼图。

核心原则

  1. HTTPS 是基础 —— 没有 HTTPS,所有鉴权方案都白搭
  2. 没有绝对安全,只有合适的权衡 —— JWT 方便但有注销问题,Session 安全但扩展性差
  3. 纵深防御 —— 不要依赖单一防御手段(仅用 SameSite 不够,XSS 防护也要做好)
  4. 最小权限 —— 只给必要的 Token 权限,有效期尽量短

最后更新:2026 年 6 月 · 前端鉴权知识点终稿

只是用户看到仅仅只是浏览器本身,却很少能看到浏览器最核心的部分—浏览器内核。从第一款 libwww(Library WorldWideWeb)浏览器发展至今已经经历了无数竞争与淘汰了。现在国内常见的浏览器有:IE、Firefox、QQ 浏览器、Safari、Opera、Google Chrome、百度浏览器、搜狗浏览器、猎豹浏览器、360 浏览器、UC 浏览器、遨游浏览器、世界之窗浏览器等。但目前最为主流浏览器有五大款,分别是 IE、Firefox、Google Chrome、Safari、Opera。

阅读全文 »

AJAX 技术使开发者能够专注于互联网中数据的传输,而不再拘泥于数据传输的载体。通过它,我们获取数据的方式变得更加灵活,可控和优雅。但 AJAX 技术并不是一把万能钥匙,互联网中的数据隐私和数据安全(例如银行账号和密码)非常重要,为了保护用户数据的隐私与安全,浏览器使用同源策略限制了 AJAX 技术获取数据的范围和能力。但有时我们不得不想办法绕过同源策略,实现跨域请求资源。因此跨域技术一直成为开发者们经久不衰的讨论话题。

阅读全文 »

DNS(Domain Name System,域名系统)是互联网最重要的基础设施之一,它负责将人类可读的域名转换为机器可识别的 IP 地址。本文将从基础到进阶,全面讲解 DNS 的工作原理、记录类型及安全机制。

阅读全文 »

CDN(Content Delivery Network,内容分发网络)是现代互联网基础设施的重要组成部分。本文将从概念到实践,全面讲解 CDN 的工作原理、关键技术及应用场景。

阅读全文 »

目录

  1. 开篇:PWA 到底是啥?
  2. PWA 概览:核心技术一图看懂
  3. Web App Manifest:把网站变成”可安装的 App”
  4. Service Worker:浏览器背后的隐形代理
  5. Cache Storage + Service Worker:离线缓存的实战模式
  6. Service Worker 的生命周期详解
  7. Workbox:生产环境的最佳实践
  8. 消息推送(Web Push)
  9. PWA 面试高频问题
  10. 总结回顾

开篇:PWA 到底是啥?

想象你去一家餐厅吃饭:

  1. 不用下载 App(渐进式)——你扫码就能看到电子菜单,不用先装一个”点餐 App”
  2. 没网也能看菜单(离线能力)——你提前打开了菜单,在地下室也能翻看
  3. 能收到推送(消息通知)——“您的餐已备好”会弹到你的手机通知栏
  4. 可以放到桌面(可安装)——你觉得这家店不错,把它的网页”存”到手机桌面,下次像 App 一样一点就开

这就是 PWA(Progressive Web Application,渐进式网页应用)——把 Web 的”随手可得”和 Native App 的”好用能力”结合在一起。

PWA 不是一门新技术,而是一组技术的组合,核心有三:

技术作用类比
Web App Manifest让网页可安装到桌面给网页一个”身份证”
Service Worker离线缓存 + 消息推送 + 网络代理浏览器背后的”隐形管家”
Web Push服务器向用户推送通知餐厅服务员来通知你

❗ 面试常问:PWA 和 Native App 相比的优缺点?
✅ 优点:无需安装、即时更新、可被搜索引擎索引、跨平台、体积极小
❌ 缺点:无法访问部分原生 API(蓝牙、NFC 等)、iOS 支持有限、后台保活能力弱于原生


PWA 概览:核心技术一图看懂

1
2
3
4
5
6
用户操作          PWA 能力          实现技术
──────────────────────────────────────────
打开网页 ───→ 可以离线浏览 ───→ Service Worker + Cache Storage
添加到桌面 ───→ 像 App 一样启动 ───→ Web App Manifest
推送通知 ───→ 消息弹窗提醒 ───→ Web Push API + Service Worker
页面秒开 ───→ 缓存优先加载 ───→ Service Worker 策略

PWA 的核心门槛是 HTTPS——Service Worker 只能在 HTTPS 下工作(本地调试的 localhost 除外)。没有 HTTPS,PWA 一切免谈。


Web App Manifest:把网站变成”可安装的 App”

什么是 Manifest

manifest.json 是一个 JSON 文件,告诉浏览器:这个网站可以像一个原生应用一样被”安装”到用户设备上。

1
2
<!-- 在 HTML 中引用 -->
<link rel="manifest" href="/manifest.json">

核心字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"name": "我的应用",
"short_name": "我的应用",
"description": "这是一个 PWA 示例应用",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#3f51b5",
"icons": [
{
"src": "/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
字段说明面试频率
start_url从桌面图标打开时显示的页面⭐⭐
display显示模式:standalone(无浏览器栏,像 App)、fullscreenminimal-uibrowser⭐⭐⭐
background_color启动时的背景色(在 Splash 屏显示)
theme_color浏览器地址栏、任务栏的颜色
icons各分辨率的应用图标,至少 192x192 和 512x512⭐⭐
scope哪些路径在 PWA 范围内,超出则用浏览器打开⭐⭐

❗ 面试题:PWA 的可安装条件是什么?

  1. manifest.json(包含 nameshort_nameiconsstart_urldisplay
  2. 注册了 Service Worker
  3. 使用 HTTPS
  4. 用户有交互行为(不能一打开页面就弹安装提示)

安装到桌面的流程

1
2
3
4
5
6
7
8
9
sequenceDiagram
participant U as 用户
participant B as 浏览器
participant P as PWA

B->>U: 检测到 manifest + SW,弹出"添加到主屏幕"提示
U->>B: 点击"安装"
B->>P: 创建独立的应用窗口(无浏览器工具栏)
Note over P: 桌面上出现图标,点击后以 standalone 模式启动

Service Worker:浏览器背后的隐形代理

什么是 Service Worker

Service Worker(简称 SW) 是浏览器在后台独立运行的一个脚本,本质是一个 Web Worker,但它比普通 Web Worker 多了拦截网络请求和离线缓存的能力。

1
2
3
4
5
6
graph LR
A[浏览器] -->|网络请求| B[Service Worker]
B -->|缓存命中| C[Cache Storage]
B -->|缓存未命中| D[网络服务器]
D --> B
B --> A

通俗理解:Service Worker 像一个”交通警察”站在浏览器和网络之间——它可以决定”这个请求走缓存”还是”这个请求走网络”。

与 Web Worker 的区别

特性Web WorkerService Worker
目的多线程计算网络代理 + 离线缓存
生命周期随页面关闭而结束独立于页面,浏览器可唤醒/终止
可拦截请求
可离线使用
可推送通知
可访问 DOM
HTTPS 要求✅(localhost 除外)

Service Worker 的特点

  1. 独立于页面:SW 安装后即使页面关闭也在后台运行
  2. 不能访问 DOM:SW 运行在与页面隔离的线程中
  3. 全使用 Promise/异步:SW 中几乎所有 API 都是异步的
  4. 可拦截请求:通过 fetch 事件拦截所有网络请求
  5. 只能 HTTPS:安全原因,SW 只能在 HTTPS 下注册(localhost 除外)

注册一个 Service Worker

1
2
3
4
5
6
7
8
9
10
11
// 主线程中注册
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const registration = await navigator.serviceWorker.register('/sw.js')
console.log('SW 注册成功,作用域:', registration.scope)
} catch (err) {
console.log('SW 注册失败:', err)
}
})
}

关键点:

  • scope(作用域)/sw.js 默认作用域是 ./(即 sw.js 所在目录)。如果想扩大作用域,需要在注册时指定 { scope: '/' },并且服务端返回的 Service-Worker-Allowed 头需要允许该作用域
  • 注册时机:建议在 load 事件后注册,避免与页面渲染竞争资源

sw.js 基础结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// sw.js — Service Worker 脚本
const CACHE_NAME = 'my-app-v1'

// 安装:预缓存静态资源
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll([
'/',
'/styles/main.css',
'/scripts/app.js',
'/images/logo.png',
])
})
)
})

// 激活:清理旧缓存
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((name) => name !== CACHE_NAME)
.map((name) => caches.delete(name))
)
})
)
})

// 拦截请求:缓存优先,网络兜底
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cachedResponse) => {
// 缓存命中则直接返回
if (cachedResponse) return cachedResponse
// 否则发网络请求
return fetch(event.request).then((networkResponse) => {
// 只缓存成功的 GET 请求
if (networkResponse && networkResponse.status === 200 && event.request.method === 'GET') {
const clone = networkResponse.clone()
caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone))
}
return networkResponse
})
})
)
})

Cache Storage + Service Worker:离线缓存的实战模式

Service Worker 配合 Cache Storage API 实现离线缓存。缓存策略的选择直接影响用户体验和新鲜度。

五种缓存策略

策略行为适用场景
Cache First(缓存优先)有缓存直接用,没有就走网络不常变的静态资源(CSS、JS 字体)
Network First(网络优先)先请求网络,失败就用缓存需保持较新但可接受旧数据的场景(文章列表)
Stale-While-Revalidate(迂回策略)立即返回缓存,同时后台发起网络请求更新缓存最常用,兼顾速度和新鲜度
Network Only只走网络实时数据(股票行情、聊天消息)
Cache Only只读缓存预缓存资源(已确定不变的静态文件)

⭐ Stale-While-Revalidate:面试最常考的策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sequenceDiagram
participant B as 页面
participant SW as Service Worker
participant C as 缓存
participant N as 网络

B->>SW: 请求 /js/app.js
SW->>C: 查缓存
C->>SW: 返回缓存版本(可能旧的)
SW->>B: ✅ 立即返回缓存(页面秒开)
SW->>N: 同时在后台发起网络请求
N->>SW: 返回最新版本
SW->>C: 更新缓存(下次用户就能看到最新的)
Note over SW: 用户完全无感知

为什么 Stale-While-Revalidate 最好?——用户永远不会等待网络,同时资源又能及时更新。

示例:对不同资源的差异化策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// sw.js
self.addEventListener('fetch', (event) => {
const { request } = event
const url = new URL(request.url)

// 1. 图片资源 → Cache First
if (/\.(png|jpg|jpeg|gif|svg|webp)$/i.test(url.pathname)) {
event.respondWith(cacheFirst(request))
return
}

// 2. API 请求 → Network First
if (url.pathname.startsWith('/api/')) {
event.respondWith(networkFirst(request))
return
}

// 3. 页面 HTML → Stale-While-Revalidate
event.respondWith(staleWhileRevalidate(request))
})

async function cacheFirst(request) {
const cached = await caches.match(request)
return cached || fetch(request)
}

async function networkFirst(request) {
try {
const response = await fetch(request)
// 更新缓存
const cache = await caches.open(CACHE_NAME)
cache.put(request, response.clone())
return response
} catch {
const cached = await caches.match(request)
if (cached) return cached
// 都失败了,返回离线页面
return caches.match('/offline.html')
}
}

async function staleWhileRevalidate(request) {
const cache = await caches.open(CACHE_NAME)
const cachedResponse = await cache.match(request)

const fetchPromise = fetch(request)
.then((networkResponse) => {
cache.put(request, networkResponse.clone())
return networkResponse
})
.catch(() => cachedResponse) // 网络失败就不更新

return cachedResponse || fetchPromise
}

Service Worker 的生命周期详解

五大状态

1
installing → installed (waiting) → activating → activated → redundant
状态触发条件关键操作
installingSW 注册后触发 install 事件预缓存静态资源
installed (waiting)install 完成,等待旧 SW 控制的页面关闭可调用 self.skipWaiting() 跳过等待
activating旧 SW 不再控制任何页面,触发 activate 事件清理旧版本缓存
activatedactivate 完成,SW 开始拦截请求可调用 self.clients.claim() 立即接管所有页面
redundant被新 SW 替换,或安装失败
1
2
3
4
5
6
7
8
stateDiagram-v2
[*] --> installing: register()
installing --> installed: install 事件完成
installed --> activating: 旧 SW 页面关闭 / skipWaiting()
activating --> activated: activate 事件完成
activated --> [*]: 被新 SW 替换
activated --> redundant: 手动卸载 / 新 SW 接管
installing --> redundant: 安装失败

关键 API:skipWaiting 和 clients.claim

1
2
3
4
5
6
7
8
9
10
// sw.js
self.addEventListener('install', (event) => {
// 跳过 waiting 状态,新 SW 安装后立即激活
event.waitUntil(self.skipWaiting())
})

self.addEventListener('activate', (event) => {
// 立即接管所有已打开的页面(无需等待页面刷新)
event.waitUntil(self.clients.claim())
})

更新 Service Worker

当浏览器检测到 sw.js字节级差异时(哪怕一个空格变了),就会触发更新流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
sequenceDiagram
participant SW_new as 新版 SW
participant SW_old as 旧版 SW(运行中)
participant B as 浏览器

Note over SW_new: 检测到 sw.js 字节变化
SW_new->>SW_new: 安装(install 事件)
Note over SW_new: 进入 waiting 状态
B->>SW_old: 旧页面仍在运行,SW_old 控制中
Note over B: 所有旧标签页关闭
SW_old->>SW_old: 停止控制
SW_new->>SW_new: 激活(activate 事件)
SW_new->>B: 开始拦截请求

**如果用了 skipWaiting()**:新 SW 安装后立即激活,但已打开的页面不会自动被接管——需要页面调用 navigator.serviceWorker.controller 变化后刷新,或在 activate 中调用 clients.claim()


Workbox:生产环境的最佳实践

什么是 Workbox

Workbox 是 Google 官方的 PWA 框架,封装了 Service Worker 的常见功能,让你不用手写 sw.js。配合 webpack 插件 workbox-webpack-plugin 可以自动化生成 SW 配置。

安装与使用

1
npm install workbox-webpack-plugin --save-dev
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// webpack.config.js
const WorkboxPlugin = require('workbox-webpack-plugin')

module.exports = {
plugins: [
new WorkboxPlugin.GenerateSW({
clientsClaim: true, // 立即接管页面
skipWaiting: true, // 跳过 waiting 状态
cleanupOutdatedCaches: true, // 清理旧版本缓存
runtimeCaching: [
{
urlPattern: /\.(js|css)$/,
handler: 'StaleWhileRevalidate', // 最推荐的策略
options: {
cacheName: 'static-resources',
expiration: {
maxEntries: 50,
maxAgeSeconds: 30 * 24 * 60 * 60, // 30天
},
},
},
{
urlPattern: /\.(png|jpg|jpeg|gif|svg|webp)$/,
handler: 'CacheFirst',
options: {
cacheName: 'images',
expiration: {
maxEntries: 100,
maxAgeSeconds: 60 * 24 * 60 * 60, // 60天
},
},
},
{
urlPattern: /\/api\//,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
networkTimeoutSeconds: 3, // 网络超时则用缓存
expiration: {
maxEntries: 50,
maxAgeSeconds: 5 * 60, // 5分钟
},
},
},
],
}),
],
}

Workbox 的三种引入方式

方式说明适用场景
cdn从 Google CDN 引入 workbox 运行时❌ 国内不可用
local插件在本地生成 workbox 代码,一起部署项目发布时就确定了
disabled自己指定引入路径推荐,可自建 CDN

调试技巧

  • Chrome DevTools → Application → Service Workers:查看 SW 状态、手动更新/卸载
  • Application → Cache Storage:查看缓存了哪些资源
  • **勾选 “Bypass for network”**:跳过 SW,直接走网络(本地开发时用)
  • Chrome DevTools → Audits / Lighthouse:检测 PWA 合格情况

消息推送(Web Push)

工作原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
sequenceDiagram
participant B as 浏览器
participant SW as Service Worker
participant S as 服务器
participant Push as 推送服务

Note over B,SW: 1. 订阅推送
B->>SW: 注册推送
SW->>Push: 生成 PushSubscription
Push->>SW: 返回 subscription 对象(包含 endpoint)
SW->>S: 将 subscription 发送到业务服务器

Note over S,Push: 2. 发送推送
S->>Push: POST 推送消息(带上 subscription)
Push->>SW: 触发 push 事件
SW->>B: showNotification() 显示通知

Note over B: 用户看到通知弹窗

前端订阅推送

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 主线程
async function subscribePush() {
const registration = await navigator.serviceWorker.ready

// 请求桌面通知权限
const permission = await Notification.requestPermission()
if (permission !== 'granted') return

// 获取 VAPID 公钥(服务端生成)
const publicKey = 'BEl62i2Rl...'

const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true, // 必须为 true,保证每条推送用户都能看到
applicationServerKey: urlBase64ToUint8Array(publicKey),
})

// 将 subscription 发送给业务服务器
await fetch('/api/push/subscribe', {
method: 'POST',
body: JSON.stringify(subscription),
})
}

// Base64 → Uint8Array 转换函数
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4)
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/')
const rawData = window.atob(base64)
return Uint8Array.from([...rawData].map((char) => char.charCodeAt(0)))
}

Service Worker 接收推送

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// sw.js
self.addEventListener('push', (event) => {
const data = event.data ? event.data.json() : { title: '默认标题', body: '默认内容' }

const options = {
body: data.body,
icon: '/icons/icon-192x192.png',
badge: '/icons/badge.png',
data: { url: data.url }, // 点击通知后打开的链接
}

event.waitUntil(self.registration.showNotification(data.title, options))
})

// 点击通知
self.addEventListener('notificationclick', (event) => {
event.notification.close()
const url = event.notification.data?.url || '/'
event.waitUntil(clients.openWindow(url))
})

VAPID 协议

Web Push 使用 VAPID(Voluntary Application Server Identification) 协议,用于标识推送服务器:

  • 服务端生成一对 VAPID 公私钥
  • 公钥暴露给前端(applicationServerKey
  • 私钥由服务端自己持有,用于向推送服务(如 Chrome 的 FCM)认证身份

PWA 面试高频问题

⭐⭐⭐⭐(高频)

  1. PWA 的核心技术有哪些?

    • Web App Manifest(可安装)、Service Worker(离线缓存+网络代理)、Web Push(消息推送)
    • 基础设施:HTTPS
  2. Service Worker 是什么?和 Web Worker 有什么区别?

    • SW:浏览器后台脚本,可拦截网络请求、缓存资源、推送通知
    • 区别:SW 可离线、可拦截请求、生命周期独立于页面;Web Worker 只做计算
  3. Service Worker 的声明周期是怎样的?

    • installing → installed(waiting) → activating → activated → redundant
    • skipWaiting() 跳过 waiting
    • clients.claim() 立即接管页面
  4. 缓存策略有哪些?分别适用于什么场景?

    • Cache First(静态资源)、Network First(API)、Stale-While-Revalidate(HTML/JS)、Network Only(实时数据)、Cache Only(预置资源)

⭐⭐⭐(中等频率)

  1. PWA 的可安装条件是什么?

    • manifest.json(name/icons/start_url/display)、注册 SW、HTTPS、用户交互
  2. 如何更新 Service Worker 的缓存?

    • 浏览器检测 sw.js 字节变化 → 触发新 SW 安装 → activate 中清理旧缓存 → clients.claim() 接管页面
    • 资源文件名加 hash(webpack 自动处理),确保请求 URL 变化
  3. Web Push 推送的流程?

    • 订阅(pushManager.subscribe)→ 存储 subscription → 服务器发推送 → SW 的 push 事件 → showNotification

⭐⭐(了解即可)

  1. PWA 在 iOS 上的支持情况?

    • iOS 11.3+ 开始支持 SW,但功能有限(无推送、后台同步不完善)
    • 安装到桌面支持,但体验不如 Android 完整
  2. Workbox 的核心作用是什么?

    • 封装 SW 的常见模式(缓存策略、更新逻辑),配合 webpack 自动化生成 sw.js
    • 提供 GenerateSW(自动)和 InjectManifest(手动高级配置)两种模式

总结回顾

PWA 的完整工作流

1
2
3
4
5
6
7
8
9
10
11
用户访问你的网站

浏览器检测 manifest.json → 弹出"添加到主屏幕"提示

用户点击安装 → 桌面生成图标(standalone 模式启动)

Service Worker 在后台安装 → install 事件预缓存静态资源

用户浏览 → SW 拦截请求 → 根据策略返回缓存或网络数据

浏览器后台 → SW 接收推送消息 → 显示通知栏提醒

核心原则

  1. 渐进增强:PWA 在不支持的浏览器上就是个普通网站,不会报错
  2. HTTPS 是一切的基础
  3. 缓存策略要因资源而异:不要把所有资源都用同一种策略
  4. **离线的底线是”有总比没有好”**:至少准备一个简单的离线页面

PWA 现状

平台SW 支持推送支持安装支持评价
Chrome(Android)✅ 完整最佳体验
Safari(iOS)✅ 11.3+✅(有限)功能受限
Firefox完善
Edge完善

最后更新:2026 年 6 月 · PWA 知识点终稿