发布于

Web安全最佳实践:构建安全可靠的前端应用

作者

Web安全最佳实践:构建安全可靠的前端应用

Web安全是现代应用开发的重要基石。本文将深入探讨前端安全的核心威胁和防护策略,帮助开发者构建更安全的Web应用。

XSS攻击防护

XSS攻击类型与防护

// XSS防护工具类
class XSSProtection {
  constructor() {
    this.htmlEntities = {
      '&': '&',
      '<': '&lt;',
      '>': '&gt;',
      '"': '&quot;',
      "'": '&#x27;',
      '/': '&#x2F;',
      '`': '&#x60;',
      '=': '&#x3D;'
    }
    
    this.allowedTags = new Set([
      'p', 'br', 'strong', 'em', 'u', 'i', 'b',
      'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
      'ul', 'ol', 'li', 'blockquote', 'code', 'pre'
    ])
    
    this.allowedAttributes = new Map([
      ['a', new Set(['href', 'title'])],
      ['img', new Set(['src', 'alt', 'title'])],
      ['*', new Set(['class', 'id'])]
    ])
  }

  // HTML实体编码
  escapeHtml(text) {
    if (typeof text !== 'string') {
      return text
    }
    
    return text.replace(/[&<>"'`=\/]/g, (match) => {
      return this.htmlEntities[match]
    })
  }

  // HTML实体解码
  unescapeHtml(text) {
    if (typeof text !== 'string') {
      return text
    }
    
    const entityMap = Object.fromEntries(
      Object.entries(this.htmlEntities).map(([key, value]) => [value, key])
    )
    
    return text.replace(/&amp;|&lt;|&gt;|&quot;|&#x27;|&#x2F;|&#x60;|&#x3D;/g, (match) => {
      return entityMap[match] || match
    })
  }

  // 安全的innerHTML设置
  safeInnerHTML(element, html) {
    // 清理HTML内容
    const cleanHtml = this.sanitizeHtml(html)
    
    // 使用DOMPurify或自定义清理
    element.innerHTML = cleanHtml
    
    // 移除所有脚本标签
    const scripts = element.querySelectorAll('script')
    scripts.forEach(script => script.remove())
    
    // 移除事件处理器属性
    this.removeEventHandlers(element)
  }

  // HTML清理
  sanitizeHtml(html) {
    // 创建临时DOM元素
    const temp = document.createElement('div')
    temp.innerHTML = html
    
    // 递归清理所有节点
    this.cleanNode(temp)
    
    return temp.innerHTML
  }

  // 清理DOM节点
  cleanNode(node) {
    // 移除不允许的标签
    const children = Array.from(node.children)
    
    children.forEach(child => {
      const tagName = child.tagName.toLowerCase()
      
      if (!this.allowedTags.has(tagName)) {
        // 移除标签但保留内容
        const textContent = child.textContent
        const textNode = document.createTextNode(textContent)
        child.parentNode.replaceChild(textNode, child)
        return
      }
      
      // 清理属性
      this.cleanAttributes(child)
      
      // 递归清理子节点
      this.cleanNode(child)
    })
  }

  // 清理属性
  cleanAttributes(element) {
    const tagName = element.tagName.toLowerCase()
    const allowedAttrs = this.allowedAttributes.get(tagName) || new Set()
    const globalAttrs = this.allowedAttributes.get('*') || new Set()
    
    const attributes = Array.from(element.attributes)
    
    attributes.forEach(attr => {
      const attrName = attr.name.toLowerCase()
      
      // 移除事件处理器
      if (attrName.startsWith('on')) {
        element.removeAttribute(attr.name)
        return
      }
      
      // 移除javascript:协议
      if (attr.value && attr.value.toLowerCase().includes('javascript:')) {
        element.removeAttribute(attr.name)
        return
      }
      
      // 检查是否为允许的属性
      if (!allowedAttrs.has(attrName) && !globalAttrs.has(attrName)) {
        element.removeAttribute(attr.name)
      }
    })
  }

  // 移除事件处理器
  removeEventHandlers(element) {
    const allElements = [element, ...element.querySelectorAll('*')]
    
    allElements.forEach(el => {
      // 移除所有事件监听器属性
      const attributes = Array.from(el.attributes)
      attributes.forEach(attr => {
        if (attr.name.toLowerCase().startsWith('on')) {
          el.removeAttribute(attr.name)
        }
      })
      
      // 清理可能的脚本内容
      if (el.tagName === 'SCRIPT') {
        el.remove()
      }
    })
  }

  // 安全的URL验证
  isValidUrl(url) {
    try {
      const urlObj = new URL(url)
      
      // 只允许http和https协议
      if (!['http:', 'https:'].includes(urlObj.protocol)) {
        return false
      }
      
      // 检查是否包含危险字符
      const dangerousPatterns = [
        /javascript:/i,
        /data:/i,
        /vbscript:/i,
        /onload=/i,
        /onerror=/i
      ]
      
      return !dangerousPatterns.some(pattern => pattern.test(url))
    } catch {
      return false
    }
  }

  // 安全的JSON解析
  safeJsonParse(jsonString, defaultValue = null) {
    try {
      // 检查是否包含危险内容
      if (this.containsDangerousContent(jsonString)) {
        console.warn('Potentially dangerous content detected in JSON')
        return defaultValue
      }
      
      return JSON.parse(jsonString)
    } catch (error) {
      console.error('JSON parse error:', error)
      return defaultValue
    }
  }

  // 检查危险内容
  containsDangerousContent(content) {
    const dangerousPatterns = [
      /<script/i,
      /javascript:/i,
      /on\w+\s*=/i,
      /<iframe/i,
      /<object/i,
      /<embed/i,
      /eval\s*\(/i,
      /Function\s*\(/i
    ]
    
    return dangerousPatterns.some(pattern => pattern.test(content))
  }

  // 内容安全策略助手
  generateCSPHeader() {
    const directives = [
      "default-src 'self'",
      "script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net",
      "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
      "font-src 'self' https://fonts.gstatic.com",
      "img-src 'self' data: https:",
      "connect-src 'self' https://api.example.com",
      "frame-ancestors 'none'",
      "base-uri 'self'",
      "form-action 'self'"
    ]
    
    return directives.join('; ')
  }
}

// 输入验证器
class InputValidator {
  constructor() {
    this.patterns = {
      email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
      phone: /^\+?[\d\s\-\(\)]+$/,
      url: /^https?:\/\/.+/,
      alphanumeric: /^[a-zA-Z0-9]+$/,
      username: /^[a-zA-Z0-9_]{3,20}$/,
      password: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/
    }
    
    this.maxLengths = {
      name: 100,
      email: 254,
      message: 5000,
      title: 200,
      description: 1000
    }
  }

  // 验证输入
  validate(input, rules) {
    const errors = []
    
    for (const [field, value] of Object.entries(input)) {
      const fieldRules = rules[field]
      if (!fieldRules) continue
      
      // 必填验证
      if (fieldRules.required && (!value || value.toString().trim() === '')) {
        errors.push(`${field} is required`)
        continue
      }
      
      // 跳过空值的其他验证
      if (!value) continue
      
      // 类型验证
      if (fieldRules.type && !this.validateType(value, fieldRules.type)) {
        errors.push(`${field} must be a valid ${fieldRules.type}`)
      }
      
      // 长度验证
      if (fieldRules.maxLength && value.toString().length > fieldRules.maxLength) {
        errors.push(`${field} must be less than ${fieldRules.maxLength} characters`)
      }
      
      if (fieldRules.minLength && value.toString().length < fieldRules.minLength) {
        errors.push(`${field} must be at least ${fieldRules.minLength} characters`)
      }
      
      // 自定义验证
      if (fieldRules.custom && !fieldRules.custom(value)) {
        errors.push(`${field} is invalid`)
      }
    }
    
    return {
      isValid: errors.length === 0,
      errors
    }
  }

  // 类型验证
  validateType(value, type) {
    const pattern = this.patterns[type]
    if (!pattern) return true
    
    return pattern.test(value.toString())
  }

  // 清理输入
  sanitizeInput(input) {
    if (typeof input !== 'string') {
      return input
    }
    
    return input
      .trim()
      .replace(/\s+/g, ' ') // 合并多个空格
      .slice(0, 10000) // 限制最大长度
  }

  // 验证文件上传
  validateFile(file, options = {}) {
    const {
      maxSize = 5 * 1024 * 1024, // 5MB
      allowedTypes = ['image/jpeg', 'image/png', 'image/gif'],
      allowedExtensions = ['.jpg', '.jpeg', '.png', '.gif']
    } = options
    
    const errors = []
    
    // 文件大小验证
    if (file.size > maxSize) {
      errors.push(`File size must be less than ${maxSize / 1024 / 1024}MB`)
    }
    
    // 文件类型验证
    if (!allowedTypes.includes(file.type)) {
      errors.push(`File type ${file.type} is not allowed`)
    }
    
    // 文件扩展名验证
    const extension = '.' + file.name.split('.').pop().toLowerCase()
    if (!allowedExtensions.includes(extension)) {
      errors.push(`File extension ${extension} is not allowed`)
    }
    
    // 文件名验证
    if (!/^[a-zA-Z0-9._-]+$/.test(file.name)) {
      errors.push('File name contains invalid characters')
    }
    
    return {
      isValid: errors.length === 0,
      errors
    }
  }
}

// 安全的DOM操作
class SafeDOM {
  static setText(element, text) {
    element.textContent = text
  }
  
  static setHTML(element, html) {
    const xssProtection = new XSSProtection()
    xssProtection.safeInnerHTML(element, html)
  }
  
  static setAttribute(element, name, value) {
    // 验证属性名
    if (!/^[a-zA-Z-]+$/.test(name)) {
      console.warn('Invalid attribute name:', name)
      return
    }
    
    // 验证属性值
    if (name.toLowerCase().startsWith('on')) {
      console.warn('Event handler attributes are not allowed:', name)
      return
    }
    
    if (typeof value === 'string' && value.toLowerCase().includes('javascript:')) {
      console.warn('JavaScript URLs are not allowed:', value)
      return
    }
    
    element.setAttribute(name, value)
  }
  
  static createElement(tagName, attributes = {}, textContent = '') {
    const element = document.createElement(tagName)
    
    // 设置属性
    Object.entries(attributes).forEach(([name, value]) => {
      this.setAttribute(element, name, value)
    })
    
    // 设置文本内容
    if (textContent) {
      this.setText(element, textContent)
    }
    
    return element
  }
  
  static createLink(href, text, attributes = {}) {
    const xssProtection = new XSSProtection()
    
    if (!xssProtection.isValidUrl(href)) {
      console.warn('Invalid URL:', href)
      return this.createElement('span', {}, text)
    }
    
    return this.createElement('a', {
      href,
      rel: 'noopener noreferrer',
      ...attributes
    }, text)
  }
}

// 使用示例
const xssProtection = new XSSProtection()
const inputValidator = new InputValidator()

// 表单验证示例
function validateForm(formData) {
  const rules = {
    name: {
      required: true,
      maxLength: 100,
      custom: (value) => !/[<>]/.test(value)
    },
    email: {
      required: true,
      type: 'email',
      maxLength: 254
    },
    message: {
      required: true,
      maxLength: 5000,
      custom: (value) => !xssProtection.containsDangerousContent(value)
    }
  }
  
  return inputValidator.validate(formData, rules)
}

// 安全的内容渲染
function renderUserContent(content, container) {
  // 清理内容
  const cleanContent = xssProtection.sanitizeHtml(content)
  
  // 安全设置HTML
  SafeDOM.setHTML(container, cleanContent)
}

// 安全的URL处理
function handleUserUrl(url) {
  if (xssProtection.isValidUrl(url)) {
    return SafeDOM.createLink(url, url, { target: '_blank' })
  } else {
    return SafeDOM.createElement('span', { class: 'invalid-url' }, 'Invalid URL')
  }
}

// 导出工具类
window.XSSProtection = XSSProtection
window.InputValidator = InputValidator
window.SafeDOM = SafeDOM

CSRF攻击防护

CSRF令牌机制

// CSRF防护管理器
class CSRFProtection {
  constructor(options = {}) {
    this.tokenName = options.tokenName || 'csrf_token'
    this.headerName = options.headerName || 'X-CSRF-Token'
    this.cookieName = options.cookieName || 'csrf_token'
    this.tokenLength = options.tokenLength || 32
    this.tokenExpiry = options.tokenExpiry || 3600000 // 1小时
    
    this.tokens = new Map()
    this.init()
  }

  init() {
    // 生成初始令牌
    this.generateToken()
    
    // 设置自动刷新
    this.setupTokenRefresh()
    
    // 拦截所有表单提交
    this.interceptForms()
    
    // 拦截AJAX请求
    this.interceptAjaxRequests()
  }

  // 生成CSRF令牌
  generateToken() {
    const token = this.createRandomToken()
    const timestamp = Date.now()
    
    this.tokens.set(token, timestamp)
    
    // 设置到cookie
    this.setCookie(this.cookieName, token)
    
    // 设置到meta标签
    this.setMetaToken(token)
    
    // 清理过期令牌
    this.cleanExpiredTokens()
    
    return token
  }

  // 创建随机令牌
  createRandomToken() {
    const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
    let token = ''
    
    for (let i = 0; i < this.tokenLength; i++) {
      token += chars.charAt(Math.floor(Math.random() * chars.length))
    }
    
    return token
  }

  // 验证令牌
  validateToken(token) {
    if (!token) return false
    
    const timestamp = this.tokens.get(token)
    if (!timestamp) return false
    
    // 检查是否过期
    const now = Date.now()
    if (now - timestamp > this.tokenExpiry) {
      this.tokens.delete(token)
      return false
    }
    
    return true
  }

  // 获取当前令牌
  getCurrentToken() {
    // 从meta标签获取
    const metaToken = document.querySelector(`meta[name="${this.tokenName}"]`)
    if (metaToken) {
      return metaToken.getAttribute('content')
    }
    
    // 从cookie获取
    return this.getCookie(this.cookieName)
  }

  // 设置Cookie
  setCookie(name, value, options = {}) {
    const {
      expires = new Date(Date.now() + this.tokenExpiry),
      path = '/',
      secure = location.protocol === 'https:',
      sameSite = 'Strict'
    } = options
    
    let cookieString = `${name}=${value}; path=${path}; SameSite=${sameSite}`
    
    if (expires) {
      cookieString += `; expires=${expires.toUTCString()}`
    }
    
    if (secure) {
      cookieString += '; Secure'
    }
    
    document.cookie = cookieString
  }

  // 获取Cookie
  getCookie(name) {
    const value = `; ${document.cookie}`
    const parts = value.split(`; ${name}=`)
    
    if (parts.length === 2) {
      return parts.pop().split(';').shift()
    }
    
    return null
  }

  // 设置Meta令牌
  setMetaToken(token) {
    let metaTag = document.querySelector(`meta[name="${this.tokenName}"]`)
    
    if (!metaTag) {
      metaTag = document.createElement('meta')
      metaTag.name = this.tokenName
      document.head.appendChild(metaTag)
    }
    
    metaTag.content = token
  }

  // 清理过期令牌
  cleanExpiredTokens() {
    const now = Date.now()
    
    for (const [token, timestamp] of this.tokens.entries()) {
      if (now - timestamp > this.tokenExpiry) {
        this.tokens.delete(token)
      }
    }
  }

  // 设置令牌刷新
  setupTokenRefresh() {
    // 每30分钟刷新一次令牌
    setInterval(() => {
      this.generateToken()
    }, 30 * 60 * 1000)
    
    // 页面可见性变化时刷新令牌
    document.addEventListener('visibilitychange', () => {
      if (!document.hidden) {
        this.generateToken()
      }
    })
  }

  // 拦截表单提交
  interceptForms() {
    document.addEventListener('submit', (e) => {
      const form = e.target
      
      if (form.tagName !== 'FORM') return
      
      // 跳过GET请求
      const method = (form.method || 'GET').toUpperCase()
      if (method === 'GET') return
      
      // 检查是否已有CSRF令牌
      let tokenInput = form.querySelector(`input[name="${this.tokenName}"]`)
      
      if (!tokenInput) {
        tokenInput = document.createElement('input')
        tokenInput.type = 'hidden'
        tokenInput.name = this.tokenName
        form.appendChild(tokenInput)
      }
      
      // 设置当前令牌
      tokenInput.value = this.getCurrentToken()
    })
  }

  // 拦截AJAX请求
  interceptAjaxRequests() {
    // 拦截fetch请求
    const originalFetch = window.fetch
    
    window.fetch = (url, options = {}) => {
      const method = (options.method || 'GET').toUpperCase()
      
      // 只处理非GET请求
      if (method !== 'GET') {
        options.headers = options.headers || {}
        options.headers[this.headerName] = this.getCurrentToken()
      }
      
      return originalFetch(url, options)
    }
    
    // 拦截XMLHttpRequest
    const originalOpen = XMLHttpRequest.prototype.open
    const originalSend = XMLHttpRequest.prototype.send
    
    XMLHttpRequest.prototype.open = function(method, url, async, user, password) {
      this._method = method.toUpperCase()
      return originalOpen.call(this, method, url, async, user, password)
    }
    
    XMLHttpRequest.prototype.send = function(data) {
      if (this._method && this._method !== 'GET') {
        this.setRequestHeader(csrfProtection.headerName, csrfProtection.getCurrentToken())
      }
      
      return originalSend.call(this, data)
    }
  }

  // 验证请求来源
  validateOrigin(request) {
    const origin = request.headers.get('Origin')
    const referer = request.headers.get('Referer')
    
    const allowedOrigins = [
      location.origin,
      // 添加其他允许的源
    ]
    
    // 检查Origin头
    if (origin && !allowedOrigins.includes(origin)) {
      return false
    }
    
    // 检查Referer头
    if (referer) {
      const refererUrl = new URL(referer)
      if (!allowedOrigins.includes(refererUrl.origin)) {
        return false
      }
    }
    
    return true
  }

  // 双重提交Cookie验证
  validateDoubleSubmitCookie(cookieToken, headerToken) {
    return cookieToken && headerToken && cookieToken === headerToken
  }
}

// SameSite Cookie管理器
class SameSiteCookieManager {
  constructor() {
    this.defaultOptions = {
      sameSite: 'Strict',
      secure: location.protocol === 'https:',
      httpOnly: false // 前端需要访问时设为false
    }
  }

  // 设置安全Cookie
  setSecureCookie(name, value, options = {}) {
    const cookieOptions = {
      ...this.defaultOptions,
      ...options
    }
    
    let cookieString = `${name}=${value}`
    
    if (cookieOptions.expires) {
      cookieString += `; expires=${cookieOptions.expires.toUTCString()}`
    }
    
    if (cookieOptions.maxAge) {
      cookieString += `; max-age=${cookieOptions.maxAge}`
    }
    
    if (cookieOptions.path) {
      cookieString += `; path=${cookieOptions.path}`
    }
    
    if (cookieOptions.domain) {
      cookieString += `; domain=${cookieOptions.domain}`
    }
    
    if (cookieOptions.secure) {
      cookieString += '; Secure'
    }
    
    if (cookieOptions.httpOnly) {
      cookieString += '; HttpOnly'
    }
    
    if (cookieOptions.sameSite) {
      cookieString += `; SameSite=${cookieOptions.sameSite}`
    }
    
    document.cookie = cookieString
  }

  // 删除Cookie
  deleteCookie(name, path = '/') {
    document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=${path}`
  }

  // 获取Cookie
  getCookie(name) {
    const value = `; ${document.cookie}`
    const parts = value.split(`; ${name}=`)
    
    if (parts.length === 2) {
      return parts.pop().split(';').shift()
    }
    
    return null
  }
}

// 请求验证器
class RequestValidator {
  constructor() {
    this.trustedDomains = new Set([
      location.hostname,
      // 添加其他信任的域名
    ])
  }

  // 验证请求URL
  validateRequestUrl(url) {
    try {
      const urlObj = new URL(url, location.origin)
      
      // 检查协议
      if (!['http:', 'https:'].includes(urlObj.protocol)) {
        return false
      }
      
      // 检查域名
      if (!this.trustedDomains.has(urlObj.hostname)) {
        return false
      }
      
      return true
    } catch {
      return false
    }
  }

  // 验证表单数据
  validateFormData(formData) {
    const maxFieldLength = 10000
    const maxFieldCount = 100
    
    let fieldCount = 0
    
    for (const [key, value] of formData.entries()) {
      fieldCount++
      
      // 检查字段数量
      if (fieldCount > maxFieldCount) {
        return false
      }
      
      // 检查字段长度
      if (typeof value === 'string' && value.length > maxFieldLength) {
        return false
      }
      
      // 检查字段名
      if (!/^[a-zA-Z0-9_\-\[\]]+$/.test(key)) {
        return false
      }
    }
    
    return true
  }

  // 验证JSON数据
  validateJsonData(data) {
    try {
      const jsonString = JSON.stringify(data)
      
      // 检查JSON大小
      if (jsonString.length > 1000000) { // 1MB
        return false
      }
      
      // 检查嵌套深度
      if (this.getObjectDepth(data) > 10) {
        return false
      }
      
      return true
    } catch {
      return false
    }
  }

  // 获取对象深度
  getObjectDepth(obj, depth = 0) {
    if (depth > 10) return depth // 防止无限递归
    
    if (typeof obj !== 'object' || obj === null) {
      return depth
    }
    
    let maxDepth = depth
    
    for (const value of Object.values(obj)) {
      const currentDepth = this.getObjectDepth(value, depth + 1)
      maxDepth = Math.max(maxDepth, currentDepth)
    }
    
    return maxDepth
  }
}

// 初始化CSRF防护
const csrfProtection = new CSRFProtection({
  tokenName: 'csrf_token',
  headerName: 'X-CSRF-Token',
  tokenLength: 32,
  tokenExpiry: 3600000 // 1小时
})

const cookieManager = new SameSiteCookieManager()
const requestValidator = new RequestValidator()

// 安全的AJAX请求封装
class SecureAjax {
  static async request(url, options = {}) {
    // 验证URL
    if (!requestValidator.validateRequestUrl(url)) {
      throw new Error('Invalid request URL')
    }
    
    // 设置默认选项
    const defaultOptions = {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
      },
      credentials: 'same-origin'
    }
    
    const mergedOptions = { ...defaultOptions, ...options }
    
    // 添加CSRF令牌
    if (mergedOptions.method !== 'GET') {
      mergedOptions.headers[csrfProtection.headerName] = csrfProtection.getCurrentToken()
    }
    
    // 验证请求数据
    if (mergedOptions.body) {
      if (typeof mergedOptions.body === 'string') {
        try {
          const data = JSON.parse(mergedOptions.body)
          if (!requestValidator.validateJsonData(data)) {
            throw new Error('Invalid request data')
          }
        } catch {
          // 非JSON数据,跳过验证
        }
      }
    }
    
    try {
      const response = await fetch(url, mergedOptions)
      
      // 检查响应状态
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`)
      }
      
      return response
    } catch (error) {
      console.error('Secure AJAX request failed:', error)
      throw error
    }
  }
  
  static async get(url, options = {}) {
    return this.request(url, { ...options, method: 'GET' })
  }
  
  static async post(url, data, options = {}) {
    return this.request(url, {
      ...options,
      method: 'POST',
      body: JSON.stringify(data)
    })
  }
  
  static async put(url, data, options = {}) {
    return this.request(url, {
      ...options,
      method: 'PUT',
      body: JSON.stringify(data)
    })
  }
  
  static async delete(url, options = {}) {
    return this.request(url, { ...options, method: 'DELETE' })
  }
}

// 使用示例
async function submitForm(formData) {
  try {
    const response = await SecureAjax.post('/api/submit', formData)
    const result = await response.json()
    console.log('Form submitted successfully:', result)
  } catch (error) {
    console.error('Form submission failed:', error)
  }
}

// 导出安全工具
window.CSRFProtection = CSRFProtection
window.SameSiteCookieManager = SameSiteCookieManager
window.RequestValidator = RequestValidator
window.SecureAjax = SecureAjax

内容安全策略(CSP)

CSP策略配置

// CSP策略管理器
class CSPManager {
  constructor() {
    this.policies = new Map()
    this.violations = []
    this.reportEndpoint = '/api/csp-report'
    
    this.init()
  }

  init() {
    // 设置违规报告监听
    this.setupViolationReporting()
    
    // 动态设置CSP策略
    this.setDefaultPolicy()
  }

  // 设置默认CSP策略
  setDefaultPolicy() {
    const policy = {
      'default-src': ["'self'"],
      'script-src': [
        "'self'",
        "'unsafe-inline'", // 仅在必要时使用
        'https://cdn.jsdelivr.net',
        'https://unpkg.com'
      ],
      'style-src': [
        "'self'",
        "'unsafe-inline'",
        'https://fonts.googleapis.com'
      ],
      'font-src': [
        "'self'",
        'https://fonts.gstatic.com'
      ],
      'img-src': [
        "'self'",
        'data:',
        'https:'
      ],
      'connect-src': [
        "'self'",
        'https://api.example.com'
      ],
      'media-src': ["'self'"],
      'object-src': ["'none'"],
      'frame-src': ["'none'"],
      'frame-ancestors': ["'none'"],
      'base-uri': ["'self'"],
      'form-action': ["'self'"],
      'upgrade-insecure-requests': true,
      'block-all-mixed-content': true
    }
    
    this.setPolicy('default', policy)
  }

  // 设置CSP策略
  setPolicy(name, policy) {
    this.policies.set(name, policy)
    
    // 生成CSP头部值
    const headerValue = this.generateCSPHeader(policy)
    
    // 设置meta标签
    this.setCSPMetaTag(headerValue)
    
    console.log(`CSP policy "${name}" set:`, headerValue)
  }

  // 生成CSP头部值
  generateCSPHeader(policy) {
    const directives = []
    
    for (const [directive, values] of Object.entries(policy)) {
      if (typeof values === 'boolean' && values) {
        directives.push(directive)
      } else if (Array.isArray(values)) {
        directives.push(`${directive} ${values.join(' ')}`)
      }
    }
    
    return directives.join('; ')
  }

  // 设置CSP meta标签
  setCSPMetaTag(content) {
    let metaTag = document.querySelector('meta[http-equiv="Content-Security-Policy"]')
    
    if (!metaTag) {
      metaTag = document.createElement('meta')
      metaTag.setAttribute('http-equiv', 'Content-Security-Policy')
      document.head.appendChild(metaTag)
    }
    
    metaTag.setAttribute('content', content)
  }

  // 添加源到指令
  addSource(directive, source) {
    const policy = this.policies.get('default')
    if (!policy) return
    
    if (!policy[directive]) {
      policy[directive] = []
    }
    
    if (!policy[directive].includes(source)) {
      policy[directive].push(source)
      this.setPolicy('default', policy)
    }
  }

  // 移除源
  removeSource(directive, source) {
    const policy = this.policies.get('default')
    if (!policy || !policy[directive]) return
    
    const index = policy[directive].indexOf(source)
    if (index > -1) {
      policy[directive].splice(index, 1)
      this.setPolicy('default', policy)
    }
  }

  // 设置违规报告
  setupViolationReporting() {
    document.addEventListener('securitypolicyviolation', (e) => {
      const violation = {
        blockedURI: e.blockedURI,
        columnNumber: e.columnNumber,
        disposition: e.disposition,
        documentURI: e.documentURI,
        effectiveDirective: e.effectiveDirective,
        lineNumber: e.lineNumber,
        originalPolicy: e.originalPolicy,
        referrer: e.referrer,
        sample: e.sample,
        sourceFile: e.sourceFile,
        statusCode: e.statusCode,
        violatedDirective: e.violatedDirective,
        timestamp: new Date().toISOString()
      }
      
      this.handleViolation(violation)
    })
  }

  // 处理CSP违规
  handleViolation(violation) {
    this.violations.push(violation)
    
    console.warn('CSP Violation:', violation)
    
    // 发送违规报告到服务器
    this.reportViolation(violation)
    
    // 限制违规记录数量
    if (this.violations.length > 1000) {
      this.violations.shift()
    }
  }

  // 报告违规
  async reportViolation(violation) {
    try {
      await fetch(this.reportEndpoint, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          'csp-report': violation
        })
      })
    } catch (error) {
      console.error('Failed to report CSP violation:', error)
    }
  }

  // 获取违规统计
  getViolationStats() {
    const stats = {}
    
    this.violations.forEach(violation => {
      const directive = violation.effectiveDirective
      stats[directive] = (stats[directive] || 0) + 1
    })
    
    return stats
  }

  // 生成nonce
  generateNonce() {
    const array = new Uint8Array(16)
    crypto.getRandomValues(array)
    return btoa(String.fromCharCode.apply(null, array))
  }

  // 安全地添加内联脚本
  addInlineScript(scriptContent, nonce) {
    const script = document.createElement('script')
    
    if (nonce) {
      script.nonce = nonce
      // 更新CSP策略以包含nonce
      this.addSource('script-src', `'nonce-${nonce}'`)
    }
    
    script.textContent = scriptContent
    document.head.appendChild(script)
    
    return script
  }

  // 安全地添加内联样式
  addInlineStyle(styleContent, nonce) {
    const style = document.createElement('style')
    
    if (nonce) {
      style.nonce = nonce
      // 更新CSP策略以包含nonce
      this.addSource('style-src', `'nonce-${nonce}'`)
    }
    
    style.textContent = styleContent
    document.head.appendChild(style)
    
    return style
  }

  // 验证外部资源
  validateExternalResource(url, type) {
    const policy = this.policies.get('default')
    if (!policy) return false
    
    const directive = this.getDirectiveForResourceType(type)
    const allowedSources = policy[directive] || policy['default-src'] || []
    
    return this.isSourceAllowed(url, allowedSources)
  }

  // 获取资源类型对应的指令
  getDirectiveForResourceType(type) {
    const typeMap = {
      'script': 'script-src',
      'style': 'style-src',
      'image': 'img-src',
      'font': 'font-src',
      'media': 'media-src',
      'object': 'object-src',
      'frame': 'frame-src'
    }
    
    return typeMap[type] || 'default-src'
  }

  // 检查源是否被允许
  isSourceAllowed(url, allowedSources) {
    try {
      const urlObj = new URL(url)
      
      return allowedSources.some(source => {
        if (source === "'self'") {
          return urlObj.origin === location.origin
        }
        
        if (source === "'none'") {
          return false
        }
        
        if (source === 'data:') {
          return urlObj.protocol === 'data:'
        }
        
        if (source === 'https:') {
          return urlObj.protocol === 'https:'
        }
        
        if (source.startsWith('https://')) {
          return url.startsWith(source)
        }
        
        // 通配符匹配
        if (source.includes('*')) {
          const pattern = source.replace(/\*/g, '.*')
          const regex = new RegExp(`^${pattern}$`)
          return regex.test(url)
        }
        
        return url.startsWith(source)
      })
    } catch {
      return false
    }
  }

  // 创建安全的iframe
  createSecureIframe(src, options = {}) {
    if (!this.validateExternalResource(src, 'frame')) {
      console.error('Iframe source not allowed by CSP:', src)
      return null
    }
    
    const iframe = document.createElement('iframe')
    iframe.src = src
    
    // 设置安全属性
    iframe.sandbox = options.sandbox || 'allow-scripts allow-same-origin'
    iframe.loading = 'lazy'
    
    // 设置其他属性
    Object.entries(options).forEach(([key, value]) => {
      if (key !== 'sandbox') {
        iframe.setAttribute(key, value)
      }
    })
    
    return iframe
  }
}

// 安全资源加载器
class SecureResourceLoader {
  constructor(cspManager) {
    this.cspManager = cspManager
  }

  // 安全加载脚本
  async loadScript(src, options = {}) {
    if (!this.cspManager.validateExternalResource(src, 'script')) {
      throw new Error(`Script source not allowed by CSP: ${src}`)
    }
    
    return new Promise((resolve, reject) => {
      const script = document.createElement('script')
      script.src = src
      script.async = options.async !== false
      script.defer = options.defer || false
      
      script.onload = () => resolve(script)
      script.onerror = () => reject(new Error(`Failed to load script: ${src}`))
      
      document.head.appendChild(script)
    })
  }

  // 安全加载样式
  async loadStylesheet(href, options = {}) {
    if (!this.cspManager.validateExternalResource(href, 'style')) {
      throw new Error(`Stylesheet source not allowed by CSP: ${href}`)
    }
    
    return new Promise((resolve, reject) => {
      const link = document.createElement('link')
      link.rel = 'stylesheet'
      link.href = href
      
      link.onload = () => resolve(link)
      link.onerror = () => reject(new Error(`Failed to load stylesheet: ${href}`))
      
      document.head.appendChild(link)
    })
  }

  // 安全加载图片
  async loadImage(src, options = {}) {
    if (!this.cspManager.validateExternalResource(src, 'image')) {
      throw new Error(`Image source not allowed by CSP: ${src}`)
    }
    
    return new Promise((resolve, reject) => {
      const img = new Image()
      
      img.onload = () => resolve(img)
      img.onerror = () => reject(new Error(`Failed to load image: ${src}`))
      
      // 设置属性
      Object.entries(options).forEach(([key, value]) => {
        img[key] = value
      })
      
      img.src = src
    })
  }
}

// 初始化CSP管理器
const cspManager = new CSPManager()
const resourceLoader = new SecureResourceLoader(cspManager)

// 使用示例
async function loadExternalResources() {
  try {
    // 安全加载脚本
    await resourceLoader.loadScript('https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js')
    
    // 安全加载样式
    await resourceLoader.loadStylesheet('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap')
    
    // 安全加载图片
    const img = await resourceLoader.loadImage('https://example.com/image.jpg', {
      alt: 'Example image',
      loading: 'lazy'
    })
    
    document.body.appendChild(img)
    
  } catch (error) {
    console.error('Failed to load external resources:', error)
  }
}

// 导出CSP工具
window.CSPManager = CSPManager
window.SecureResourceLoader = SecureResourceLoader

总结

Web安全最佳实践的核心要点:

  1. XSS防护:输入验证、输出编码、CSP策略
  2. CSRF防护:令牌验证、SameSite Cookie、来源检查
  3. CSP策略:内容安全策略、资源白名单、违规报告
  4. 安全编码:输入验证、安全DOM操作、错误处理
  5. 持续监控:安全日志、违规报告、定期审计

Web安全是一个持续的过程,需要在开发的每个阶段都考虑安全因素。通过实施这些最佳实践,可以显著提高Web应用的安全性。