0%

前言

在现代 Web 应用中,跨站脚本攻击(XSS)依旧是最常见且危害最大的漏洞之一。即使前端开发者已经做过各种输入过滤和转义工作,攻击者仍可能通过第三方库、广告脚本或 DOM 操作将恶意代码注入页面。内容安全策略(CSP)应运而生,它是一种强大的防御机制,用于指定允许加载的资源来源,从而有效降低 XSS 风险。

本篇文章将深入探讨 CSP 的各项配置、典型实践以及排查技巧。内容将覆盖:

  1. CSP 基础概念与语法
  2. 指令详解(script-src、style-src、connect-src 等)
  3. 报告功能和调试方法
  4. 常见坑与绕过方式
  5. 实战范例:从无到有逐步增强
  6. 与其他安全机制的配合使用

希望读者通过本文,既能在项目中正确配置 CSP,又能在遇到阻断问题时进行有效排查。

什么是 CSP?

CSP 全称 Content Security Policy,是通过 HTTP 响应头(或 <meta> 标签)下发的一组规则。浏览器在加载页面时,会根据这些规则决定哪些资源可以被加载和执行,哪些需要被阻止。

浏览器解析 CSP 时,会检查页面中的:

  • 脚本(包括内联、外链、动态创建的脚本)
  • 样式表(内联、外链)
  • 图像、字体、媒体资源
  • AJAX 请求
  • 其他如 frame, object, worker

若某项资源不符合 CSP 指定的来源列表,则会被禁止加载或执行,并可选择性地生成违规报告。

CSP through HTTP header

一般通过服务器设置:

1
Content-Security-Policy: default-src 'self'; script-src 'self' https://apis.example.com; object-src 'none'; report-uri /csp-report-endpoint

或者使用强化的 Content-Security-Policy-Report-Only 头来先监测效果而不阻断。

CSP 语法与指令

CSP 规则由若干指令组成,每个指令控制一种资源类型,并附带允许的源列表。以下指令是常用的:

  • default-src:默认资源策略,若其他指令未指定,则使用此策略。
  • script-src:脚本来源。
  • style-src:样式来源。
  • img-src:图像来源。
  • connect-src:通过 XHR、Fetch 或 WebSocket 连接的目标。
  • font-src:字体来源。
  • object-src<object><embed><applet> 等元素的来源。
  • media-src:音视频来源。
  • frame-src / child-src<frame><iframe> 的来源。
  • worker-src:Web Worker 和 Shared Worker 来源。
  • manifest-src:Web App Manifest 的来源。
  • base-uri:允许的 <base> 元素 URL。
  • form-action:允许的表单提交目标。
  • frame-ancestors:允许作为 <iframe> 嵌入的父页面。
  • sandbox:启用沙箱特性。

源表达式

源列表中的项可以是:

  • 'self':当前源
  • 'unsafe-inline':允许内联资源(不安全)
  • 'unsafe-eval':允许 eval() 等执行代码 (极不安全)
  • 'none':不允许任何来源
  • URL 协议 + 域名(如 https://cdn.example.com
  • 通配符 * 或子域通配 *.example.com
  • 数据 URI data:

例如:

1
script-src 'self' 'unsafe-inline' https://cdn.example.com

表示脚本可以来自当前域、允许内联脚本、以及 cdn.example.com

脚本相关指令详解

脚本是 XSS 的主要攻击载体,因此 script-srcnonce / hash 的运用非常关键。

内联脚本限制

默认情况下,script-src 'self' 会阻止所有内联脚本(例如 <script>alert(1)</script>),这对许多旧版库或模板生成器来说是个大问题。可以通过三种方式允许特定内联脚本:

  1. unsafe-inline:完全允许,不安全
  2. 指定 nonce-<random>:给 <script> 添加随机值且在 HTTP 头中声明
  3. 使用 sha256-<hash>:根据脚本内容哈希

示例:

1
Content-Security-Policy: script-src 'nonce-abc123' 'strict-dynamic' https:;

在上述规则下,浏览器仅允许带有 nonce="abc123" 属性的脚本执行,同时允许经过 strict-dynamic 处理的后续加载脚本来自任意 HTTPS 源。这种组合方式既保证了安全性,又兼顾了灵活性。

使用哈希

另一种更细粒度的控制方式是使用哈希值:

1
Content-Security-Policy: script-src 'sha256-AbCdEf123456...';

如果内联脚本内容的 SHA256 哈希与头部指定值匹配,则浏览器允许执行。哈希方法适用于不便修改脚本标签但内容固定的场景。

‘unsafe-eval’ 和动态代码

'unsafe-eval' 会允许 eval()new Function() 等运行时编译方法,这对 XSS 风险极大,因此应尽量避免。可以通过工具(如 CSP evaluator)检查代码是否依赖它,并寻求替代方案。

样式相关安全

style-src 指令则控制 CSS 的来源。与脚本类似,它也存在内联样式的问题。

1
Content-Security-Policy: style-src 'self' 'unsafe-inline';

默认情况下仅允许外部样式表,引入 'unsafe-inline' 会允许 <style> 标签和 style="" 属性,但应与严格的规则结合,或通过哈希/nonce 方式进行授权。

资源加载和网络请求

img-srcfont-src 等指令控制静态资源,而 connect-src 影响跨域请求。它可以防止敏感 API 被劫持:

1
Content-Security-Policy: connect-src 'self' https://api.example.com;

该配置禁止页面向非指定域提交 AJAX 请求,有助于限制数据泄露。

报告机制

CSP 可以在检测到违规时发送报告,以便分析攻击活动。可通过 report-urireport-to 指令设置:

1
Content-Security-Policy: default-src 'self'; report-uri /csp-report-endpoint

实际项目中可使用专门的收集服务(例如 Sentry、Google CSP Evaluator)来集中管理这些报告。

实战案例:逐步强化

  1. 初期部署: 使用 Report-Only 模式监测现有内容是否合规。
1
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report
  1. 修复违规: 根据报告调整代码或导航到允许的来源。
  2. 全面启用: 一旦无明显违规,切换到正式 CSP 头并移除 Report-Only

常见坑与绕过

  • 误用通配符(*)会放宽策略,建议避免。
  • 使用 data: URI 时需谨慎,它可能被用于注入。
  • frame-ancestors 忽略时页面可被钓鱼站点嵌套。

攻击者可能通过 CSP 缺陷利用 CSP Header 注入、clickjacking、或者利用浏览器漏洞绕过策略。因此,CSP 应与其他安全措施(如输入验证、HTTP-only Cookie)共同部署。

与其他安全机制的协同

机制 功能 优势
CSP 限制资源来源 防范 XSS、减少数据泄露
X-Content-Type-Options 防止 MIME 屏蔽 避免脚本伪装
X-Frame-Options / frame-ancestors 防止点击劫持 提升安全性
Referrer-Policy 控制引用头 保护隐私

综合使用这些头部,形成多层防护。

工具与资源

总结

CSP 是强大的内容安全策略,通过合理规划和逐步实施,它可以显著降低 XSS 等资源注入风险。虽然配置和调试稍有挑战,但结合报告和自动化工具,团队可以快速迭代,最终建立起安全可靠的前端防线。

继续保持警惕,定期回顾策略并监控违规报告,是保证长久安全的关键。

为什么需要缓存策略

即便使用了 Service Worker,如何准确缓存资源、更新逻辑以及清除过期内容,都是开发者必须应对的挑战。未优化的缓存策略会导致旧资源长期存在、用户获取不到最新内容,甚至出现版本冲突。

本篇将从基础到进阶详解多种常见缓存策略,讨论它们的适用场景、优缺点,并逐步构建一套可扩展的缓存体系。

核心 API 回顾

在开始之前,复习一下 Service Worker 提供的缓存相关 API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const cacheName = 'my-app-v1';

self.addEventListener('install', event => {
event.waitUntil(
caches.open(cacheName).then(cache => cache.addAll([
'/index.html',
'/styles/main.css',
'/scripts/app.js'
]))
);
});

self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request);
})
);
});

caches.matchcaches.opencache.putcache.delete 是组成缓存策略的基本工具。

策略一:Cache First

最简单的策略:优先从缓存读取,缓存命中则直接返回,否则请求网络并缓存结果。

适用场景:静态资源、图片、第三方库等。

优点:离线可用、速度快。

缺点:更新复杂,需要手动管理缓存版本。

完整实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const cacheName = 'static-v1';

self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(cachedResponse => {
if (cachedResponse) {
return cachedResponse;
}
return fetch(event.request).then(networkResponse => {
if (networkResponse.status === 200) {
const responseClone = networkResponse.clone();
caches.open(cacheName).then(cache => {
cache.put(event.request, responseClone);
});
}
return networkResponse;
});
})
);
});

策略二:Network First

优先尝试从网络获取,只有在失败时才回退到缓存。

适用于:API 数据、新闻、动态内容。

优点:保证数据最新。

缺点:网络延迟大时体验差;离线时可能无响应(除非缓存了上一次数据)。

实现示例:

1
2
3
4
5
6
7
8
9
10
11
self.addEventListener('fetch', event => {
event.respondWith(
fetch(event.request)
.then(networkResponse => {
const responseClone = networkResponse.clone();
caches.open(cacheName).then(cache => cache.put(event.request, responseClone));
return networkResponse;
})
.catch(() => caches.match(event.request))
);
});

策略三:Stale-while-revalidate

既返回缓存内容提升速度,同时异步请求最新资源并更新缓存。

适用于需要平衡快速响应与数据更新的场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
self.addEventListener('fetch', event => {
event.respondWith(
caches.open(cacheName).then(cache => {
return cache.match(event.request).then(cachedResponse => {
const networkFetch = fetch(event.request).then(networkResponse => {
if (networkResponse.status === 200) {
cache.put(event.request, networkResponse.clone());
}
return networkResponse;
});
return cachedResponse || networkFetch;
});
})
);
});

策略四:Cache and Network Race

同时发起缓存和网络请求,只要先返回者即可,另一个仍然需要写入缓存。

适合对时效要求高,并且可以容忍某些冗余请求的场景。

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
self.addEventListener('fetch', event => {
event.respondWith(
new Promise((resolve, reject) => {
let rejected = false;
const abortController = new AbortController();

const failOnce = () => {
if (rejected) {
reject('both failed');
} else {
rejected = true;
}
};

caches.match(event.request).then(response => {
if (response) {
resolve(response);
} else {
failOnce();
}
}).catch(failOnce);

fetch(event.request, { signal: abortController.signal })
.then(networkResponse => {
resolve(networkResponse);
caches.open(cacheName).then(cache => cache.put(event.request, networkResponse.clone()));
abortController.abort();
})
.catch(failOnce);
})
);
});

缓存版本管理与清理

当应用升级时,旧缓存可能过时。常见做法是在 installactivate 事件中删除旧缓存:

1
2
3
4
5
6
7
8
9
10
self.addEventListener('activate', event => {
const currentCaches = ['static-v2', 'dynamic-v1'];
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.filter(name => !currentCaches.includes(name)).map(name => caches.delete(name))
);
})
);
});

此外,可以为缓存资源设置 maxEntriesmaxAgeSeconds,通过定期清理确保不占用过多空间。

离线优先策略组合

对于绝大多数前端应用,我们会采用混合策略:

  1. 基本静态资源 使用 Cache First
  2. API 数据 使用 Network First 或 Stale-while-revalidate
  3. 大型媒体 可采用 Cache First 并加上容量控制

通过配置不同的路由规则(如通过 workbox 插件)可简化工
具化配置。下面示例展示 Workbox 的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// workbox-config.js
module.exports = {
runtimeCaching: [
{
urlPattern: /\.(?:png|jpg|jpeg|svg|gif)$/,
handler: 'CacheFirst',
options: {
cacheName: 'images-cache',
expiration: {
maxEntries: 60,
maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
},
},
},
{
urlPattern: /^https?:\/\/(api|another)/,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
networkTimeoutSeconds: 10,
},
},
],
};

监控与分析

缺陷的缓存策略常常表现为:

  • 用户始终看到旧版本页面
  • 缓存不断膨胀导致存储配额耗尽

可通过在 fetch 事件中添加日志、结合 clients.matchAll 检查缓存状态,或者向后端发送异常报告来监控。

进阶技巧

  • 缓存优先级:为不同资源设定权重,优先清理低优先级内容。
  • 条件更新:根据 HTTP 头部(如 ETag)判断资源是否改变,减少网络开销。
  • 外部 CDN 缓存:结合 Service Worker 与 CDN 缓存可以双重加速。

总结

Service Worker 缓存策略不是一刀切,需要根据应用特点灵活选择。本文介绍的几种策略可作为参考,实际项目中常常结合使用,并通过版本控制、监控与清理机制保持缓存健康。掌握这些技巧,可以让你的前端在断网、慢网环境下表现更优秀,同时避免资源过期带来的问题。

继续探索更多缓存优化方案,将显著提升用户体验和系统稳定性。

Promise 基础

Promise 表示异步操作最终完成或失败。

1
2
3
4
5
6
7
8
const promise = new Promise((resolve, reject) => {
// 异步操作
if (success) {
resolve(value)
} else {
reject(error)
}
})

Promise 状态

  • Pending:初始状态
  • Fulfilled:操作成功
  • Rejected:操作失败

Promise 方法

then()

1
2
3
4
5
promise.then(value => {
// 成功处理
}).catch(error => {
// 失败处理
})

catch()

1
2
3
promise.catch(error => {
// 处理错误
})

finally()

1
2
3
promise.finally(() => {
// 总是执行
})

Promise 静态方法

Promise.all()

1
2
3
Promise.all([p1, p2, p3]).then(values => {
// 所有 Promise 成功
})

Promise.race()

1
2
3
Promise.race([p1, p2]).then(value => {
// 第一个完成
})

Promise.allSettled()

1
2
3
Promise.allSettled([p1, p2]).then(results => {
// 所有完成,无论成功失败
})

async/await

1
2
3
4
5
6
7
8
async function asyncFunction() {
try {
const value = await promise
return value
} catch (error) {
throw error
}
}

错误处理

1
2
3
4
5
6
7
8
9
10
11
12
13
// Promise
promise
.then(() => { throw new Error('error') })
.catch(error => console.error(error))

// async/await
async function func() {
try {
await promise
} catch (error) {
console.error(error)
}
}

总结

Promise 和 async/await 让异步代码更可读。async/await 是 Promise 的语法糖,底层仍是 Promise。

Commitlint 是什么?

Commitlint 检查 Git 提交信息格式。

安装

1
npm install @commitlint/cli @commitlint/config-conventional --save-dev

配置

1
2
3
4
// commitlint.config.js
module.exports = {
extends: ['@commitlint/config-conventional']
};

提交类型

  • feat: 新功能
  • fix: 修复
  • docs: 文档
  • style: 样式
  • refactor: 重构
  • test: 测试
  • chore: 杂项

使用

1
echo "feat: add new feature" | npx commitlint

与 Husky 结合

1
npx husky add .husky/commit-msg "npx --no-install commitlint --edit \$1"

总结

Commitlint 保持提交历史清晰。团队协作必备。

Prettier 是什么?

Prettier 自动格式化代码。

安装

1
npm install prettier --save-dev

配置

1
2
3
4
5
6
7
8
// .prettierrc
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 80,
"tabWidth": 2
}

使用

1
npx prettier --write .

编辑器集成

VS Code 安装 Prettier 插件,保存时自动格式化。

忽略文件

1
2
3
4
// .prettierignore
node_modules/
dist/
*.min.js

总结

Prettier 解决代码风格争议。专注于代码逻辑。

ESLint 是什么?

ESLint 检查和修复 JavaScript 代码问题。

安装

1
npm install eslint --save-dev

初始化

1
npx eslint --init

配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// .eslintrc.js
module.exports = {
env: {
browser: true,
es2021: true
},
extends: [
'eslint:recommended',
'@vue/eslint-config-prettier'
],
parserOptions: {
ecmaVersion: 12,
sourceType: 'module'
},
rules: {
'no-unused-vars': 'error',
'no-console': 'warn'
}
};

规则

  • error: 错误
  • warn: 警告
  • off: 关闭

自动修复

1
npx eslint . --fix

总结

ESLint 保持代码质量。团队统一配置。

Babel 是什么?

Babel 将新版 JavaScript 转换为兼容版本。

安装

1
npm install @babel/core @babel/cli --save-dev

预设

1
2
3
4
5
6
7
8
9
10
// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', {
targets: {
browsers: ['> 1%', 'last 2 versions']
}
}]
]
};

插件

1
2
3
4
5
6
module.exports = {
plugins: [
'@babel/plugin-transform-arrow-functions',
'@babel/plugin-transform-template-literals'
]
};

Polyfill

1
2
3
4
5
6
7
8
module.exports = {
presets: [
['@babel/preset-env', {
useBuiltIns: 'usage',
corejs: 3
}]
]
};

总结

Babel 确保代码在老浏览器运行。配置灵活。

Rollup 是什么?

Rollup 是 JavaScript 模块打包器,生成小巧的包。

安装

1
npm install rollup --save-dev

基本配置

1
2
3
4
5
6
7
8
// rollup.config.js
export default {
input: 'src/main.js',
output: {
file: 'bundle.js',
format: 'cjs'
}
};

输出格式

  • esm: ES modules
  • cjs: CommonJS
  • iife: 立即执行函数
  • umd: 通用模块定义

插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { terser } from 'rollup-plugin-terser';
import resolve from '@rollup/plugin-node-resolve';

export default {
input: 'src/main.js',
output: {
file: 'bundle.js',
format: 'iife'
},
plugins: [
resolve(),
terser()
]
};

Tree Shaking

Rollup 天然支持静态分析和 Tree Shaking。

总结

Rollup 适合库开发。生成干净的代码。

代码分割

入口分割

1
2
3
4
5
6
module.exports = {
entry: {
app: './src/app.js',
vendor: './src/vendor.js'
}
};

动态导入

1
2
3
import('./module.js').then(module => {
// 使用模块
});

缓存

文件名哈希

1
2
3
output: {
filename: '[name].[contenthash].js'
}

运行时 chunk

1
2
3
optimization: {
runtimeChunk: 'single'
}

压缩

Terser

1
2
3
4
optimization: {
minimize: true,
minimizer: [new TerserPlugin()]
}

CSS 压缩

1
2
3
4
5
6
7
8
9
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');

module.exports = {
optimization: {
minimizer: [
new CssMinimizerPlugin()
]
}
};

Tree Shaking

1
2
3
4
5
6
module.exports = {
mode: 'production', // 自动启用
optimization: {
usedExports: true
}
};

分析包大小

1
2
3
4
5
6
7
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
};

外部依赖

1
2
3
externals: {
jquery: 'jQuery'
}

总结

Webpack 优化提升构建性能和用户体验。定期分析和调整。

CSS 变量是什么?

CSS 变量允许在 CSS 中定义可重用的值。

定义变量

1
2
3
4
:root {
--primary-color: #007bff;
--font-size: 16px;
}

使用变量

1
2
3
4
.button {
background-color: var(--primary-color);
font-size: var(--font-size);
}

默认值

1
2
3
.button {
color: var(--text-color, black);
}

作用域

1
2
3
4
5
6
7
8
9
10
11
.component {
--local-color: red;
}

.component .child {
color: var(--local-color); /* red */
}

.another {
color: var(--local-color); /* undefined */
}

动态修改

1
2
const root = document.documentElement;
root.style.setProperty('--primary-color', 'green');

计算

1
2
3
4
:root {
--base-size: 16px;
--large-size: calc(var(--base-size) * 2);
}

主题切换

1
2
3
4
5
6
7
8
9
.dark-theme {
--bg-color: black;
--text-color: white;
}

.light-theme {
--bg-color: white;
--text-color: black;
}

浏览器支持

现代浏览器支持,IE11 不支持。

总结

CSS 变量让样式更灵活。结合 JavaScript,可以实现动态主题。