Files
gym-manage/docs/design/前端安全规范.md
T
2026-03-05 13:48:13 +08:00

22 KiB
Raw Blame History

健身房管理系统前端安全规范文档

文档编号: GYM-FE-SEC-001
版本: v1.0
日期: 2026-03-04
作者: 张翔
状态: 初稿


文档修订历史

版本 日期 作者 修订内容
v1.0 2026-03-04 张翔 创建前端安全规范

参考文档

  • 《健身房管理系统前端技术架构详细设计》 GYM-FE-ARCH-001
  • OWASP Top 10 Web Application Security Risks
  • Content Security Policy (CSP) Level 3
  • Web Content Accessibility Guidelines (WCAG) 2.1

一、安全概述

1.1 安全目标

  • 数据安全:保护用户敏感数据(手机号、身份证号、银行卡号等)
  • 交易安全:确保支付和充值操作的安全性
  • 身份安全:防止未授权访问和身份冒用
  • 隐私保护:符合GDPR等隐私法规要求
  • 合规性:符合金融行业安全标准和监管要求

1.2 安全原则

原则 描述 实施方式
最小权限 只授予必要的权限 基于角色的访问控制(RBAC
纵深防御 多层安全防护 输入验证、输出转义、加密传输
默认安全 默认配置安全 CSP策略、安全Headers
审计追踪 记录关键操作 操作日志、异常日志
持续监控 实时安全监控 错误监控、性能监控

1.3 安全威胁

威胁类型 风险等级 防护措施
XSS(跨站脚本攻击) 输入过滤、输出转义、CSP
CSRF(跨站请求伪造) Token验证、SameSite Cookie
点击劫持 X-Frame-Options、CSP
中间人攻击 HTTPS、HSTS
敏感信息泄露 数据加密、脱敏显示
暴力破解 验证码、登录限制
会话劫持 安全Cookie、会话超时

二、XSS防护

2.1 输入验证

2.1.1 白名单验证

// utils/validator.ts
export function sanitizeInput(input: string, allowedChars: RegExp): string {
  return input.replace(allowedChars, '')
}

export function validatePhoneNumber(phone: string): boolean {
  const phoneRegex = /^1[3-9]\d{9}$/
  return phoneRegex.test(phone)
}

export function validateIdCard(idCard: string): boolean {
  const idCardRegex = /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/
  return idCardRegex.test(idCard)
}

export function validateEmail(email: string): boolean {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
  return emailRegex.test(email)
}

2.1.2 输入长度限制

// utils/validator.ts
export const MAX_INPUT_LENGTH = {
  name: 64,
  phone: 11,
  idCard: 18,
  address: 256,
  remark: 512
}

export function validateLength(input: string, maxLength: number): boolean {
  return input.length <= maxLength
}

2.2 输出转义

2.2.1 HTML转义

// utils/sanitize.ts
import DOMPurify from 'dompurify'

export function sanitizeHtml(html: string): string {
  return DOMPurify.sanitize(html, {
    ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u'],
    ALLOWED_ATTR: []
  })
}

export function escapeHtml(text: string): string {
  const map: Record<string, string> = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#039;'
  }
  return text.replace(/[&<>"']/g, (m) => map[m])
}

2.2.2 URL转义

// utils/sanitize.ts
export function sanitizeUrl(url: string): string {
  try {
    const parsed = new URL(url)
    if (!['http:', 'https:'].includes(parsed.protocol)) {
      return '#'
    }
    return url
  } catch {
    return '#'
  }
}

2.3 CSP策略

2.3.1 基础CSP配置

<!-- index.html -->
<meta http-equiv="Content-Security-Policy" 
      content="default-src 'self'; 
               script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net; 
               style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; 
               img-src 'self' data: https:; 
               font-src 'self' https://cdn.jsdelivr.net; 
               connect-src 'self' https://api.example.com; 
               frame-ancestors 'none'; 
               base-uri 'self'; 
               form-action 'self';">

2.3.2 动态CSP配置

// utils/csp.ts
export function generateCSP(config: CSPConfig): string {
  const directives = [
    `default-src ${config.defaultSrc.join(' ')}`,
    `script-src ${config.scriptSrc.join(' ')}`,
    `style-src ${config.styleSrc.join(' ')}`,
    `img-src ${config.imgSrc.join(' ')}`,
    `connect-src ${config.connectSrc.join(' ')}`
  ]
  
  return directives.join('; ')
}

// 使用
const csp = generateCSP({
  defaultSrc: ["'self'"],
  scriptSrc: ["'self'", "https://cdn.jsdelivr.net"],
  styleSrc: ["'self'", "'unsafe-inline'"],
  imgSrc: ["'self'", "data:", "https:"],
  connectSrc: ["'self'", "https://api.example.com"]
})

document.querySelector('meta[http-equiv="Content-Security-Policy"]')
  ?.setAttribute('content', csp)

三、CSRF防护

3.1 Token验证

3.1.1 Token生成与存储

// utils/csrf.ts
export function generateCSRFToken(): string {
  const array = new Uint8Array(32)
  crypto.getRandomValues(array)
  return Array.from(array, (byte) => byte.toString(16).padStart(2, '0')).join('')
}

export function setCSRFToken(token: string): void {
  localStorage.setItem('csrf_token', token)
}

export function getCSRFToken(): string {
  return localStorage.getItem('csrf_token') || ''
}

3.1.2 Token注入请求

// api/request.ts
import { getCSRFToken } from '@/utils/csrf'

instance.interceptors.request.use((config) => {
  const csrfToken = getCSRFToken()
  if (csrfToken) {
    config.headers['X-CSRF-Token'] = csrfToken
  }
  return config
})
// api/request.ts
instance.interceptors.request.use((config) => {
  config.withCredentials = true
  return config
})

3.3 双重Cookie提交

// utils/csrf.ts
export function setDoubleSubmitCookie(token: string): void {
  document.cookie = `csrf_token=${token}; path=/; SameSite=Strict; Secure`
}

export function getDoubleSubmitCookie(): string {
  const match = document.cookie.match(/csrf_token=([^;]+)/)
  return match ? match[1] : ''
}

四、数据安全

4.1 数据加密

4.1.1 AES加密

// utils/crypto.ts
import CryptoJS from 'crypto-js'

const SECRET_KEY = import.meta.env.VITE_CRYPTO_SECRET_KEY

export function encrypt(text: string): string {
  const key = CryptoJS.enc.Utf8.parse(SECRET_KEY)
  const iv = CryptoJS.lib.WordArray.random(16)
  const encrypted = CryptoJS.AES.encrypt(text, key, {
    iv: iv,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7
  })
  return iv.toString() + ':' + encrypted.toString()
}

export function decrypt(ciphertext: string): string {
  const key = CryptoJS.enc.Utf8.parse(SECRET_KEY)
  const [ivHex, encrypted] = ciphertext.split(':')
  const iv = CryptoJS.enc.Hex.parse(ivHex)
  const decrypted = CryptoJS.AES.decrypt(encrypted, key, {
    iv: iv,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7
  })
  return decrypted.toString(CryptoJS.enc.Utf8)
}

4.1.2 RSA加密(用于敏感数据)

// utils/crypto.ts
import JSEncrypt from 'jsencrypt'

const PUBLIC_KEY = import.meta.env.VITE_RSA_PUBLIC_KEY

export function encryptWithRSA(text: string): string {
  const encrypt = new JSEncrypt()
  encrypt.setPublicKey(PUBLIC_KEY)
  return encrypt.encrypt(text) || ''
}

4.2 数据脱敏

4.2.1 手机号脱敏

// utils/mask.ts
export function maskPhone(phone: string): string {
  if (!phone || phone.length !== 11) {
    return phone
  }
  return phone.substring(0, 3) + '****' + phone.substring(7)
}

4.2.2 身份证号脱敏

// utils/mask.ts
export function maskIdCard(idCard: string): string {
  if (!idCard || idCard.length !== 18) {
    return idCard
  }
  return idCard.substring(0, 6) + '********' + idCard.substring(14)
}

4.2.3 银行卡号脱敏

// utils/mask.ts
export function maskBankCard(bankCard: string): string {
  if (!bankCard || bankCard.length < 16) {
    return bankCard
  }
  return bankCard.substring(0, 4) + ' **** **** ' + bankCard.substring(bankCard.length - 4)
}

4.3 敏感信息存储

4.3.1 安全存储

// utils/storage.ts
import { encrypt, decrypt } from './crypto'

export const secureStorage = {
  setItem(key: string, value: any): void {
    const encrypted = encrypt(JSON.stringify(value))
    localStorage.setItem(key, encrypted)
  },
  
  getItem<T>(key: string): T | null {
    const encrypted = localStorage.getItem(key)
    if (!encrypted) return null
    try {
      return JSON.parse(decrypt(encrypted)) as T
    } catch {
      return null
    }
  },
  
  removeItem(key: string): void {
    localStorage.removeItem(key)
  },
  
  clear(): void {
    localStorage.clear()
  }
}

4.3.2 会话存储

// utils/storage.ts
export const sessionStorage = {
  setItem(key: string, value: any): void {
    const encrypted = encrypt(JSON.stringify(value))
    window.sessionStorage.setItem(key, encrypted)
  },
  
  getItem<T>(key: string): T | null {
    const encrypted = window.sessionStorage.getItem(key)
    if (!encrypted) return null
    try {
      return JSON.parse(decrypt(encrypted)) as T
    } catch {
      return null
    }
  },
  
  removeItem(key: string): void {
    window.sessionStorage.removeItem(key)
  },
  
  clear(): void {
    window.sessionStorage.clear()
  }
}

五、身份认证与授权

5.1 认证安全

5.1.1 密码安全

// utils/password.ts
export function validatePassword(password: string): { valid: boolean; message?: string } {
  if (password.length < 8) {
    return { valid: false, message: '密码长度至少8位' }
  }
  
  if (!/[A-Z]/.test(password)) {
    return { valid: false, message: '密码必须包含大写字母' }
  }
  
  if (!/[a-z]/.test(password)) {
    return { valid: false, message: '密码必须包含小写字母' }
  }
  
  if (!/[0-9]/.test(password)) {
    return { valid: false, message: '密码必须包含数字' }
  }
  
  if (!/[!@#$%^&*]/.test(password)) {
    return { valid: false, message: '密码必须包含特殊字符' }
  }
  
  return { valid: true }
}

5.1.2 Token管理

// stores/auth.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useAuthStore = defineStore('auth', () => {
  const token = ref<string>('')
  const refreshToken = ref<string>('')
  const tokenExpireTime = ref<number>(0)

  const setToken = (newToken: string, expireIn: number) => {
    token.value = newToken
    tokenExpireTime.value = Date.now() + expireIn * 1000
    secureStorage.setItem('auth_token', newToken)
    secureStorage.setItem('token_expire_time', tokenExpireTime.value)
  }

  const setRefreshToken = (newRefreshToken: string) => {
    refreshToken.value = newRefreshToken
    secureStorage.setItem('refresh_token', newRefreshToken)
  }

  const isTokenExpired = (): boolean => {
    return Date.now() >= tokenExpireTime.value
  }

  const clearAuth = () => {
    token.value = ''
    refreshToken.value = ''
    tokenExpireTime.value = 0
    secureStorage.removeItem('auth_token')
    secureStorage.removeItem('refresh_token')
    secureStorage.removeItem('token_expire_time')
  }

  return {
    token,
    refreshToken,
    tokenExpireTime,
    setToken,
    setRefreshToken,
    isTokenExpired,
    clearAuth
  }
})

5.1.3 Token刷新

// api/request.ts
import { useAuthStore } from '@/stores/auth'

instance.interceptors.request.use(async (config) => {
  const authStore = useAuthStore()
  
  if (authStore.isTokenExpired()) {
    try {
      const response = await instance.post('/auth/refresh', {
        refreshToken: authStore.refreshToken
      })
      authStore.setToken(response.token, response.expireIn)
      authStore.setRefreshToken(response.refreshToken)
      config.headers.Authorization = `Bearer ${response.token}`
    } catch (error) {
      authStore.clearAuth()
      window.location.href = '/login'
      return Promise.reject(error)
    }
  } else {
    config.headers.Authorization = `Bearer ${authStore.token}`
  }
  
  return config
})

5.2 授权安全

5.2.1 权限验证

// utils/permission.ts
export interface Permission {
  resource: string
  action: string
}

export function hasPermission(userPermissions: string[], required: Permission): boolean {
  const permissionString = `${required.resource}:${required.action}`
  return userPermissions.includes(permissionString)
}

export function hasAnyPermission(userPermissions: string[], required: Permission[]): boolean {
  return required.some(p => hasPermission(userPermissions, p))
}

export function hasAllPermissions(userPermissions: string[], required: Permission[]): boolean {
  return required.every(p => hasPermission(userPermissions, p))
}

5.2.2 路由权限守卫

// router/guards/permission.ts
import { useAuthStore } from '@/stores/auth'
import { usePermissionStore } from '@/stores/permission'

router.beforeEach(async (to, from, next) => {
  const authStore = useAuthStore()
  const permissionStore = usePermissionStore()
  
  if (to.meta.requiresAuth && !authStore.token) {
    next('/login')
    return
  }
  
  if (to.meta.permission) {
    const required = to.meta.permission as Permission
    if (!hasPermission(permissionStore.permissions, required)) {
      next('/403')
      return
    }
  }
  
  next()
})

六、安全Headers

6.1 基础安全Headers

// utils/headers.ts
export const securityHeaders = {
  'X-Content-Type-Options': 'nosniff',
  'X-Frame-Options': 'DENY',
  'X-XSS-Protection': '1; mode=block',
  'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
  'Referrer-Policy': 'strict-origin-when-cross-origin',
  'Permissions-Policy': 'geolocation=(), microphone=(), camera=()'
}

6.2 动态设置Headers

// api/request.ts
instance.interceptors.request.use((config) => {
  Object.assign(config.headers, securityHeaders)
  return config
})

七、点击劫持防护

7.1 X-Frame-Options

<!-- index.html -->
<meta http-equiv="X-Frame-Options" content="DENY">

7.2 CSP frame-ancestors

<!-- index.html -->
<meta http-equiv="Content-Security-Policy" 
      content="frame-ancestors 'none';">

7.3 JavaScript防护

// utils/clickjacking.ts
export function preventClickjacking(): void {
  if (window.self !== window.top) {
    window.top.location = window.self.location
  }
}

// 在应用初始化时调用
preventClickjacking()

八、安全键盘

8.1 安全键盘组件

<!-- components/base/SecureKeyboard.vue -->
<template>
  <div class="secure-keyboard">
    <div class="keyboard-display">
      <span v-for="i in maskedValue.length" :key="i"></span>
    </div>
    <div class="keyboard-grid">
      <button
        v-for="key in keys"
        :key="key"
        @click="handleKeyPress(key)"
        class="key-button"
      >
        {{ key }}
      </button>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue'

const keys = ['1', '2', '3', '4', '5', '6', '7', '8', '9', 'C', '0', '⌫']

const value = ref<string>('')
const maxLength = 6

const maskedValue = computed(() => value.value)

const emit = defineEmits<{
  input: [value: string]
  complete: [value: string]
}>()

const handleKeyPress = (key: string) => {
  if (key === 'C') {
    value.value = ''
  } else if (key === '⌫') {
    value.value = value.value.slice(0, -1)
  } else if (value.value.length < maxLength) {
    value.value += key
  }
  
  emit('input', value.value)
  
  if (value.value.length === maxLength) {
    emit('complete', value.value)
  }
}
</script>

<style scoped>
.secure-keyboard {
  background: #f5f5f5;
  padding: 16px;
  border-radius: 8px;
}

.keyboard-display {
  display: flex;
  justify-content: center;
  gap: 8px;
  margin-bottom: 16px;
  font-size: 24px;
}

.keyboard-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
}

.key-button {
  padding: 16px;
  font-size: 20px;
  background: white;
  border: 1px solid #ddd;
  border-radius: 8px;
  cursor: pointer;
}

.key-button:active {
  background: #e0e0e0;
}
</style>

九、安全日志与监控

9.1 操作日志

// utils/logger.ts
interface LogEntry {
  timestamp: number
  level: 'info' | 'warn' | 'error'
  action: string
  userId?: number
  details?: any
}

export class SecurityLogger {
  private logs: LogEntry[] = []
  
  log(action: string, details?: any, level: 'info' | 'warn' | 'error' = 'info') {
    const entry: LogEntry = {
      timestamp: Date.now(),
      level,
      action,
      userId: this.getUserId(),
      details
    }
    
    this.logs.push(entry)
    this.sendToServer(entry)
  }
  
  private getUserId(): number | undefined {
    const authStore = useAuthStore()
    return authStore.user?.id
  }
  
  private async sendToServer(entry: LogEntry) {
    try {
      await api.post('/security/log', entry)
    } catch (error) {
      console.error('Failed to send security log:', error)
    }
  }
}

export const securityLogger = new SecurityLogger()

9.2 异常监控

// utils/sentry.ts
import * as Sentry from '@sentry/vue'

export function setupSentry(app: App) {
  Sentry.init({
    app,
    dsn: import.meta.env.VITE_SENTRY_DSN,
    environment: import.meta.env.MODE,
    tracesSampleRate: 1.0,
    beforeSend(event) {
      if (event.request) {
        delete event.request.cookies
        delete event.request.headers
      }
      return event
    }
  })
}

// 全局错误处理
window.addEventListener('error', (event) => {
  securityLogger.log('window.error', {
    message: event.message,
    filename: event.filename,
    lineno: event.lineno
  }, 'error')
})

window.addEventListener('unhandledrejection', (event) => {
  securityLogger.log('unhandledrejection', {
    reason: event.reason
  }, 'error')
})

十、安全最佳实践

10.1 开发阶段

  1. 代码审查

    • 所有代码必须经过安全审查
    • 使用ESLint安全规则
    • 使用npm audit检查依赖漏洞
  2. 依赖管理

    • 定期更新依赖包
    • 使用npm audit fix修复漏洞
    • 使用Snyk等工具监控依赖安全
  3. 环境变量

    • 敏感信息使用环境变量
    • 不要将密钥提交到代码仓库
    • 使用.env文件管理配置

10.2 测试阶段

  1. 安全测试

    • 使用OWASP ZAP进行安全扫描
    • 进行渗透测试
    • 测试XSS、CSRF等漏洞
  2. 代码扫描

    • 使用SonarQube进行代码质量检查
    • 使用ESLint进行代码规范检查
    • 使用Prettier进行代码格式化

10.3 部署阶段

  1. HTTPS强制

    • 使用SSL证书
    • 配置HSTS
    • 禁用HTTP访问
  2. 安全配置

    • 配置CSP策略
    • 配置安全Headers
    • 配置防火墙规则
  3. 监控告警

    • 配置错误监控
    • 配置性能监控
    • 配置安全告警

十一、合规性要求

11.1 GDPR合规

  1. 数据最小化

    • 只收集必要的用户数据
    • 提供数据删除功能
    • 提供数据导出功能
  2. 用户同意

    • 明确告知数据使用目的
    • 获取用户明确同意
    • 提供撤回同意的选项
  3. 数据保护

    • 加密存储敏感数据
    • 限制数据访问权限
    • 定期进行安全审计

11.2 无障碍合规

  1. WCAG 2.1 AA级标准

    • 键盘导航支持
    • 屏幕阅读器支持
    • 颜色对比度符合标准
  2. ARIA标签

    • 为交互元素添加ARIA标签
    • 为动态内容添加ARIA标签
    • 为表单元素添加ARIA标签

十二、安全检查清单

12.1 代码提交前检查

  • 所有用户输入都经过验证和过滤
  • 所有输出都经过转义
  • 敏感数据都经过加密存储
  • 敏感信息都经过脱敏显示
  • 所有API请求都包含CSRF Token
  • 所有页面都配置了CSP策略
  • 所有页面都配置了安全Headers
  • 所有密码都符合复杂度要求
  • 所有权限都经过验证
  • 所有操作都记录了日志

12.2 部署前检查

  • 所有依赖包都是最新版本
  • 所有依赖包都没有已知漏洞
  • 所有环境变量都正确配置
  • 所有HTTPS证书都有效
  • 所有监控和告警都正常工作
  • 所有安全策略都正确配置
  • 所有安全测试都通过
  • 所有安全扫描都通过
  • 所有安全文档都完整
  • 所有安全培训都完成

十三、总结

本文档详细描述了健身房管理系统前端的安全规范,包括:

  1. 安全概述:安全目标、安全原则、安全威胁
  2. XSS防护:输入验证、输出转义、CSP策略
  3. CSRF防护Token验证、SameSite Cookie、双重Cookie提交
  4. 数据安全:数据加密、数据脱敏、敏感信息存储
  5. 身份认证与授权:认证安全、授权安全
  6. 安全Headers:基础安全Headers、动态设置Headers
  7. 点击劫持防护X-Frame-Options、CSP frame-ancestors
  8. 安全键盘:安全键盘组件
  9. 安全日志与监控:操作日志、异常监控
  10. 安全最佳实践:开发阶段、测试阶段、部署阶段
  11. 合规性要求GDPR合规、无障碍合规
  12. 安全检查清单:代码提交前检查、部署前检查

通过遵循本文档的安全规范,可以确保健身房管理系统前端的安全性,符合金融级安全标准和监管要求。