返回笔记首页

18.1 前端常见安全问题

主题配置

一、技术实现方案

1.1 前端安全架构

plain
前端安全防护体系
  ├── XSS防御层
  │   ├── 输入过滤
  │   ├── 输出转义
  │   ├── CSP策略
  │   └── HttpOnly Cookie
  │
  ├── CSRF防御层
  │   ├── Token验证
  │   ├── SameSite Cookie
  │   ├── Referer检查
  │   └── 双重Cookie验证
  │
  ├── 点击劫持防护
  │   ├── X-Frame-Options
  │   ├── Frame Busting
  │   └── CSP frame-ancestors
  │
  ├── 注入防护
  │   ├── 参数化查询
  │   ├── 输入验证
  │   └── 白名单过滤
  │
  └── 数据加密层
      ├── 传输加密(HTTPS)
      ├── 存储加密
      ├── 敏感字段脱敏
      └── 密钥管理

五、SQL注入防护

5.1 前端输入验证

sql-injection-defense.js

javascript
export class SQLInjectionDefense {
  constructor() {
    this.sqlKeywords = [
      'SELECT', 'INSERT', 'UPDATE', 'DELETE', 'DROP', 'CREATE',
      'ALTER', 'EXEC', 'EXECUTE', 'UNION', 'DECLARE', 'CAST'
    ]

    this.dangerousPatterns = [
      /(\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|EXEC|EXECUTE|UNION|DECLARE)\b)/gi,
      /(--|#|\/\*|\*\/)/g,  // SQL注释
      /(\bOR\b.*=.*)/gi,     // OR 1=1
      /(\bAND\b.*=.*)/gi,    // AND 1=1
      /(;)/g,                // 分号
      /('|")/g              // 引号
    ]
  }

  // 检测SQL注入
  detectSQLInjection(input) {
    for (const pattern of this.dangerousPatterns) {
      if (pattern.test(input)) {
        return {
          detected: true,
          pattern: pattern.source,
          input: input
        }
      }
    }

    return { detected: false }
  }

  // 过滤危险字符
  sanitizeInput(input) {
    // 移除SQL关键字
    let sanitized = input

    this.sqlKeywords.forEach(keyword => {
      const regex = new RegExp(`\\b${keyword}\\b`, 'gi')
      sanitized = sanitized.replace(regex, '')
    })

    // 移除特殊字符
    sanitized = sanitized
      .replace(/--/g, '')
      .replace(/#/g, '')
      .replace(/;/g, '')
      .replace(/'/g, "''")  // 转义单引号
      .replace(/"/g, '""')  // 转义双引号

    return sanitized.trim()
  }

  // 参数化查询示例(前端不直接执行SQL,这里仅作演示)
  buildParameterizedQuery(params) {
    return {
      query: 'SELECT * FROM users WHERE username = ? AND age > ?',
      params: params
    }
  }

  // 白名单验证
  validateWithWhitelist(input, whitelist) {
    return whitelist.includes(input)
  }

  // 类型验证
  validateType(input, expectedType) {
    switch (expectedType) {
      case 'number':
        return !isNaN(input) && isFinite(input)
      case 'email':
        return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input)
      case 'alphanumeric':
        return /^[a-zA-Z0-9]+$/.test(input)
      case 'string':
        return typeof input === 'string' && input.length > 0
      default:
        return false
    }
  }

  // 长度限制
  validateLength(input, min, max) {
    const length = input.length
    return length >= min && length <= max
  }
}

export default new SQLInjectionDefense()

六、敏感信息加密

6.1 加密工具类

crypto-utils.js

javascript
import CryptoJS from 'crypto-js'

export class CryptoUtils {
  constructor() {
    // 密钥应该从环境变量或配置中读取,不要硬编码
    this.secretKey = process.env.VUE_APP_SECRET_KEY || 'default-secret-key'
  }

  // AES加密
  encryptAES(text) {
    return CryptoJS.AES.encrypt(text, this.secretKey).toString()
  }

  // AES解密
  decryptAES(ciphertext) {
    const bytes = CryptoJS.AES.decrypt(ciphertext, this.secretKey)
    return bytes.toString(CryptoJS.enc.Utf8)
  }

  // Base64编码
  encodeBase64(text) {
    return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(text))
  }

  // Base64解码
  decodeBase64(base64) {
    return CryptoJS.enc.Base64.parse(base64).toString(CryptoJS.enc.Utf8)
  }

  // MD5哈希
  hashMD5(text) {
    return CryptoJS.MD5(text).toString()
  }

  // SHA256哈希
  hashSHA256(text) {
    return CryptoJS.SHA256(text).toString()
  }

  // 生成随机盐
  generateSalt(length = 16) {
    const array = new Uint8Array(length)
    crypto.getRandomValues(array)
    return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('')
  }

  // 加盐哈希
  hashWithSalt(text, salt) {
    return this.hashSHA256(text + salt)
  }

  // 手机号脱敏
  maskPhone(phone) {
    return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
  }

  // 身份证号脱敏
  maskIdCard(idCard) {
    return idCard.replace(/(\d{6})\d{8}(\d{4})/, '$1********$2')
  }

  // 邮箱脱敏
  maskEmail(email) {
    return email.replace(/(.{2}).*@/, '$1***@')
  }

  // 银行卡号脱敏
  maskBankCard(cardNo) {
    return cardNo.replace(/(\d{4})\d+(\d{4})/, '$1 **** **** $2')
  }

  // 密码强度检测
  checkPasswordStrength(password) {
    const checks = {
      length: password.length >= 8,
      lowercase: /[a-z]/.test(password),
      uppercase: /[A-Z]/.test(password),
      number: /[0-9]/.test(password),
      special: /[!@#$%^&*(),.?":{}|<>]/.test(password)
    }

    const score = Object.values(checks).filter(Boolean).length

    if (score <= 2) return { strength: 'weak', score }
    if (score === 3) return { strength: 'medium', score }
    if (score === 4) return { strength: 'strong', score }
    return { strength: 'very-strong', score }
  }
}

export default new CryptoUtils()

七、综合安全演示组件

SecurityDemo.vue

vue
<script setup>
import { ref } from 'vue'
import xssDefense from './xss-defense.js'
import csrfDefense from './csrf-defense.js'
import sqlInjectionDefense from './sql-injection-defense.js'
import cryptoUtils from './crypto-utils.js'

// XSS测试
const xssInput = ref('')
const xssResult = ref(null)

const testXSS = () => {
  xssResult.value = xssDefense.detectXss(xssInput.value)
}

// SQL注入测试
const sqlInput = ref('')
const sqlResult = ref(null)

const testSQLInjection = () => {
  sqlResult.value = sqlInjectionDefense.detectSQLInjection(sqlInput.value)
}

// 数据加密测试
const plainText = ref('')
const encrypted = ref('')
const decrypted = ref('')

const encrypt = () => {
  encrypted.value = cryptoUtils.encryptAES(plainText.value)
}

const decrypt = () => {
  decrypted.value = cryptoUtils.decryptAES(encrypted.value)
}

// 敏感信息脱敏
const sensitiveData = ref({
  phone: '13800138000',
  idCard: '110101199001011234',
  email: 'user@example.com',
  bankCard: '6222021234567890123'
})

const maskedData = ref({})

const maskSensitiveInfo = () => {
  maskedData.value = {
    phone: cryptoUtils.maskPhone(sensitiveData.value.phone),
    idCard: cryptoUtils.maskIdCard(sensitiveData.value.idCard),
    email: cryptoUtils.maskEmail(sensitiveData.value.email),
    bankCard: cryptoUtils.maskBankCard(sensitiveData.value.bankCard)
  }
}
</script>

<template>
  <div class="security-demo">
    <h1>前端安全综合演示</h1>

    <!-- XSS防御 -->
    <section class="demo-section">
      <h2>XSS攻击防御</h2>
      <div class="input-group">
        <input v-model="xssInput" placeholder="输入可能的XSS代码" />
        <button @click="testXSS">检测XSS</button>
      </div>
      <div v-if="xssResult" class="result">
        <div :class="['status', xssResult.detected ? 'danger' : 'safe']">
          {{ xssResult.detected ? '检测到XSS攻击' : '安全' }}
        </div>
      </div>
    </section>

    <!-- SQL注入防护 -->
    <section class="demo-section">
      <h2>SQL注入防护</h2>
      <div class="input-group">
        <input v-model="sqlInput" placeholder="输入可能的SQL注入" />
        <button @click="testSQLInjection">检测注入</button>
      </div>
      <div v-if="sqlResult" class="result">
        <div :class="['status', sqlResult.detected ? 'danger' : 'safe']">
          {{ sqlResult.detected ? '检测到SQL注入' : '安全' }}
        </div>
      </div>
    </section>

    <!-- 数据加密 -->
    <section class="demo-section">
      <h2>数据加密</h2>
      <div class="input-group">
        <input v-model="plainText" placeholder="输入要加密的文本" />
        <button @click="encrypt">加密</button>
      </div>
      <div v-if="encrypted" class="result">
        <div class="label">加密结果:</div>
        <code>{{ encrypted }}</code>
        <button @click="decrypt">解密</button>
      </div>
      <div v-if="decrypted" class="result">
        <div class="label">解密结果:</div>
        <span>{{ decrypted }}</span>
      </div>
    </section>

    <!-- 敏感信息脱敏 -->
    <section class="demo-section">
      <h2>敏感信息脱敏</h2>
      <button @click="maskSensitiveInfo">脱敏处理</button>

      <div v-if="Object.keys(maskedData).length" class="mask-result">
        <div class="mask-item">
          <span class="label">手机号:</span>
          <span class="original">{{ sensitiveData.phone }}</span>
          <span class="arrow">→</span>
          <span class="masked">{{ maskedData.phone }}</span>
        </div>
        <div class="mask-item">
          <span class="label">身份证:</span>
          <span class="original">{{ sensitiveData.idCard }}</span>
          <span class="arrow">→</span>
          <span class="masked">{{ maskedData.idCard }}</span>
        </div>
        <div class="mask-item">
          <span class="label">邮箱:</span>
          <span class="original">{{ sensitiveData.email }}</span>
          <span class="arrow">→</span>
          <span class="masked">{{ maskedData.email }}</span>
        </div>
        <div class="mask-item">
          <span class="label">银行卡:</span>
          <span class="original">{{ sensitiveData.bankCard }}</span>
          <span class="arrow">→</span>
          <span class="masked">{{ maskedData.bankCard }}</span>
        </div>
      </div>
    </section>
  </div>
</template>

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

.demo-section {
  margin-bottom: 40px;
  padding: 24px;
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

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

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

button {
  padding: 10px 20px;
  background: #409eff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.result {
  margin-top: 16px;
  padding: 16px;
  background: #f5f7fa;
  border-radius: 4px;
}

.status.danger {
  color: #f56c6c;
  font-weight: bold;
}

.status.safe {
  color: #67c23a;
  font-weight: bold;
}

.mask-result {
  margin-top: 16px;
}

.mask-item {
  padding: 12px;
  background: #f5f7fa;
  border-radius: 4px;
  margin-bottom: 8px;
  display: flex;
  align-items: center;
  gap: 12px;
}

.mask-item .label {
  font-weight: 600;
  min-width: 80px;
}

.mask-item .original {
  color: #909399;
}

.mask-item .arrow {
  color: #409eff;
}

.mask-item .masked {
  color: #303133;
  font-weight: 600;
}
</style>

八、简历描述模板

前端安全体系建设 (2023.08 - 2024.02)

负责公司前端安全防护体系搭建,实现XSS、CSRF、点击劫持等常见攻击的全面防御,覆盖15+核心业务系统。

核心职责

  • 开发XSS防御工具库,实现HTML转义、内容清理和URL过滤
  • 实现CSRF Token验证机制,采用双重Cookie方案
  • 部署点击劫持防护,配置X-Frame-Options和CSP策略
  • 建立SQL注入防护机制,实现输入验证和参数化查询
  • 开发敏感信息加密模块,实现AES加密和数据脱敏
技术实现
  • 使用DOMPurify实现HTML清理,支持自定义白名单
  • 实现CSRF Token自动添加的Axios拦截器
  • 配置Frame Busting防止页面被恶意嵌套
  • 使用CryptoJS实现AES/SHA256加密
  • 开发正则表达式规则引擎检测SQL注入
项目成果
  • XSS攻击拦截率99.9%,有效防御500+次攻击尝试
  • CSRF防护覆盖所有敏感接口,零安全事故
  • 敏感数据加密存储,通过等保三级认证
  • 建立安全编码规范,开发人员安全意识提升40%

九、SOP标准回答

面试问题: 如何防御XSS攻击?

标准回答

"XSS防御主要从三个方面入手。

第一是输入过滤。用户输入的数据要经过严格验证,使用白名单机制只允许安全的字符。比如富文本编辑器的内容,我用DOMPurify库清理,只保留b、i、strong这些安全标签,移除script、iframe等危险标签。对于URL,只允许http、https、mailto协议,javascript伪协议会被过滤掉。

第二是输出转义。数据输出到页面时要根据上下文选择合适的转义方式。输出到HTML内容用HTML转义,把<>转成<>。输出到HTML属性用属性转义,转义引号和特殊字符。输出到JavaScript字符串用JS转义,转义反斜杠和引号。Vue框架默认会对插值进行转义,但如果用v-html就要格外小心。

第三是使用CSP。配置Content-Security-Policy响应头,限制脚本来源。比如设置script-src 'self'只允许加载同源脚本,禁用eval和inline script。这样即使有XSS漏洞,攻击者注入的脚本也无法执行。

另外还要设置HttpOnly Cookie,防止Cookie被脚本读取。这样即使有XSS漏洞,攻击者也拿不到用户的session。

实际项目中我封装了XSS防御工具类,包括HTML转义、URL清理、属性转义等方法,开发人员直接调用就行。还在构建时集成了XSS检测工具,自动扫描代码中的危险用法。上线后通过安全测试,成功拦截了500多次XSS攻击尝试,防御率99.9%。"

面试问题: CSRF和XSS有什么区别?如何防御CSRF?

标准回答

"CSRF和XSS虽然都是前端常见攻击,但原理完全不同。

XSS是跨站脚本攻击,攻击者在网站注入恶意脚本,利用用户的权限执行操作。比如在评论框注入