Skip to content

前端安全架构:从防御点到体系化

目标:建立前端安全架构思维,掌握威胁建模、SDL、供应链安全与隐私架构设计方法。


核心要点(TL;DR)

  • 前端安全不仅是防范 XSS/CSRF,更是贯穿设计、开发、发布、运营全生命周期的系统工程。
  • 安全架构的核心是识别资产、评估威胁、设计控制、持续验证
  • 威胁建模(Threat Modeling)是安全设计的起点,STRIDE 是常用方法论。
  • 供应链安全已成为前端重大风险来源,依赖审计和 SRI 是基础防护。
  • 隐私架构需要遵循"隐私设计"(Privacy by Design)和最小必要原则。
  • 安全左移:将安全检查融入 CI/CD 和 Code Review,而非上线前突击。

学习时长与前置知识

  • 建议学习时长:3-4 周(每周投入 6-8 小时)
  • 前置知识:Web 安全基础(F05)、网络协议(F04)、JavaScript/Node.js(F01/E10)

一、为什么需要前端安全架构?

1.1 前端安全的新挑战

传统前端安全主要关注 XSS、CSRF。现代前端面临更复杂的威胁:

威胁说明
供应链攻击npm 包被植入恶意代码,影响数百万项目
第三方脚本风险广告、统计脚本可能窃取数据或篡改页面
API 滥用前端凭证泄露、业务接口被逆向
隐私合规GDPR、个保法要求明确的数据处理边界
客户端逻辑暴露前端代码被逆向分析,暴露业务规则

1.2 从"修漏洞"到"建体系"

阶段特征
被动防御出现漏洞后修复
主动防御编码规范、安全扫描
安全架构威胁建模、SDL、持续监控、应急响应

前端架构师需要将安全从"技术点"提升为"组织能力"。


二、威胁建模(Threat Modeling)

2.1 什么是威胁建模?

威胁建模是一种结构化方法,用于识别系统面临的潜在威胁,并设计缓解措施。

2.2 STRIDE 模型

威胁含义前端示例
Spoofing伪装伪造身份认证
Tampering篡改修改本地存储、请求参数
Repudiation抵赖缺乏操作日志
Information Disclosure信息泄露敏感信息打印到控制台
Denial of Service拒绝服务无限循环导致页面卡死
Elevation of Privilege权限提升普通用户越权访问管理接口

2.3 威胁建模步骤

1. 识别资产(用户数据、API 凭证、业务逻辑)
2. 绘制数据流图(DFD)
3. 识别威胁(STRIDE)
4. 评估风险(概率 × 影响)
5. 设计缓解措施
6. 验证和迭代

2.4 前端数据流图示例

用户 → 浏览器 → CDN/静态资源 → BFF → 后端服务 → 数据库

      第三方脚本

      埋点/分析平台

通过这个图可以识别:

  • CDN 被劫持的风险(使用 SRI)。
  • 第三方脚本越权访问数据的风险(CSP、沙箱)。
  • 传输过程中被窃听的风险(HTTPS、HSTS)。

三、安全开发生命周期(SDL)深度实践

3.1 SDL 是什么?

SDL(Security Development Lifecycle)是微软提出的安全开发方法论,将安全融入软件开发的每个阶段。SDL的核心原则是"安全左移"(Shift Left),即在开发早期发现和修复安全问题,而非在上线前集中处理。

3.2 需求分析阶段:识别安全需求

在新功能立项时,安全架构师需要与产品经理一起识别关键安全问题:

  • 数据分类:功能涉及哪些数据?哪些是敏感数据(PII、支付信息)?
  • 信任边界:数据从用户的浏览器流向哪些系统?哪些环节可能被攻破?
  • 合规要求:是否需要满足 GDPR、CCPA、个保法等法规要求?
  • 第三方依赖:功能是否依赖第三方 SDK 或 CDN 资源?

3.3 设计阶段:威胁建模

在技术设计阶段,团队应进行威胁建模:

  1. 绘制架构图和数据流图
  2. 使用 STRIDE 逐项分析威胁
  3. 记录威胁清单并分配缓解措施
  4. 设计评审时安全架构师签字确认

3.4 编码阶段:安全编码规范

javascript
// 错误:使用 innerHTML 插入用户内容
element.innerHTML = userInput;

// 正确:使用 textContent 或安全的 DOM API
element.textContent = userInput;

// 正确:使用 DOMPurify 清洗后再插入
import DOMPurify from 'dompurify';
element.innerHTML = DOMPurify.sanitize(userInput);

// 错误:使用 eval
const result = eval(code);

3.5 测试阶段:安全测试矩阵

测试类型覆盖范围工具
SAST(静态分析)代码中的安全漏洞模式CodeQL、Semgrep、SonarQube
SCA(组件分析)依赖中的已知漏洞npm audit、Snyk、Dependabot
密钥扫描硬编码的密钥和 TokenGitLeaks、truffleHog
DAST(动态分析)运行时安全测试OWASP ZAP
渗透测试人工挖掘复杂漏洞专业安全团队

3.6 发布阶段:上线安全检查清单

  • [ ] CSP 策略配置正确且无绕过
  • [ ] 所有第三方资源已配置 SRI
  • [ ] 安全响应头已生效(HSTS、XFO、CSP)
  • [ ] 无敏感信息泄露(console.log、注释中的密钥)
  • [ ] 依赖无已知高危漏洞
  • [ ] 权限配置最小化

3.7 运营阶段:持续监控

  • 收集 CSP 违规报告并分析
  • 监控异常请求模式
  • 订阅安全公告,及时更新依赖
  • 建立应急响应流程

四、CSP 深度实践

4.1 CSP 指令分类

类别关键指令说明
获取指令default-src, script-src, style-src, img-src, connect-src, font-src, media-src, object-src控制各类资源的加载来源
文档指令base-uri, plugin-types, sandbox控制页面行为
导航指令form-action, frame-ancestors, navigate-to控制导航和嵌入
报告指令report-uri, report-to违规报告

4.2 script-src 详细配置

http
# 最严格:仅同源脚本
Content-Security-Policy: script-src 'self'

# 允许特定 CDN
Content-Security-Policy: script-src 'self' https://cdn.example.com

# 使用 nonce(推荐)
Content-Security-Policy: script-src 'self' 'nonce-abc123'

# 使用 hash(适合内联脚本)
Content-Security-Policy: script-src 'self' 'sha256-xyz789='

# 严格 CSP(Strict CSP)—— 推荐方案
Content-Security-Policy: script-src 'self' 'strict-dynamic' 'nonce-abc123'

4.3 Nonce 模式 vs Hash 模式

Nonce 模式:服务端为每个请求生成唯一的随机数。

html
<!-- 服务端生成 nonce 并注入 -->
<script nonce="a1b2c3d4">
  initApp();
</script>

<script src="app.js" nonce="a1b2c3d4"></script>

Hash 模式:对脚本内容计算哈希,适合静态内联脚本。

http
Content-Security-Policy: script-src 'sha256-abc123def456='
html
<!-- 只有内容完全匹配的脚本才能执行 -->
<script>console.log('hello')</script>

对比

特性NonceHash
动态内容适合不适合
性能需服务端生成无运行时开销
安全性需保证 nonce 不可预测依赖哈希碰撞难度
维护成本低(静态内容)

4.4 报告端点配置

http
# 仅报告不阻断(用于灰度验证)
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report

# 生产环境,报告并阻断
Content-Security-Policy: default-src 'self'; report-uri /csp-report

# 使用新的 Report-To API(CSP Level 3)
Content-Security-Policy: default-src 'self'; report-to csp-endpoint
Report-To: {"group":"csp-endpoint","max_age":10886400,"endpoints":[{"url":"/csp-report"}]}

前端收集 CSP 违规的示例:

javascript
// 使用 ReportingObserver 捕获 CSP 违规(Chrome)
const observer = new ReportingObserver((reports) => {
  for (const report of reports) {
    console.log('CSP Violation:', report);
    sendToAnalytics(report.toJSON());
  }
}, { types: ['csp-violation'] });
observer.observe();

4.5 CSP 常见陷阱

http
# 危险:'unsafe-inline' 使 script-src 形同虚设
Content-Security-Policy: script-src 'self' 'unsafe-inline'

# 危险:'unsafe-eval' 允许 eval() 执行
Content-Security-Policy: script-src 'self' 'unsafe-eval'

# 危险:通配符过于宽泛
Content-Security-Policy: script-src https://*.cdn.com  # 子域名可能不安全

# 正确:精确指定来源
Content-Security-Policy: script-src 'self' https://cdn.example.com

4.6 CSP Evaluator 工具

推荐使用 Google 的 CSP Evaluator(https://csp-evaluator.withgoogle.com/)验证策略安全性:

  • 检查是否存在已知的 CSP 绕过
  • 评估配置的严格程度
  • 提供改进建议

五、CORS 深度实践

5.1 CORS 基础流

简单请求:
 浏览器 → Origin: https://app.example.com → 服务器
 浏览器 ← Access-Control-Allow-Origin: https://app.example.com ← 服务器

预检请求(Preflight):
 浏览器 → OPTIONS /api/data (Preflight) → 服务器
 浏览器 ← Access-Control-Allow-Origin, Allow-Methods, Allow-Headers ← 服务器
 浏览器 → GET /api/data (实际请求) → 服务器

5.2 预检请求详解

满足以下条件的请求会触发预检:

  • 使用 PUT、DELETE、CONNECT、OPTIONS、TRACE、PATCH 方法
  • 请求头包含非简单头(如 Authorization、X-Custom-Header)
  • Content-Type 不是 application/x-www-form-urlencoded、multipart/form-data 或 text/plain

5.3 凭据(Credentials)处理

http
# 服务端必须明确指定,不能使用通配符
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
javascript
// 前端设置
fetch('https://api.example.com/data', {
  credentials: 'include',
});

const xhr = new XMLHttpRequest();
xhr.withCredentials = true;

重要限制:当 Access-Control-Allow-Credentials: true 时,Access-Control-Allow-Origin 不能为 *

5.4 CORS 最佳实践

  • 不要使用 Access-Control-Allow-Origin: *,应精确指定白名单
  • 限制允许的方法:只暴露必要的 HTTP 方法
  • 限制允许的请求头:只暴露必要的请求头
  • 合理设置 Max-Age:减少预检请求频次,但不直过长(建议 24h)
  • 不要在允许的 Header 中包含敏感头(如 Cookie)

六、安全响应头体系

6.1 完整的响应头配置模板

nginx
# Nginx 配置示例
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header X-XSS-Protection "0" always;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'" always;

6.2 HSTS(HTTP Strict Transport Security)

http
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
  • max-age:强制使用 HTTPS 的时长(单位秒)
  • includeSubDomains:对所有子域名生效
  • preload:提交到浏览器预加载列表

注意事项:先用低 max-age 验证,逐步增加。确保所有子域名都支持 HTTPS。

6.3 X-Frame-Options

http
# 禁止所有嵌入
X-Frame-Options: DENY

# 仅同源嵌入
X-Frame-Options: SAMEORIGIN

# 更细粒度的 CSP 方案
Content-Security-Policy: frame-ancestors 'self' https://trusted-site.com

6.4 Permissions-Policy

http
# 禁用所有敏感权限
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()

# 仅允许特定来源获取位置
Permissions-Policy: geolocation=(self "https://maps.example.com")

七、身份认证与授权

7.1 Token 存储策略对比

存储方式优点缺点
HttpOnly Cookie不可被 JS 读取,防 XSS 窃取易受 CSRF 攻击(需配合 SameSite)
memory(变量)最安全,XSS 无法窃取页面刷新后丢失,需配合 Refresh Token
localStorage使用方便任何 XSS 都可读取
sessionStorage仅当前标签页有效作用域有限

推荐方案:Access Token 存内存,Refresh Token 存 HttpOnly Secure SameSite Cookie。

javascript
// BFF 模式下,Token 完全由服务端管理
async function fetchWithAuth(url, options = {}) {
  const response = await fetch(url, { ...options, credentials: 'include' });
  if (response.status === 401) {
    // BFF 会自动刷新 Token,前端无需处理
  }
  return response;
}

7.2 Refresh Token 轮转

javascript
async function refreshToken(oldRefreshToken) {
  const response = await fetch('/api/auth/refresh', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ refreshToken: oldRefreshToken }),
  });
  const { accessToken, refreshToken: newRefreshToken } = await response.json();
  // 旧的 refreshToken 已被服务端标记为无效
  return { accessToken, refreshToken: newRefreshToken };
}

7.3 BFF(Backend for Frontend)模式

BFF 是前端认证的最佳实践架构:

浏览器 → [HttpOnly Cookie] → BFF → [Access Token] → 后端 API

BFF 的优势

  1. Token 对浏览器不可见,消除 XSS 窃取风险
  2. Token 刷新逻辑集中管理
  3. 可以添加请求签名、IP 校验等额外安全层
  4. 减少前后端耦合
javascript
// BFF 代理示例(Node.js)
app.post('/api/auth/login', async (req, res) => {
  const { username, password } = req.body;
  const tokenResponse = await fetchAuthServer('/oauth/token', {
    username, password, grant_type: 'password',
  });
  req.session.accessToken = tokenResponse.access_token;
  req.session.refreshToken = tokenResponse.refresh_token;
  res.json({ success: true });
});

app.get('/api/user/profile', async (req, res) => {
  const response = await fetch('https://api.example.com/user/profile', {
    headers: { Authorization: `Bearer ${req.session.accessToken}` },
  });
  const data = await response.json();
  res.json(data);
});

八、OAuth 2.0 / OIDC 前端实践

8.1 PKCE 流程详解

PKCE(Proof Key for Code Exchange)是 SPA 的推荐 OAuth 流程:

1. 前端生成 code_verifier(随机字符串)
2. 前端计算 code_challenge = SHA256(code_verifier)
3. 前端重定向到授权服务器:
   /authorize?response_type=code&client_id=app
            &redirect_uri=https://app/callback
            &code_challenge=<SHA256(verifier)>
            &code_challenge_method=S256
4. 授权服务器返回 authorization code
5. 前端用 authorization code + code_verifier 换取 Token
6. 授权服务器验证 code_verifier 与 code_challenge 匹配后返回 Token
javascript
async function generatePKCE() {
  const verifier = generateRandomString(128);
  const challenge = await sha256(verifier);
  return { verifier, challenge };
}

function generateRandomString(length) {
  const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
  const array = new Uint8Array(length);
  crypto.getRandomValues(array);
  return Array.from(array, byte => charset[byte % charset.length]).join('');
}

async function sha256(plain) {
  const encoder = new TextEncoder();
  const data = encoder.encode(plain);
  const hash = await crypto.subtle.digest('SHA-256', data);
  return base64URLEncode(hash);
}

function base64URLEncode(buffer) {
  const bytes = new Uint8Array(buffer);
  let binary = '';
  bytes.forEach(b => binary += String.fromCharCode(b));
  return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

8.2 为什么隐式流程(Implicit Flow)被废弃

OAuth 2.0 Security BCP(RFC 9700)明确废弃了 Implicit Flow:

问题说明
Token 在 URL 中暴露可能被浏览器历史、服务器日志记录
无法使用 Refresh Token无法安全地刷新令牌
没有客户端认证无法验证客户端身份
无法使用 PKCE隐式流程不支持 PKCE 扩展
浏览器安全问题Token 暴露给 JavaScript 环境

结论:SPA 应始终使用 Authorization Code + PKCE 流程。

8.3 OIDC(OpenID Connect)

OIDC 在 OAuth 2.0 基础上增加身份认证层:

  • ID Token:JWT 格式,包含用户身份信息(sub、name、email)
  • UserInfo Endpoint:获取用户详细信息的 API
  • Discovery:通过 .well-known/openid-configuration 获取配置

九、WebAuthn / Passkeys

9.1 注册流程

javascript
// 1. 服务端生成注册选项
const publicKeyCredentialCreationOptions = {
  challenge: await fetchChallenge(),
  rp: { name: 'My App', id: 'example.com' },
  user: {
    id: new TextEncoder().encode(userId),
    name: 'user@example.com',
    displayName: 'User Name',
  },
  pubKeyCredParams: [
    { type: 'public-key', alg: -7 },    // ES256
    { type: 'public-key', alg: -257 },  // RS256
  ],
  authenticatorSelection: {
    authenticatorAttachment: 'platform',
    residentKey: 'preferred',
    userVerification: 'preferred',
  },
  timeout: 60000,
  attestation: 'none',
};

// 2. 浏览器调用 WebAuthn API
const credential = await navigator.credentials.create({
  publicKey: publicKeyCredentialCreationOptions,
});

// 3. 将认证器响应发送给服务端验证
const attestationResponse = {
  id: credential.id,
  rawId: arrayBufferToBase64(credential.rawId),
  type: credential.type,
  response: {
    clientDataJSON: arrayBufferToBase64(credential.response.clientDataJSON),
    attestationObject: arrayBufferToBase64(credential.response.attestationObject),
  },
};

9.2 认证流程

javascript
// 1. 服务端生成认证选项
const publicKeyCredentialRequestOptions = {
  challenge: await fetchChallenge(),
  timeout: 60000,
  rpId: 'example.com',
  allowCredentials: [{
    id: base64ToArrayBuffer(credentialId),
    type: 'public-key',
  }],
  userVerification: 'preferred',
};

// 2. 浏览器调用 WebAuthn API
const assertion = await navigator.credentials.get({
  publicKey: publicKeyCredentialRequestOptions,
});

// 3. 将断言发送给服务端验证
const assertionResponse = {
  id: assertion.id,
  rawId: arrayBufferToBase64(assertion.rawId),
  type: assertion.type,
  response: {
    clientDataJSON: arrayBufferToBase64(assertion.response.clientDataJSON),
    authenticatorData: arrayBufferToBase64(assertion.response.authenticatorData),
    signature: arrayBufferToBase64(assertion.response.signature),
    userHandle: arrayBufferToBase64(assertion.response.userHandle),
  },
};

十、SRI(Subresource Integrity)

10.1 工作原理

SRI 确保 CDN 资源未被篡改:浏览器下载资源后计算哈希,与 integrity 属性比较,不匹配则拒绝执行。

html
<script
  src="https://cdn.example.com/vue@3.4.js"
  integrity="sha384-ABC123..."
  crossorigin="anonymous"
></script>
bash
# 生成 SRI 哈希
openssl dgst -sha384 -binary file.js | openssl base64 -A

10.2 工程化集成

javascript
// webpack 中使用 webpack-subresource-integrity 插件自动生成 SRI
import SriPlugin from 'webpack-subresource-integrity';

export default {
  output: { crossOriginLoading: 'anonymous' },
  plugins: [
    new SriPlugin({
      hashFuncNames: ['sha384'],
      enabled: process.env.NODE_ENV === 'production',
    }),
  ],
};

十一、Trusted Types

11.1 什么是 Trusted Types?

Trusted Types 是浏览器级别的 XSS 防御机制,强制要求所有 DOM 赋值操作使用安全的类型对象。

http
Content-Security-Policy: require-trusted-types-for 'script'

11.2 使用示例

javascript
// 启用后,以下操作会被阻止:
element.innerHTML = userInput;

// 必须使用 Trusted Types
const sanitizePolicy = TrustedTypes.createPolicy('default', {
  createHTML: (input) => DOMPurify.sanitize(input),
  createScriptURL: (input) => {
    const url = new URL(input, window.location.origin);
    if (url.origin === window.location.origin) return url.href;
    throw new Error('Blocked');
  },
});

element.innerHTML = sanitizePolicy.createHTML(userInput);

十二、iframe 安全

12.1 sandbox 属性

html
<!-- 完全沙箱化 -->
<iframe src="https://third-party.com" sandbox></iframe>

<!-- 选择性启用 -->
<iframe
  src="https://third-party.com"
  sandbox="allow-scripts allow-same-origin allow-forms"
></iframe>

sandbox 选项:allow-scripts, allow-same-origin, allow-forms, allow-popups, allow-modals, allow-top-navigation

安全警告:同时启用 allow-scripts 和 allow-same-origin 会使沙箱失效。

12.2 frame-ancestors CSP

http
Content-Security-Policy: frame-ancestors 'none'        # 禁止嵌入
Content-Security-Policy: frame-ancestors 'self'         # 仅同源
Content-Security-Policy: frame-ancestors https://trusted-app.com  # 白名单

12.3 postMessage 安全验证

javascript
window.addEventListener('message', (event) => {
  if (event.origin !== 'https://trusted-app.com') return;
  if (typeof event.data !== 'object' || !event.data.type) return;

  switch (event.data.type) {
    case 'RESIZE':
      handleResize(event.data.payload);
      break;
    case 'NAVIGATE':
      // 使用白名单,不要直接使用 event.data.url
      navigateTo(event.data.payload);
      break;
  }
});

十三、常见误区与反模式

误区说明正确做法
"前端没有敏感逻辑"前端仍有权限、凭证、业务规则前端做体验层控制,服务端做最终控制
"HTTPS 就够了"HTTPS 只是传输安全配合 CSP、HSTS、SRI 等多层防御
"依赖不用审计"供应链攻击频发定期审计和更新依赖
"安全是安全团队的事"安全需要全员参与建立 SDL 和安全文化
"上线前扫描即可"安全应左移从需求和设计阶段介入
"CSP 配置了就行"CSP 需要持续维护和调试使用 report-only 模式验证,监控违规
"OAuth 隐式流程没问题"Token 暴露在 URL 中使用 Authorization Code + PKCE

十四、最佳实践

  1. 威胁建模先行:新功能设计前进行 STRIDE 分析。
  2. 安全左移:将安全扫描纳入 CI 和 Code Review。
  3. 最小权限:前端只拥有必要的权限和数据。
  4. 纵深防御:不依赖单一安全控制。
  5. 供应链治理:固定版本、审计依赖、SRI 校验。
  6. 隐私默认:收集数据前明确目的并获得同意。
  7. CSP Strict Dynamic:优先使用 nonce + strict-dynamic 方案。
  8. BFF 模式:前端不直接管理 Token。
  9. Trusted Types:启用浏览器级别的 XSS 防御。
  10. 应急响应:建立漏洞响应流程和回滚机制。

十五、相关领域


十六、延伸阅读


标签#security #security-architecture #threat-modeling #supply-chain #privacy #sdl #csp #cors #oauth #webauthn #trusted-types

最后更新:2026-07-06

十七、隐私架构

17.1 隐私设计原则

  • 数据最小化:只收集必要数据。
  • 目的限制:数据仅用于声明目的。
  • 默认隐私:默认开启最高隐私保护。
  • 透明可控:用户知晓数据用途,可撤回同意。

17.2 前端隐私实践

  • 敏感数据不存储在 localStorage。
  • 埋点数据脱敏。
  • 提供"拒绝所有非必要 Cookie"选项。
  • 用户注销后清除客户端缓存。
  • 使用 DNT(Do Not Track)和 GPC(Global Privacy Control)。

17.3 合规要求

法规关键要求
GDPR用户同意、数据可携带、被遗忘权
中国个保法告知-同意、最小必要、敏感信息保护
CCPA用户知情权、删除权、退出权

十八、安全测试与监控

8.1 自动化安全测试

类型工具/方法阶段
SASTSemgrep、SonarQube、CodeQLCI
依赖审计npm audit、Snyk、DependabotCI
密钥扫描GitLeaks、truffleHogPre-commit/CI
DASTOWASP ZAP测试
渗透测试人工上线前

8.2 运行时监控

  • 异常行为埋点。
  • CSP 违规报告:report-uri
  • 前端错误监控(Sentry)中的安全相关错误。


本领域学习进度

学习进度0 / 43 (0%)

基于 MIT 协议发布