返回笔记首页

18.2 内容安全策略

主题配置

一、技术实现方案

1.1 内容安全策略架构

plain
内容安全策略体系
  ├── CSP (Content Security Policy)
  │   ├── 脚本源限制
  │   ├── 样式源限制
  │   ├── 图片源限制
  │   ├── 字体源限制
  │   ├── 连接源限制
  │   └── Frame源限制
  │
  ├── SRI (Subresource Integrity)
  │   ├── 完整性校验
  │   ├── Hash生成
  │   ├── CDN资源保护
  │   └── 降级处理
  │
  ├── HTTPS部署
  │   ├── SSL/TLS配置
  │   ├── HSTS策略
  │   ├── 证书管理
  │   └── 混合内容处理
  │
  └── Cookie安全
      ├── Secure标志
      ├── HttpOnly标志
      ├── SameSite属性
      ├── Domain和Path设置
      └── 过期时间管理

1.2 技术栈

  • CSP: Content-Security-Policy响应头
  • SRI: integrity属性、sha256/sha384/sha512
  • HTTPS: Let's Encrypt、SSL Labs
  • Cookie: Secure、HttpOnly、SameSite

二、CSP (内容安全策略) 配置

2.1 CSP管理器

csp-manager.js

javascript
export class CSPManager {
  constructor() {
    this.policies = {
      'default-src': ["'self'"],
      'script-src': ["'self'"],
      'style-src': ["'self'", "'unsafe-inline'"],
      'img-src': ["'self'", 'data:', 'https:'],
      'font-src': ["'self'", 'data:'],
      'connect-src': ["'self'"],
      'frame-src': ["'none'"],
      'object-src': ["'none'"],
      'base-uri': ["'self'"],
      'form-action': ["'self'"],
      'frame-ancestors': ["'none'"]
    }

    this.reportUri = '/api/csp-report'
    this.reportOnly = false
  }

  // 添加源
  addSource(directive, source) {
    if (!this.policies[directive]) {
      this.policies[directive] = []
    }

    if (!this.policies[directive].includes(source)) {
      this.policies[directive].push(source)
    }
  }

  // 移除源
  removeSource(directive, source) {
    if (this.policies[directive]) {
      this.policies[directive] = this.policies[directive].filter(s => s !== source)
    }
  }

  // 允许内联脚本(使用nonce)
  allowInlineScript(nonce) {
    this.addSource('script-src', `'nonce-${nonce}'`)
  }

  // 允许内联样式(使用nonce)
  allowInlineStyle(nonce) {
    this.addSource('style-src', `'nonce-${nonce}'`)
  }

  // 生成CSP字符串
  generateCSP() {
    const directives = Object.entries(this.policies)
      .filter(([_, sources]) => sources.length > 0)
      .map(([directive, sources]) => `${directive} ${sources.join(' ')}`)
      .join('; ')

    const reportDirective = `report-uri ${this.reportUri}`

    return `${directives}; ${reportDirective}`
  }

  // 生成CSP响应头
  generateHeader() {
    const headerName = this.reportOnly
      ? 'Content-Security-Policy-Report-Only'
      : 'Content-Security-Policy'

    return {
      [headerName]: this.generateCSP()
    }
  }

  // 生成meta标签
  generateMetaTag() {
    return `<meta http-equiv="Content-Security-Policy" content="${this.generateCSP()}">`
  }

  // 解析CSP违规报告
  parseViolationReport(report) {
    return {
      documentUri: report['document-uri'],
      violatedDirective: report['violated-directive'],
      effectiveDirective: report['effective-directive'],
      blockedUri: report['blocked-uri'],
      sourceFile: report['source-file'],
      lineNumber: report['line-number'],
      columnNumber: report['column-number'],
      statusCode: report['status-code']
    }
  }

  // 常见场景配置

  // 严格模式(最安全)
  strictMode() {
    this.policies = {
      'default-src': ["'none'"],
      'script-src': ["'self'"],
      'style-src': ["'self'"],
      'img-src': ["'self'"],
      'font-src': ["'self'"],
      'connect-src': ["'self'"],
      'frame-src': ["'none'"],
      'object-src': ["'none'"],
      'base-uri': ["'self'"],
      'form-action': ["'self'"],
      'frame-ancestors': ["'none'"],
      'upgrade-insecure-requests': []
    }
  }

  // CDN模式(允许CDN资源)
  cdnMode(cdnDomains = []) {
    this.addSource('script-src', ...cdnDomains)
    this.addSource('style-src', ...cdnDomains)
    this.addSource('font-src', ...cdnDomains)
    this.addSource('img-src', ...cdnDomains)
  }

  // 开发模式(宽松配置)
  developmentMode() {
    this.policies['script-src'].push("'unsafe-eval'")
    this.policies['style-src'].push("'unsafe-inline'")
    this.reportOnly = true
  }
}

export default new CSPManager()

2.2 CSP配置演示组件

CSPDemo.vue

vue
<script setup>
import { ref, computed } from 'vue'
import cspManager from './csp-manager.js'

const selectedMode = ref('custom')
const customPolicies = ref({
  'script-src': ["'self'", 'https://cdn.jsdelivr.net'],
  'style-src': ["'self'", "'unsafe-inline'"],
  'img-src': ["'self'", 'data:', 'https:'],
  'connect-src': ["'self'", 'https://api.example.com']
})

const violations = ref([])

// CSP预设模式
const modes = [
  {
    id: 'strict',
    name: '严格模式',
    description: '最安全的配置,禁止所有外部资源',
    config: {
      'default-src': ["'none'"],
      'script-src': ["'self'"],
      'style-src': ["'self'"],
      'img-src': ["'self'"],
      'font-src': ["'self'"],
      'connect-src': ["'self'"]
    }
  },
  {
    id: 'standard',
    name: '标准模式',
    description: '允许常用CDN和必要的内联样式',
    config: {
      'default-src': ["'self'"],
      'script-src': ["'self'", 'https://cdn.jsdelivr.net'],
      'style-src': ["'self'", "'unsafe-inline'"],
      'img-src': ["'self'", 'data:', 'https:'],
      'font-src': ["'self'", 'data:'],
      'connect-src': ["'self'"]
    }
  },
  {
    id: 'development',
    name: '开发模式',
    description: '宽松配置,方便开发调试',
    config: {
      'default-src': ["'self'"],
      'script-src': ["'self'", "'unsafe-eval'", "'unsafe-inline'"],
      'style-src': ["'self'", "'unsafe-inline'"],
      'img-src': ["'self'", 'data:', 'https:', 'http:'],
      'connect-src': ["'self'", '*']
    }
  }
]

// 生成的CSP字符串
const generatedCSP = computed(() => {
  const config = selectedMode.value === 'custom'
    ? customPolicies.value
    : modes.find(m => m.id === selectedMode.value)?.config || {}

  const directives = Object.entries(config)
    .map(([directive, sources]) => `${directive} ${sources.join(' ')}`)
    .join('; ')

  return directives
})

// 生成meta标签
const metaTag = computed(() => {
  return `<meta http-equiv="Content-Security-Policy" content="${generatedCSP.value}">`
})

// 生成服务器配置
const serverConfigs = computed(() => {
  return [
    {
      server: 'Express (Node.js)',
      code: `app.use((req, res, next) => {
  res.setHeader('Content-Security-Policy', '${generatedCSP.value}')
  next()
})`
    },
    {
      server: 'Nginx',
      code: `add_header Content-Security-Policy "${generatedCSP.value}";`
    },
    {
      server: 'Apache',
      code: `Header set Content-Security-Policy "${generatedCSP.value}"`
    }
  ]
})

// 添加自定义指令
const newDirective = ref('')
const newSource = ref('')

const addCustomDirective = () => {
  if (newDirective.value && newSource.value) {
    if (!customPolicies.value[newDirective.value]) {
      customPolicies.value[newDirective.value] = []
    }
    if (!customPolicies.value[newDirective.value].includes(newSource.value)) {
      customPolicies.value[newDirective.value].push(newSource.value)
    }
    newDirective.value = ''
    newSource.value = ''
  }
}

// 移除源
const removeSource = (directive, source) => {
  customPolicies.value[directive] = customPolicies.value[directive].filter(s => s !== source)
}

// 模拟CSP违规
const simulateViolation = (type) => {
  const violation = {
    id: Date.now(),
    type: type,
    timestamp: new Date().toLocaleString(),
    documentUri: 'https://example.com/page',
    blockedUri: '',
    violatedDirective: '',
    message: ''
  }

  switch (type) {
    case 'inline-script':
      violation.blockedUri = 'inline'
      violation.violatedDirective = 'script-src'
      violation.message = '页面尝试执行内联脚本'
      break
    case 'external-script':
      violation.blockedUri = 'https://evil.com/malicious.js'
      violation.violatedDirective = 'script-src'
      violation.message = '页面尝试加载外部脚本'
      break
    case 'inline-style':
      violation.blockedUri = 'inline'
      violation.violatedDirective = 'style-src'
      violation.message = '页面尝试使用内联样式'
      break
    case 'external-image':
      violation.blockedUri = 'http://untrusted.com/image.jpg'
      violation.violatedDirective = 'img-src'
      violation.message = '页面尝试加载不安全来源的图片'
      break
  }

  violations.value.unshift(violation)

  if (violations.value.length > 10) {
    violations.value.pop()
  }
}

// 清空违规记录
const clearViolations = () => {
  violations.value = []
}
</script>

<template>
  <div class="csp-demo">
    <div class="demo-header">
      <h2>CSP (内容安全策略) 配置</h2>
      <p class="subtitle">配置和测试Content Security Policy</p>
    </div>

    <!-- 模式选择 -->
    <div class="mode-section">
      <h3>选择配置模式</h3>
      <div class="mode-grid">
        <div
          v-for="mode in modes"
          :key="mode.id"
          class="mode-card"
          :class="{ active: selectedMode === mode.id }"
          @click="selectedMode = mode.id"
        >
          <h4>{{ mode.name }}</h4>
          <p>{{ mode.description }}</p>
        </div>

        <div
          class="mode-card"
          :class="{ active: selectedMode === 'custom' }"
          @click="selectedMode = 'custom'"
        >
          <h4>自定义模式</h4>
          <p>完全自定义CSP配置</p>
        </div>
      </div>
    </div>

    <!-- 自定义配置 -->
    <div v-if="selectedMode === 'custom'" class="custom-section">
      <h3>自定义CSP配置</h3>

      <div class="add-directive">
        <input
          v-model="newDirective"
          type="text"
          placeholder="指令名称 (如: script-src)"
          class="directive-input"
        />
        <input
          v-model="newSource"
          type="text"
          placeholder="源 (如: https://cdn.example.com)"
          class="source-input"
        />
        <button @click="addCustomDirective" class="add-btn">
          添加
        </button>
      </div>

      <div class="directives-list">
        <div
          v-for="(sources, directive) in customPolicies"
          :key="directive"
          class="directive-item"
        >
          <div class="directive-name">{{ directive }}</div>
          <div class="sources">
            <span
              v-for="source in sources"
              :key="source"
              class="source-tag"
            >
              {{ source }}
              <button @click="removeSource(directive, source)" class="remove-btn">×</button>
            </span>
          </div>
        </div>
      </div>
    </div>

    <!-- 生成的CSP -->
    <div class="output-section">
      <h3>生成的CSP策略</h3>

      <div class="output-box">
        <div class="output-label">CSP字符串:</div>
        <pre class="output-code">{{ generatedCSP }}</pre>
      </div>

      <div class="output-box">
        <div class="output-label">HTML Meta标签:</div>
        <pre class="output-code">{{ metaTag }}</pre>
      </div>
    </div>

    <!-- 服务器配置 -->
    <div class="server-config-section">
      <h3>服务器配置示例</h3>
      <div class="config-grid">
        <div
          v-for="config in serverConfigs"
          :key="config.server"
          class="config-card"
        >
          <h4>{{ config.server }}</h4>
          <pre><code>{{ config.code }}</code></pre>
        </div>
      </div>
    </div>

    <!-- CSP违规测试 -->
    <div class="violation-test-section">
      <h3>CSP违规模拟</h3>
      <p class="hint">点击按钮模拟不同类型的CSP违规</p>

      <div class="test-buttons">
        <button @click="simulateViolation('inline-script')" class="test-btn">
          内联脚本
        </button>
        <button @click="simulateViolation('external-script')" class="test-btn">
          外部脚本
        </button>
        <button @click="simulateViolation('inline-style')" class="test-btn">
          内联样式
        </button>
        <button @click="simulateViolation('external-image')" class="test-btn">
          不安全图片
        </button>
      </div>
    </div>

    <!-- 违规记录 -->
    <div class="violations-section">
      <div class="violations-header">
        <h3>CSP违规记录</h3>
        <button @click="clearViolations" class="clear-btn">清空</button>
      </div>

      <div class="violations-list">
        <div
          v-for="violation in violations"
          :key="violation.id"
          class="violation-item"
        >
          <div class="violation-header">
            <span class="violation-type">{{ violation.type }}</span>
            <span class="violation-time">{{ violation.timestamp }}</span>
          </div>
          <div class="violation-details">
            <div class="detail-row">
              <label>违反指令:</label>
              <code>{{ violation.violatedDirective }}</code>
            </div>
            <div class="detail-row">
              <label>被阻止的URI:</label>
              <code>{{ violation.blockedUri }}</code>
            </div>
            <div class="detail-row">
              <label>说明:</label>
              <span>{{ violation.message }}</span>
            </div>
          </div>
        </div>

        <div v-if="violations.length === 0" class="empty-violations">
          暂无违规记录
        </div>
      </div>
    </div>

    <!-- CSP指令说明 -->
    <div class="directives-doc-section">
      <h3>CSP指令说明</h3>
      <div class="doc-grid">
        <div class="doc-card">
          <h4>default-src</h4>
          <p>默认策略,作为其他指令的后备</p>
        </div>
        <div class="doc-card">
          <h4>script-src</h4>
          <p>控制JavaScript来源</p>
        </div>
        <div class="doc-card">
          <h4>style-src</h4>
          <p>控制CSS样式来源</p>
        </div>
        <div class="doc-card">
          <h4>img-src</h4>
          <p>控制图片来源</p>
        </div>
        <div class="doc-card">
          <h4>font-src</h4>
          <p>控制字体文件来源</p>
        </div>
        <div class="doc-card">
          <h4>connect-src</h4>
          <p>控制AJAX、WebSocket等连接来源</p>
        </div>
        <div class="doc-card">
          <h4>frame-src</h4>
          <p>控制iframe来源</p>
        </div>
        <div class="doc-card">
          <h4>object-src</h4>
          <p>控制object、embed、applet标签</p>
        </div>
      </div>
    </div>

    <!-- 最佳实践 -->
    <div class="best-practices-section">
      <h3>CSP最佳实践</h3>
      <ul class="practices-list">
        <li>从严格策略开始,逐步放宽而非反向</li>
        <li>先使用report-only模式测试,确认无误后再启用强制模式</li>
        <li>避免使用unsafe-inline和unsafe-eval</li>
        <li>使用nonce或hash允许特定的内联脚本</li>
        <li>将所有资源迁移到HTTPS</li>
        <li>定期审查和更新CSP策略</li>
        <li>配置report-uri收集违规报告</li>
        <li>在生产环境禁用unsafe指令</li>
      </ul>
    </div>
  </div>
</template>

<style scoped>
.csp-demo {
  padding: 20px;
  background: #f5f7fa;
  min-height: 100vh;
}

.demo-header {
  text-align: center;
  margin-bottom: 40px;
}

.demo-header h2 {
  margin: 0 0 10px 0;
  font-size: 32px;
  color: #303133;
}

.subtitle {
  margin: 0;
  font-size: 16px;
  color: #909399;
}

/* 各个section的通用样式 */
.mode-section,
.custom-section,
.output-section,
.server-config-section,
.violation-test-section,
.violations-section,
.directives-doc-section,
.best-practices-section {
  margin-bottom: 30px;
  padding: 24px;
  background: white;
  border-radius: 8px;
}

.mode-section h3,
.custom-section h3,
.output-section h3,
.server-config-section h3,
.violation-test-section h3,
.violations-section h3,
.directives-doc-section h3,
.best-practices-section h3 {
  margin: 0 0 20px 0;
  font-size: 18px;
  color: #303133;
}

/* 模式选择 */
.mode-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: 16px;
}

.mode-card {
  padding: 20px;
  border: 2px solid #e4e7ed;
  border-radius: 8px;
  cursor: pointer;
  transition: all 0.3s;
}

.mode-card:hover {
  border-color: #409eff;
  box-shadow: 0 4px 12px rgba(64, 158, 255, 0.2);
}

.mode-card.active {
  border-color: #409eff;
  background: #ecf5ff;
}

.mode-card h4 {
  margin: 0 0 8px 0;
  font-size: 16px;
  color: #303133;
}

.mode-card p {
  margin: 0;
  font-size: 14px;
  color: #606266;
  line-height: 1.6;
}

/* 自定义配置 */
.add-directive {
  display: flex;
  gap: 12px;
  margin-bottom: 20px;
}

.directive-input,
.source-input {
  flex: 1;
  padding: 10px;
  border: 1px solid #dcdfe6;
  border-radius: 4px;
  font-size: 14px;
}

.add-btn {
  padding: 10px 24px;
  background: #409eff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
  font-weight: 600;
}

.directives-list {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.directive-item {
  padding: 16px;
  background: #f5f7fa;
  border-radius: 8px;
}

.directive-name {
  font-size: 14px;
  font-weight: 600;
  color: #303133;
  margin-bottom: 12px;
}

.sources {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.source-tag {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 6px 12px;
  background: white;
  border: 1px solid #409eff;
  border-radius: 16px;
  font-size: 13px;
  color: #409eff;
}

.source-tag .remove-btn {
  padding: 0;
  width: 16px;
  height: 16px;
  background: #f56c6c;
  color: white;
  border: none;
  border-radius: 50%;
  cursor: pointer;
  font-size: 12px;
  line-height: 1;
}

/* 输出区域 */
.output-box {
  margin-bottom: 20px;
}

.output-label {
  font-size: 14px;
  font-weight: 600;
  color: #606266;
  margin-bottom: 8px;
}

.output-code {
  margin: 0;
  padding: 16px;
  background: #282c34;
  border-radius: 4px;
  color: #abb2bf;
  font-size: 13px;
  line-height: 1.6;
  overflow-x: auto;
  font-family: 'Courier New', monospace;
}

/* 服务器配置 */
.config-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
  gap: 20px;
}

.config-card {
  padding: 20px;
  background: #f5f7fa;
  border-radius: 8px;
}

.config-card h4 {
  margin: 0 0 12px 0;
  font-size: 16px;
  color: #303133;
}

.config-card pre {
  margin: 0;
  padding: 16px;
  background: #282c34;
  border-radius: 4px;
  overflow-x: auto;
}

.config-card code {
  font-size: 13px;
  line-height: 1.6;
  color: #abb2bf;
  font-family: 'Courier New', monospace;
}

/* 违规测试 */
.hint {
  margin: 0 0 16px 0;
  padding: 12px;
  background: #ecf5ff;
  border-left: 4px solid #409eff;
  border-radius: 4px;
  font-size: 14px;
  color: #409eff;
}

.test-buttons {
  display: flex;
  gap: 12px;
  flex-wrap: wrap;
}

.test-btn {
  padding: 10px 20px;
  background: #e6a23c;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
  font-weight: 600;
  transition: all 0.3s;
}

.test-btn:hover {
  opacity: 0.8;
}

/* 违规记录 */
.violations-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 16px;
}

.violations-header h3 {
  margin: 0;
}

.clear-btn {
  padding: 6px 16px;
  background: #f4f4f5;
  color: #606266;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 13px;
}

.violations-list {
  max-height: 400px;
  overflow-y: auto;
}

.violation-item {
  padding: 16px;
  background: #fef0f0;
  border-left: 4px solid #f56c6c;
  border-radius: 4px;
  margin-bottom: 12px;
}

.violation-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 12px;
}

.violation-type {
  padding: 4px 12px;
  background: #f56c6c;
  color: white;
  border-radius: 12px;
  font-size: 12px;
  font-weight: 700;
}

.violation-time {
  font-size: 12px;
  color: #c0c4cc;
}

.violation-details {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.detail-row {
  display: flex;
  gap: 12px;
  font-size: 13px;
}

.detail-row label {
  font-weight: 600;
  color: #606266;
  min-width: 80px;
}

.detail-row code {
  padding: 2px 6px;
  background: white;
  border-radius: 4px;
  font-size: 12px;
  color: #f56c6c;
}

.empty-violations {
  padding: 60px 20px;
  text-align: center;
  color: #909399;
  font-size: 14px;
}

/* 文档说明 */
.doc-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 16px;
}

.doc-card {
  padding: 16px;
  background: #f5f7fa;
  border-radius: 8px;
  border-left: 4px solid #409eff;
}

.doc-card h4 {
  margin: 0 0 8px 0;
  font-size: 14px;
  color: #409eff;
  font-family: 'Courier New', monospace;
}

.doc-card p {
  margin: 0;
  font-size: 13px;
  color: #606266;
  line-height: 1.6;
}

/* 最佳实践 */
.practices-list {
  margin: 0;
  padding-left: 24px;
}

.practices-list li {
  font-size: 14px;
  line-height: 2;
  color: #606266;
}
</style>

三、SRI (子资源完整性)

3.1 SRI管理器

sri-manager.js

javascript
export class SRIManager {
  constructor() {
    this.algorithms = ['sha256', 'sha384', 'sha512']
  }

  // 生成SRI Hash (需要Web Crypto API)
  async generateHash(content, algorithm = 'sha384') {
    const encoder = new TextEncoder()
    const data = encoder.encode(content)

    const hashBuffer = await crypto.subtle.digest(algorithm.toUpperCase(), data)
    const hashArray = Array.from(new Uint8Array(hashBuffer))
    const hashBase64 = btoa(String.fromCharCode(...hashArray))

    return `${algorithm}-${hashBase64}`
  }

  // 生成多个算法的Hash
  async generateMultipleHashes(content, algorithms = ['sha384', 'sha512']) {
    const hashes = await Promise.all(
      algorithms.map(algo => this.generateHash(content, algo))
    )
    return hashes.join(' ')
  }

  // 为script标签添加integrity
  addScriptIntegrity(src, integrity) {
    const script = document.createElement('script')
    script.src = src
    script.integrity = integrity
    script.crossOrigin = 'anonymous'
    return script
  }

  // 为link标签添加integrity
  addLinkIntegrity(href, integrity) {
    const link = document.createElement('link')
    link.rel = 'stylesheet'
    link.href = href
    link.integrity = integrity
    link.crossOrigin = 'anonymous'
    return link
  }

  // 验证资源完整性(客户端模拟)
  async verifyIntegrity(content, expectedHash) {
    const [algorithm] = expectedHash.split('-')
    const actualHash = await this.generateHash(content, algorithm)
    return actualHash === expectedHash
  }

  // 从URL获取资源并生成Hash
  async generateHashFromUrl(url) {
    try {
      const response = await fetch(url)
      const content = await response.text()
      return await this.generateHash(content)
    } catch (error) {
      console.error('Failed to generate hash from URL:', error)
      return null
    }
  }

  // 批量生成CDN资源的SRI
  async generateCDNHashes(urls) {
    const results = []

    for (const url of urls) {
      const hash = await this.generateHashFromUrl(url)
      results.push({
        url,
        integrity: hash,
        crossorigin: 'anonymous'
      })
    }

    return results
  }
}

export default new SRIManager()

3.2 SRI演示组件

SRIDemo.vue

vue
<script setup>
import { ref } from 'vue'
import sriManager from './sri-manager.js'

const resourceUrl = ref('https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js')
const resourceType = ref('script')
const generatedHash = ref('')
const isGenerating = ref(false)

const cdnResources = ref([
  'https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js',
  'https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js',
  'https://cdn.jsdelivr.net/npm/element-plus/dist/index.css'
])

const batchResults = ref([])

// 生成单个资源的Hash
const generateHash = async () => {
  if (!resourceUrl.value) return

  isGenerating.value = true

  try {
    generatedHash.value = await sriManager.generateHashFromUrl(resourceUrl.value)
  } catch (error) {
    console.error('Generate hash failed:', error)
    alert('生成Hash失败,请检查URL')
  } finally {
    isGenerating.value = false
  }
}

// 批量生成
const batchGenerate = async () => {
  isGenerating.value = true

  try {
    batchResults.value = await sriManager.generateCDNHashes(cdnResources.value)
  } catch (error) {
    console.error('Batch generate failed:', error)
  } finally {
    isGenerating.value = false
  }
}

// 生成HTML代码
const generateHTMLCode = () => {
  if (!generatedHash.value) return ''

  if (resourceType.value === 'script') {
    return `<script src="${resourceUrl.value}"
        integrity="${generatedHash.value}"
        crossorigin="anonymous"></script>`
  } else {
    return `<link rel="stylesheet"
      href="${resourceUrl.value}"
      integrity="${generatedHash.value}"
      crossorigin="anonymous">`
  }
}
</script>

<template>
  <div class="sri-demo">
    <h2>SRI (子资源完整性) 配置</h2>

    <div class="generator-section">
      <h3>生成SRI Hash</h3>

      <div class="input-group">
        <input
          v-model="resourceUrl"
          type="text"
          placeholder="输入CDN资源URL"
          class="url-input"
        />
        <select v-model="resourceType" class="type-select">
          <option value="script">Script</option>
          <option value="stylesheet">Stylesheet</option>
        </select>
        <button @click="generateHash" :disabled="isGenerating" class="generate-btn">
          {{ isGenerating ? '生成中...' : '生成Hash' }}
        </button>
      </div>

      <div v-if="generatedHash" class="result-section">
        <div class="result-label">生成的Integrity值:</div>
        <code class="hash-code">{{ generatedHash }}</code>

        <div class="result-label">HTML代码:</div>
        <pre class="html-code">{{ generateHTMLCode() }}</pre>
      </div>
    </div>

    <div class="batch-section">
      <h3>批量生成</h3>

      <div class="cdn-list">
        <div v-for="(url, index) in cdnResources" :key="index" class="cdn-item">
          <input v-model="cdnResources[index]" type="text" class="cdn-input" />
        </div>
      </div>

      <button @click="batchGenerate" :disabled="isGenerating" class="batch-btn">
        批量生成
      </button>

      <div v-if="batchResults.length" class="batch-results">
        <div v-for="result in batchResults" :key="result.url" class="batch-result-item">
          <div class="result-url">{{ result.url }}</div>
          <div class="result-integrity">
            <code>{{ result.integrity }}</code>
          </div>
        </div>
      </div>
    </div>

    <div class="info-section">
      <h3>SRI说明</h3>
      <ul>
        <li>SRI可以验证从CDN加载的资源是否被篡改</li>
        <li>浏览器会计算资源的Hash并与integrity属性比对</li>
        <li>如果不匹配,资源会被拒绝加载</li>
        <li>必须同时设置crossorigin="anonymous"</li>
        <li>推荐使用sha384或sha512算法</li>
      </ul>
    </div>
  </div>
</template>

<style scoped>
.sri-demo {
  padding: 20px;
  max-width: 1200px;
  margin: 0 auto;
}

.generator-section,
.batch-section,
.info-section {
  margin-bottom: 30px;
  padding: 24px;
  background: white;
  border-radius: 8px;
}

.input-group {
  display: flex;
  gap: 12px;
  margin-bottom: 20px;
}

.url-input {
  flex: 1;
  padding: 10px;
  border: 1px solid #dcdfe6;
  border-radius: 4px;
}

.type-select {
  padding: 10px;
  border: 1px solid #dcdfe6;
  border-radius: 4px;
}

.generate-btn,
.batch-btn {
  padding: 10px 24px;
  background: #409eff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.result-section {
  margin-top: 20px;
}

.result-label {
  font-weight: 600;
  margin-bottom: 8px;
}

.hash-code,
.html-code {
  display: block;
  padding: 12px;
  background: #f5f7fa;
  border-radius: 4px;
  margin-bottom: 16px;
  overflow-x: auto;
}
</style>

[由于篇幅限制,继续其他部分...]


四、HTTPS部署

4.1 HTTPS配置检查器

https-checker.js

javascript
export class HTTPSChecker {
  constructor() {
    this.checks = []
  }

  // 检查是否使用HTTPS
  checkHTTPS() {
    const isHTTPS = window.location.protocol === 'https:'

    this.checks.push({
      name: 'HTTPS协议',
      passed: isHTTPS,
      message: isHTTPS ? '网站使用HTTPS协议' : '网站未使用HTTPS协议',
      severity: 'high'
    })

    return isHTTPS
  }

  // 检查混合内容
  checkMixedContent() {
    const images = document.querySelectorAll('img[src^="http:"]')
    const scripts = document.querySelectorAll('script[src^="http:"]')
    const styles = document.querySelectorAll('link[href^="http:"]')

    const hasMixedContent = images.length > 0 || scripts.length > 0 || styles.length > 0

    this.checks.push({
      name: '混合内容检查',
      passed: !hasMixedContent,
      message: hasMixedContent
        ? `检测到${images.length}个HTTP图片,${scripts.length}个HTTP脚本,${styles.length}个HTTP样式`
        : '未检测到混合内容',
      severity: 'medium'
    })

    return !hasMixedContent
  }

  // 检查HSTS
  async checkHSTS() {
    // 这个需要检查HTTP响应头,前端无法直接检查
    // 这里仅作演示
    this.checks.push({
      name: 'HSTS策略',
      passed: null,
      message: 'HSTS需要在服务器响应头中设置,前端无法直接检查',
      severity: 'medium'
    })
  }

  // 获取检查结果
  getResults() {
    return this.checks
  }

  // 生成HTTPS配置建议
  generateHTTPSConfig() {
    return {
      nginx: `server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;

    # 强制HTTPS
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    # 重定向HTTP到HTTPS
    if ($scheme != "https") {
        return 301 https://$server_name$request_uri;
    }
}`,
      apache: `<VirtualHost *:443>
    ServerName example.com

    SSLEngine on
    SSLCertificateFile /path/to/cert.pem
    SSLCertificateKeyFile /path/to/key.pem

    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
</VirtualHost>

<VirtualHost *:80>
    ServerName example.com
    Redirect permanent / https://example.com/
</VirtualHost>`
    }
  }
}

export default new HTTPSChecker()

五、Cookie安全

5.1 Cookie安全管理器

cookie-security.js

javascript
export class CookieSecurity {
  // 设置安全Cookie
  setSecureCookie(name, value, options = {}) {
    const defaults = {
      secure: true,           // 只通过HTTPS传输
      httpOnly: false,        // 前端不能设置HttpOnly,需要服务端设置
      sameSite: 'Strict',     // 防止CSRF
      maxAge: 86400,          // 1天
      path: '/',
      domain: window.location.hostname
    }

    const config = { ...defaults, ...options }

    let cookieString = `${name}=${encodeURIComponent(value)}`

    if (config.maxAge) {
      cookieString += `; Max-Age=${config.maxAge}`
    }

    if (config.expires) {
      cookieString += `; Expires=${config.expires.toUTCString()}`
    }

    if (config.path) {
      cookieString += `; Path=${config.path}`
    }

    if (config.domain) {
      cookieString += `; Domain=${config.domain}`
    }

    if (config.secure) {
      cookieString += '; Secure'
    }

    if (config.sameSite) {
      cookieString += `; SameSite=${config.sameSite}`
    }

    document.cookie = cookieString
  }

  // 获取Cookie
  getCookie(name) {
    const matches = document.cookie.match(
      new RegExp('(?:^|; )' + name.replace(/([.$?*|{}()[]\\\/\+^])/g, '\\$1') + '=([^;]*)')
    )
    return matches ? decodeURIComponent(matches[1]) : null
  }

  // 删除Cookie
  deleteCookie(name, options = {}) {
    this.setSecureCookie(name, '', {
      ...options,
      maxAge: -1
    })
  }

  // 检查Cookie安全性
  checkCookieSecurity(cookieString) {
    const checks = {
      hasSecure: /Secure/i.test(cookieString),
      hasHttpOnly: /HttpOnly/i.test(cookieString),
      hasSameSite: /SameSite=/i.test(cookieString),
      sameSiteValue: null
    }

    const sameSiteMatch = cookieString.match(/SameSite=(Strict|Lax|None)/i)
    if (sameSiteMatch) {
      checks.sameSiteValue = sameSiteMatch[1]
    }

    return checks
  }

  // 列出所有Cookie
  listCookies() {
    const cookies = document.cookie.split(';')
    return cookies.map(cookie => {
      const [name, value] = cookie.trim().split('=')
      return {
        name: name,
        value: decodeURIComponent(value || '')
      }
    })
  }
}

export default new CookieSecurity()

六、简历描述模板

内容安全策略实施 (2024.03 - 2024.08)

负责公司Web应用的内容安全策略部署,实现CSP配置、SRI完整性验证、HTTPS全站迁移和Cookie安全加固。

核心职责

  • 配置Content Security Policy,限制资源加载来源,防御XSS攻击
  • 实施SRI子资源完整性验证,保护CDN资源不被篡改
  • 推动HTTPS全站部署,配置HSTS强制安全连接
  • 加固Cookie安全,设置Secure、HttpOnly、SameSite属性
  • 建立CSP违规监控系统,实时收集和分析安全事件
技术实现
  • 使用严格的CSP策略,禁用unsafe-inline和unsafe-eval
  • 为所有CDN资源生成SRI Hash值
  • 配置Nginx反向代理,强制HTTP重定向到HTTPS
  • 使用Let's Encrypt自动化证书管理
  • 实现CSP report-uri接口收集违规报告
项目成果
  • CSP部署后XSS攻击拦截率提升到100%
  • 通过SRI验证,成功阻止3次CDN资源篡改
  • HTTPS迁移完成,全站SSL Labs评级达到A+
  • Cookie安全加固后,未发生会话劫持事件

七、SOP标准回答

面试问题: 什么是CSP?如何配置?

标准回答

"CSP是Content Security Policy的缩写,是一种浏览器安全机制,用于防御XSS等注入攻击。

CSP的核心思想是白名单。你明确告诉浏览器哪些资源是可信的,浏览器只加载这些来源的资源,其他的都拒绝。比如你设置script-src 'self',浏览器就只加载同源的JavaScript,外部域名的脚本都会被阻止。

配置CSP有两种方式。一种是HTTP响应头,服务器返回Content-Security-Policy头部。另一种是HTML的meta标签。我一般用响应头方式,因为meta标签无法设置report-uri等某些指令。

CSP有很多指令。default-src是默认策略,script-src控制JavaScript来源,style-src控制CSS来源,img-src控制图片来源,connect-src控制AJAX和WebSocket连接。每个指令可以设置多个源,比如'self'表示同源,'unsafe-inline'允许内联代码,但这不安全,应该避免。

我在项目中用的是严格模式。禁止所有内联脚本和eval,所有资源必须从白名单域名加载。对于确实需要的内联脚本,我用nonce机制。服务器生成随机nonce值,既加到CSP策略里script-src 'nonce-abc123',也加到script标签