22 KiB
22 KiB
健身房管理系统前端安全规范文档
文档编号: 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> = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
}
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
})
3.2 SameSite Cookie
// 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 开发阶段
-
代码审查
- 所有代码必须经过安全审查
- 使用ESLint安全规则
- 使用npm audit检查依赖漏洞
-
依赖管理
- 定期更新依赖包
- 使用npm audit fix修复漏洞
- 使用Snyk等工具监控依赖安全
-
环境变量
- 敏感信息使用环境变量
- 不要将密钥提交到代码仓库
- 使用.env文件管理配置
10.2 测试阶段
-
安全测试
- 使用OWASP ZAP进行安全扫描
- 进行渗透测试
- 测试XSS、CSRF等漏洞
-
代码扫描
- 使用SonarQube进行代码质量检查
- 使用ESLint进行代码规范检查
- 使用Prettier进行代码格式化
10.3 部署阶段
-
HTTPS强制
- 使用SSL证书
- 配置HSTS
- 禁用HTTP访问
-
安全配置
- 配置CSP策略
- 配置安全Headers
- 配置防火墙规则
-
监控告警
- 配置错误监控
- 配置性能监控
- 配置安全告警
十一、合规性要求
11.1 GDPR合规
-
数据最小化
- 只收集必要的用户数据
- 提供数据删除功能
- 提供数据导出功能
-
用户同意
- 明确告知数据使用目的
- 获取用户明确同意
- 提供撤回同意的选项
-
数据保护
- 加密存储敏感数据
- 限制数据访问权限
- 定期进行安全审计
11.2 无障碍合规
-
WCAG 2.1 AA级标准
- 键盘导航支持
- 屏幕阅读器支持
- 颜色对比度符合标准
-
ARIA标签
- 为交互元素添加ARIA标签
- 为动态内容添加ARIA标签
- 为表单元素添加ARIA标签
十二、安全检查清单
12.1 代码提交前检查
- 所有用户输入都经过验证和过滤
- 所有输出都经过转义
- 敏感数据都经过加密存储
- 敏感信息都经过脱敏显示
- 所有API请求都包含CSRF Token
- 所有页面都配置了CSP策略
- 所有页面都配置了安全Headers
- 所有密码都符合复杂度要求
- 所有权限都经过验证
- 所有操作都记录了日志
12.2 部署前检查
- 所有依赖包都是最新版本
- 所有依赖包都没有已知漏洞
- 所有环境变量都正确配置
- 所有HTTPS证书都有效
- 所有监控和告警都正常工作
- 所有安全策略都正确配置
- 所有安全测试都通过
- 所有安全扫描都通过
- 所有安全文档都完整
- 所有安全培训都完成
十三、总结
本文档详细描述了健身房管理系统前端的安全规范,包括:
- 安全概述:安全目标、安全原则、安全威胁
- XSS防护:输入验证、输出转义、CSP策略
- CSRF防护:Token验证、SameSite Cookie、双重Cookie提交
- 数据安全:数据加密、数据脱敏、敏感信息存储
- 身份认证与授权:认证安全、授权安全
- 安全Headers:基础安全Headers、动态设置Headers
- 点击劫持防护:X-Frame-Options、CSP frame-ancestors
- 安全键盘:安全键盘组件
- 安全日志与监控:操作日志、异常监控
- 安全最佳实践:开发阶段、测试阶段、部署阶段
- 合规性要求:GDPR合规、无障碍合规
- 安全检查清单:代码提交前检查、部署前检查
通过遵循本文档的安全规范,可以确保健身房管理系统前端的安全性,符合金融级安全标准和监管要求。