959 lines
22 KiB
Markdown
959 lines
22 KiB
Markdown
# 健身房管理系统前端安全规范文档
|
||
|
||
> 文档编号: 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 白名单验证
|
||
|
||
```typescript
|
||
// 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 输入长度限制
|
||
|
||
```typescript
|
||
// 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转义
|
||
|
||
```typescript
|
||
// 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转义
|
||
|
||
```typescript
|
||
// 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配置
|
||
|
||
```html
|
||
<!-- 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配置
|
||
|
||
```typescript
|
||
// 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生成与存储
|
||
|
||
```typescript
|
||
// 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注入请求
|
||
|
||
```typescript
|
||
// 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
|
||
|
||
```typescript
|
||
// api/request.ts
|
||
instance.interceptors.request.use((config) => {
|
||
config.withCredentials = true
|
||
return config
|
||
})
|
||
```
|
||
|
||
### 3.3 双重Cookie提交
|
||
|
||
```typescript
|
||
// 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加密
|
||
|
||
```typescript
|
||
// 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加密(用于敏感数据)
|
||
|
||
```typescript
|
||
// 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 手机号脱敏
|
||
|
||
```typescript
|
||
// 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 身份证号脱敏
|
||
|
||
```typescript
|
||
// 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 银行卡号脱敏
|
||
|
||
```typescript
|
||
// 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 安全存储
|
||
|
||
```typescript
|
||
// 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 会话存储
|
||
|
||
```typescript
|
||
// 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 密码安全
|
||
|
||
```typescript
|
||
// 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管理
|
||
|
||
```typescript
|
||
// 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刷新
|
||
|
||
```typescript
|
||
// 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 权限验证
|
||
|
||
```typescript
|
||
// 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 路由权限守卫
|
||
|
||
```typescript
|
||
// 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
|
||
|
||
```typescript
|
||
// 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
|
||
|
||
```typescript
|
||
// api/request.ts
|
||
instance.interceptors.request.use((config) => {
|
||
Object.assign(config.headers, securityHeaders)
|
||
return config
|
||
})
|
||
```
|
||
|
||
---
|
||
|
||
## 七、点击劫持防护
|
||
|
||
### 7.1 X-Frame-Options
|
||
|
||
```html
|
||
<!-- index.html -->
|
||
<meta http-equiv="X-Frame-Options" content="DENY">
|
||
```
|
||
|
||
### 7.2 CSP frame-ancestors
|
||
|
||
```html
|
||
<!-- index.html -->
|
||
<meta http-equiv="Content-Security-Policy"
|
||
content="frame-ancestors 'none';">
|
||
```
|
||
|
||
### 7.3 JavaScript防护
|
||
|
||
```typescript
|
||
// utils/clickjacking.ts
|
||
export function preventClickjacking(): void {
|
||
if (window.self !== window.top) {
|
||
window.top.location = window.self.location
|
||
}
|
||
}
|
||
|
||
// 在应用初始化时调用
|
||
preventClickjacking()
|
||
```
|
||
|
||
---
|
||
|
||
## 八、安全键盘
|
||
|
||
### 8.1 安全键盘组件
|
||
|
||
```vue
|
||
<!-- 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 操作日志
|
||
|
||
```typescript
|
||
// 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 异常监控
|
||
|
||
```typescript
|
||
// 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. **安全检查清单**:代码提交前检查、部署前检查
|
||
|
||
通过遵循本文档的安全规范,可以确保健身房管理系统前端的安全性,符合金融级安全标准和监管要求。
|