Skip to content

13.3 安全最佳实践

关键词: 安全最佳实践, 输入验证, 输出编码, 安全表单处理, 文件上传安全, 第三方集成, 安全开发, 防护策略

学习目标

  • 掌握输入验证的全面策略和实施方法
  • 学会正确的输出编码技术
  • 了解安全的表单处理最佳实践
  • 掌握文件上传的安全防护措施
  • 学会安全地集成第三方服务和组件

13.3.1 输入验证

全面的输入验证策略

html
<!-- 全面的输入验证实现 -->
<script>
// 高级输入验证器
class AdvancedInputValidator {
    constructor() {
        this.initializeValidationRules();
        this.setupValidationMethods();
    }
    
    // 初始化验证规则
    initializeValidationRules() {
        this.validationRules = {
            // 基本数据类型验证
            string: {
                minLength: 1,
                maxLength: 255,
                pattern: null,
                allowEmpty: false
            },
            number: {
                min: Number.MIN_SAFE_INTEGER,
                max: Number.MAX_SAFE_INTEGER,
                isInteger: false
            },
            email: {
                pattern: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
                maxLength: 254
            },
            url: {
                pattern: /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/,
                allowedProtocols: ['http', 'https']
            },
            phone: {
                pattern: /^1[3456789]\d{9}$/,
                format: 'mobile'
            },
            // 安全相关验证
            password: {
                minLength: 8,
                maxLength: 128,
                requireUppercase: true,
                requireLowercase: true,
                requireNumbers: true,
                requireSpecialChars: true,
                forbiddenPatterns: ['password', '123456', 'qwerty']
            },
            // 业务相关验证
            username: {
                minLength: 3,
                maxLength: 20,
                pattern: /^[a-zA-Z0-9_]{3,20}$/,
                forbiddenNames: ['admin', 'root', 'system']
            }
        };
    }
    
    // 设置验证方法
    setupValidationMethods() {
        this.validationMethods = {
            required: (value) => value !== null && value !== undefined && value !== '',
            length: (value, min, max) => value.length >= min && value.length <= max,
            pattern: (value, pattern) => pattern.test(value),
            range: (value, min, max) => value >= min && value <= max,
            whitelist: (value, allowedValues) => allowedValues.includes(value),
            blacklist: (value, forbiddenValues) => !forbiddenValues.includes(value)
        };
    }
    
    // 验证字符串
    validateString(value, fieldName, options = {}) {
        const rules = { ...this.validationRules.string, ...options };
        const errors = [];
        
        // 检查是否为空
        if (!rules.allowEmpty && !this.validationMethods.required(value)) {
            errors.push(`${fieldName}不能为空`);
            return { valid: false, errors };
        }
        
        if (value) {
            // 检查长度
            if (!this.validationMethods.length(value, rules.minLength, rules.maxLength)) {
                errors.push(`${fieldName}长度必须在${rules.minLength}-${rules.maxLength}之间`);
            }
            
            // 检查模式
            if (rules.pattern && !this.validationMethods.pattern(value, rules.pattern)) {
                errors.push(`${fieldName}格式不正确`);
            }
            
            // 安全检查
            if (this.containsMaliciousContent(value)) {
                errors.push(`${fieldName}包含不安全的内容`);
            }
        }
        
        return {
            valid: errors.length === 0,
            errors: errors,
            sanitized: this.sanitizeString(value)
        };
    }
    
    // 验证数字
    validateNumber(value, fieldName, options = {}) {
        const rules = { ...this.validationRules.number, ...options };
        const errors = [];
        
        // 类型检查
        const numValue = Number(value);
        if (isNaN(numValue)) {
            errors.push(`${fieldName}必须是有效的数字`);
            return { valid: false, errors };
        }
        
        // 整数检查
        if (rules.isInteger && !Number.isInteger(numValue)) {
            errors.push(`${fieldName}必须是整数`);
        }
        
        // 范围检查
        if (!this.validationMethods.range(numValue, rules.min, rules.max)) {
            errors.push(`${fieldName}必须在${rules.min}-${rules.max}之间`);
        }
        
        return {
            valid: errors.length === 0,
            errors: errors,
            sanitized: numValue
        };
    }
    
    // 验证邮箱
    validateEmail(value, fieldName, options = {}) {
        const rules = { ...this.validationRules.email, ...options };
        const errors = [];
        
        if (!this.validationMethods.required(value)) {
            errors.push(`${fieldName}不能为空`);
            return { valid: false, errors };
        }
        
        // 长度检查
        if (value.length > rules.maxLength) {
            errors.push(`${fieldName}长度不能超过${rules.maxLength}个字符`);
        }
        
        // 格式检查
        if (!this.validationMethods.pattern(value, rules.pattern)) {
            errors.push(`${fieldName}格式不正确`);
        }
        
        // 域名检查
        if (this.isDisposableEmail(value)) {
            errors.push(`${fieldName}不能使用临时邮箱`);
        }
        
        return {
            valid: errors.length === 0,
            errors: errors,
            sanitized: value.toLowerCase().trim()
        };
    }
    
    // 验证密码
    validatePassword(value, fieldName, options = {}) {
        const rules = { ...this.validationRules.password, ...options };
        const errors = [];
        
        if (!this.validationMethods.required(value)) {
            errors.push(`${fieldName}不能为空`);
            return { valid: false, errors };
        }
        
        // 长度检查
        if (!this.validationMethods.length(value, rules.minLength, rules.maxLength)) {
            errors.push(`${fieldName}长度必须在${rules.minLength}-${rules.maxLength}之间`);
        }
        
        // 强度检查
        if (rules.requireUppercase && !/[A-Z]/.test(value)) {
            errors.push(`${fieldName}必须包含大写字母`);
        }
        
        if (rules.requireLowercase && !/[a-z]/.test(value)) {
            errors.push(`${fieldName}必须包含小写字母`);
        }
        
        if (rules.requireNumbers && !/\d/.test(value)) {
            errors.push(`${fieldName}必须包含数字`);
        }
        
        if (rules.requireSpecialChars && !/[!@#$%^&*(),.?":{}|<>]/.test(value)) {
            errors.push(`${fieldName}必须包含特殊字符`);
        }
        
        // 弱密码检查
        const lowerValue = value.toLowerCase();
        for (const forbidden of rules.forbiddenPatterns) {
            if (lowerValue.includes(forbidden)) {
                errors.push(`${fieldName}不能包含常见的弱密码模式`);
                break;
            }
        }
        
        return {
            valid: errors.length === 0,
            errors: errors,
            strength: this.calculatePasswordStrength(value)
        };
    }
    
    // 验证文件
    validateFile(file, fieldName, options = {}) {
        const defaultOptions = {
            maxSize: 10 * 1024 * 1024, // 10MB
            allowedTypes: ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'],
            allowedExtensions: ['.jpg', '.jpeg', '.png', '.gif', '.pdf'],
            checkMagicNumbers: true
        };
        
        const rules = { ...defaultOptions, ...options };
        const errors = [];
        
        if (!file || !(file instanceof File)) {
            errors.push(`${fieldName}必须是有效的文件`);
            return { valid: false, errors };
        }
        
        // 大小检查
        if (file.size > rules.maxSize) {
            errors.push(`${fieldName}大小不能超过${this.formatFileSize(rules.maxSize)}`);
        }
        
        // 类型检查
        if (!rules.allowedTypes.includes(file.type)) {
            errors.push(`${fieldName}类型不被允许`);
        }
        
        // 扩展名检查
        const fileName = file.name.toLowerCase();
        const hasValidExtension = rules.allowedExtensions.some(ext => fileName.endsWith(ext));
        if (!hasValidExtension) {
            errors.push(`${fieldName}扩展名不被允许`);
        }
        
        // 文件名安全检查
        if (this.hasUnsafeFileName(file.name)) {
            errors.push(`${fieldName}文件名包含不安全的字符`);
        }
        
        return {
            valid: errors.length === 0,
            errors: errors,
            fileInfo: {
                name: file.name,
                size: file.size,
                type: file.type,
                lastModified: file.lastModified
            }
        };
    }
    
    // 检查恶意内容
    containsMaliciousContent(value) {
        const maliciousPatterns = [
            /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
            /javascript:/gi,
            /vbscript:/gi,
            /onload\s*=/gi,
            /onerror\s*=/gi,
            /onclick\s*=/gi,
            /onfocus\s*=/gi,
            /onblur\s*=/gi,
            /onchange\s*=/gi,
            /onsubmit\s*=/gi
        ];
        
        return maliciousPatterns.some(pattern => pattern.test(value));
    }
    
    // 字符串清理
    sanitizeString(value) {
        if (!value || typeof value !== 'string') {
            return '';
        }
        
        return value
            .trim()
            .replace(/\s+/g, ' ') // 合并多个空格
            .replace(/[<>]/g, '') // 移除尖括号
            .substring(0, 1000); // 限制长度
    }
    
    // 检查临时邮箱
    isDisposableEmail(email) {
        const disposableDomains = [
            '10minutemail.com',
            'tempmail.org',
            'guerrillamail.com',
            'mailinator.com',
            'throwaway.email'
        ];
        
        const domain = email.split('@')[1]?.toLowerCase();
        return disposableDomains.includes(domain);
    }
    
    // 计算密码强度
    calculatePasswordStrength(password) {
        let score = 0;
        
        // 长度分数
        if (password.length >= 8) score += 1;
        if (password.length >= 12) score += 1;
        if (password.length >= 16) score += 1;
        
        // 字符类型分数
        if (/[a-z]/.test(password)) score += 1;
        if (/[A-Z]/.test(password)) score += 1;
        if (/\d/.test(password)) score += 1;
        if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) score += 1;
        
        // 复杂度分数
        if (!/(.)\1{2,}/.test(password)) score += 1; // 无重复字符
        if (!/012|123|234|345|456|567|678|789|890/.test(password)) score += 1; // 无连续数字
        
        const strength = ['很弱', '弱', '一般', '强', '很强'];
        return {
            score: score,
            level: strength[Math.min(Math.floor(score / 2), 4)]
        };
    }
    
    // 格式化文件大小
    formatFileSize(bytes) {
        const sizes = ['B', 'KB', 'MB', 'GB'];
        if (bytes === 0) return '0 B';
        const i = Math.floor(Math.log(bytes) / Math.log(1024));
        return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
    }
    
    // 检查不安全的文件名
    hasUnsafeFileName(fileName) {
        const unsafePatterns = [
            /\.\./g, // 路径遍历
            /[<>:"|?*]/g, // 不安全字符
            /^(con|prn|aux|nul|com[0-9]|lpt[0-9])$/i, // Windows保留名
            /^\./g, // 隐藏文件
            /\s$/g // 末尾空格
        ];
        
        return unsafePatterns.some(pattern => pattern.test(fileName));
    }
}

// 使用示例
const validator = new AdvancedInputValidator();

// 验证表单数据
function validateForm(formData) {
    const results = {};
    
    // 验证用户名
    results.username = validator.validateString(
        formData.username,
        '用户名',
        { minLength: 3, maxLength: 20, pattern: /^[a-zA-Z0-9_]+$/ }
    );
    
    // 验证邮箱
    results.email = validator.validateEmail(formData.email, '邮箱');
    
    // 验证密码
    results.password = validator.validatePassword(formData.password, '密码');
    
    // 验证年龄
    results.age = validator.validateNumber(
        formData.age,
        '年龄',
        { min: 1, max: 150, isInteger: true }
    );
    
    // 验证文件
    if (formData.avatar) {
        results.avatar = validator.validateFile(
            formData.avatar,
            '头像',
            { maxSize: 5 * 1024 * 1024, allowedTypes: ['image/jpeg', 'image/png'] }
        );
    }
    
    return results;
}
</script>

13.3.2 输出编码

HTML输出编码

html
<!-- 安全的HTML输出编码 -->
<script>
// HTML编码器
class HTMLEncoder {
    constructor() {
        this.entityMap = {
            '&': '&amp;',
            '<': '&lt;',
            '>': '&gt;',
            '"': '&quot;',
            "'": '&#39;',
            '/': '&#x2F;',
            '`': '&#x60;',
            '=': '&#x3D;'
        };
        
        this.contextEncoders = {
            html: this.encodeHTML.bind(this),
            attribute: this.encodeAttribute.bind(this),
            javascript: this.encodeJavaScript.bind(this),
            css: this.encodeCSS.bind(this),
            url: this.encodeURL.bind(this)
        };
    }
    
    // HTML内容编码
    encodeHTML(text) {
        if (!text || typeof text !== 'string') {
            return '';
        }
        
        return text.replace(/[&<>"'\/`=]/g, (match) => {
            return this.entityMap[match];
        });
    }
    
    // HTML属性编码
    encodeAttribute(text) {
        if (!text || typeof text !== 'string') {
            return '';
        }
        
        // 属性值应该总是被引号包围
        return text.replace(/[&<>"'\/`=]/g, (match) => {
            return this.entityMap[match];
        });
    }
    
    // JavaScript编码
    encodeJavaScript(text) {
        if (!text || typeof text !== 'string') {
            return '';
        }
        
        return text.replace(/[\\"\u0000-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, (match) => {
            return '\\u' + ('0000' + match.charCodeAt(0).toString(16)).slice(-4);
        });
    }
    
    // CSS编码
    encodeCSS(text) {
        if (!text || typeof text !== 'string') {
            return '';
        }
        
        return text.replace(/[^a-zA-Z0-9]/g, (match) => {
            return '\\' + match.charCodeAt(0).toString(16) + ' ';
        });
    }
    
    // URL编码
    encodeURL(text) {
        if (!text || typeof text !== 'string') {
            return '';
        }
        
        return encodeURIComponent(text);
    }
    
    // 安全的模板渲染
    renderTemplate(template, data, context = 'html') {
        const encoder = this.contextEncoders[context];
        if (!encoder) {
            throw new Error(`不支持的编码上下文: ${context}`);
        }
        
        return template.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
            const value = data[key.trim()];
            return encoder(value);
        });
    }
    
    // 安全的DOM操作
    safeSetTextContent(element, text) {
        if (!element || !element.textContent !== undefined) {
            console.warn('无效的DOM元素');
            return;
        }
        
        element.textContent = text || '';
    }
    
    safeSetHTML(element, html) {
        if (!element || !element.innerHTML !== undefined) {
            console.warn('无效的DOM元素');
            return;
        }
        
        // 先清理HTML
        const cleanHTML = this.sanitizeHTML(html);
        element.innerHTML = cleanHTML;
    }
    
    // HTML清理
    sanitizeHTML(html) {
        if (!html || typeof html !== 'string') {
            return '';
        }
        
        // 创建临时DOM元素
        const tempDiv = document.createElement('div');
        tempDiv.innerHTML = html;
        
        // 移除脚本标签
        const scripts = tempDiv.querySelectorAll('script');
        scripts.forEach(script => script.remove());
        
        // 移除危险属性
        const allElements = tempDiv.querySelectorAll('*');
        allElements.forEach(element => {
            // 移除事件处理程序
            const attributes = [...element.attributes];
            attributes.forEach(attr => {
                if (attr.name.startsWith('on')) {
                    element.removeAttribute(attr.name);
                }
            });
            
            // 移除javascript:协议
            if (element.href && element.href.startsWith('javascript:')) {
                element.href = '#';
            }
            
            if (element.src && element.src.startsWith('javascript:')) {
                element.removeAttribute('src');
            }
        });
        
        return tempDiv.innerHTML;
    }
    
    // 安全的JSON输出
    safeJSONStringify(obj) {
        return JSON.stringify(obj, (key, value) => {
            if (typeof value === 'string') {
                return this.encodeHTML(value);
            }
            return value;
        });
    }
}

// 使用示例
const encoder = new HTMLEncoder();

// 不同上下文的编码
const userData = {
    name: '<script>alert("XSS")</script>',
    email: 'user@example.com',
    message: 'Hello "World" & <everyone>!'
};

// HTML内容编码
const htmlContent = encoder.renderTemplate(
    '<h1>Hello {{name}}</h1><p>{{message}}</p>',
    userData,
    'html'
);

// 属性编码
const attributeValue = encoder.renderTemplate(
    '<input type="text" value="{{name}}" title="{{message}}">',
    userData,
    'attribute'
);

// JavaScript编码
const jsCode = encoder.renderTemplate(
    'var userName = "{{name}}"; console.log(userName);',
    userData,
    'javascript'
);

console.log('HTML编码:', htmlContent);
console.log('属性编码:', attributeValue);
console.log('JavaScript编码:', jsCode);
</script>

13.3.3 安全的表单处理

表单安全增强

html
<!-- 安全的表单处理 -->
<script>
// 安全表单管理器
class SecureFormManager {
    constructor() {
        this.forms = new Map();
        this.tokenGenerator = new TokenGenerator();
        this.rateLimiter = new RateLimiter();
        this.init();
    }
    
    init() {
        // 监听表单提交
        document.addEventListener('submit', (event) => {
            if (event.target.tagName === 'FORM') {
                this.handleFormSubmit(event);
            }
        });
        
        // 监听表单重置
        document.addEventListener('reset', (event) => {
            if (event.target.tagName === 'FORM') {
                this.handleFormReset(event);
            }
        });
    }
    
    // 注册安全表单
    registerForm(formId, options = {}) {
        const form = document.getElementById(formId);
        if (!form) {
            throw new Error(`表单 ${formId} 未找到`);
        }
        
        const defaultOptions = {
            enableCSRF: true,
            enableRateLimit: true,
            maxSubmissions: 5,
            timeWindow: 60000, // 1分钟
            validateOnSubmit: true,
            encryptSensitiveData: false
        };
        
        const formOptions = { ...defaultOptions, ...options };
        
        // 添加CSRF令牌
        if (formOptions.enableCSRF) {
            this.addCSRFToken(form);
        }
        
        // 添加表单验证
        if (formOptions.validateOnSubmit) {
            this.addFormValidation(form);
        }
        
        // 注册表单
        this.forms.set(formId, {
            form: form,
            options: formOptions,
            submissionCount: 0,
            lastSubmission: 0
        });
        
        return formId;
    }
    
    // 添加CSRF令牌
    addCSRFToken(form) {
        // 移除旧的CSRF令牌
        const oldToken = form.querySelector('input[name="csrf_token"]');
        if (oldToken) {
            oldToken.remove();
        }
        
        // 生成新的CSRF令牌
        const token = this.tokenGenerator.generateCSRFToken();
        const tokenInput = document.createElement('input');
        tokenInput.type = 'hidden';
        tokenInput.name = 'csrf_token';
        tokenInput.value = token;
        
        form.appendChild(tokenInput);
        
        // 存储令牌到sessionStorage
        sessionStorage.setItem('csrf_token', token);
    }
    
    // 添加表单验证
    addFormValidation(form) {
        const inputs = form.querySelectorAll('input, textarea, select');
        
        inputs.forEach(input => {
            // 添加实时验证
            input.addEventListener('blur', (event) => {
                this.validateField(event.target);
            });
            
            // 添加输入过滤
            input.addEventListener('input', (event) => {
                this.filterInput(event.target);
            });
        });
    }
    
    // 字段验证
    validateField(field) {
        const validator = new AdvancedInputValidator();
        let result = { valid: true, errors: [] };
        
        switch (field.type) {
            case 'email':
                result = validator.validateEmail(field.value, field.name);
                break;
            case 'password':
                result = validator.validatePassword(field.value, field.name);
                break;
            case 'tel':
                result = validator.validateString(field.value, field.name, {
                    pattern: /^1[3456789]\d{9}$/
                });
                break;
            case 'url':
                result = validator.validateURL(field.value, field.name);
                break;
            default:
                if (field.required) {
                    result = validator.validateString(field.value, field.name, {
                        allowEmpty: false
                    });
                }
        }
        
        this.showValidationResult(field, result);
        return result.valid;
    }
    
    // 显示验证结果
    showValidationResult(field, result) {
        // 移除旧的错误消息
        const oldError = field.parentNode.querySelector('.validation-error');
        if (oldError) {
            oldError.remove();
        }
        
        // 更新字段样式
        field.classList.remove('valid', 'invalid');
        
        if (result.valid) {
            field.classList.add('valid');
        } else {
            field.classList.add('invalid');
            
            // 显示错误消息
            const errorDiv = document.createElement('div');
            errorDiv.className = 'validation-error';
            errorDiv.textContent = result.errors.join(', ');
            field.parentNode.appendChild(errorDiv);
        }
    }
    
    // 输入过滤
    filterInput(field) {
        let value = field.value;
        
        // 基本过滤
        switch (field.type) {
            case 'email':
                // 移除不允许的字符
                value = value.replace(/[^a-zA-Z0-9@._-]/g, '');
                break;
            case 'tel':
                // 只允许数字
                value = value.replace(/[^\d]/g, '');
                break;
            case 'url':
                // 基本URL字符
                value = value.replace(/[^a-zA-Z0-9:\/\.\-_?=&%]/g, '');
                break;
            default:
                // 移除潜在的恶意字符
                value = value.replace(/[<>]/g, '');
        }
        
        // 长度限制
        if (field.maxLength && value.length > field.maxLength) {
            value = value.substring(0, field.maxLength);
        }
        
        field.value = value;
    }
    
    // 处理表单提交
    handleFormSubmit(event) {
        const form = event.target;
        const formId = form.id;
        const formData = this.forms.get(formId);
        
        if (!formData) {
            // 未注册的表单,阻止提交
            event.preventDefault();
            console.warn('未注册的表单提交被阻止');
            return;
        }
        
        // 验证CSRF令牌
        if (formData.options.enableCSRF && !this.validateCSRFToken(form)) {
            event.preventDefault();
            this.showError(form, '安全验证失败,请刷新页面重试');
            return;
        }
        
        // 速率限制检查
        if (formData.options.enableRateLimit && !this.checkRateLimit(formId)) {
            event.preventDefault();
            this.showError(form, '提交过于频繁,请稍后重试');
            return;
        }
        
        // 表单验证
        if (formData.options.validateOnSubmit && !this.validateForm(form)) {
            event.preventDefault();
            this.showError(form, '请检查表单输入');
            return;
        }
        
        // 数据加密
        if (formData.options.encryptSensitiveData) {
            this.encryptSensitiveFields(form);
        }
        
        // 更新提交计数
        this.updateSubmissionCount(formId);
        
        // 显示提交状态
        this.showSubmissionStatus(form);
    }
    
    // 验证CSRF令牌
    validateCSRFToken(form) {
        const formToken = form.querySelector('input[name="csrf_token"]')?.value;
        const sessionToken = sessionStorage.getItem('csrf_token');
        
        return formToken && sessionToken && formToken === sessionToken;
    }
    
    // 检查速率限制
    checkRateLimit(formId) {
        const formData = this.forms.get(formId);
        const now = Date.now();
        
        // 检查时间窗口
        if (now - formData.lastSubmission < formData.options.timeWindow) {
            if (formData.submissionCount >= formData.options.maxSubmissions) {
                return false;
            }
        } else {
            // 重置计数
            formData.submissionCount = 0;
        }
        
        return true;
    }
    
    // 验证整个表单
    validateForm(form) {
        const fields = form.querySelectorAll('input, textarea, select');
        let isValid = true;
        
        fields.forEach(field => {
            if (!this.validateField(field)) {
                isValid = false;
            }
        });
        
        return isValid;
    }
    
    // 加密敏感字段
    encryptSensitiveFields(form) {
        const sensitiveFields = form.querySelectorAll('input[type="password"], input[data-encrypt="true"]');
        
        sensitiveFields.forEach(field => {
            if (field.value) {
                // 简单加密(实际应用中应使用更强的加密)
                field.value = btoa(field.value);
            }
        });
    }
    
    // 更新提交计数
    updateSubmissionCount(formId) {
        const formData = this.forms.get(formId);
        formData.submissionCount++;
        formData.lastSubmission = Date.now();
    }
    
    // 显示错误消息
    showError(form, message) {
        let errorDiv = form.querySelector('.form-error');
        if (!errorDiv) {
            errorDiv = document.createElement('div');
            errorDiv.className = 'form-error';
            form.insertBefore(errorDiv, form.firstChild);
        }
        
        errorDiv.textContent = message;
        errorDiv.style.display = 'block';
        
        // 3秒后自动隐藏
        setTimeout(() => {
            errorDiv.style.display = 'none';
        }, 3000);
    }
    
    // 显示提交状态
    showSubmissionStatus(form) {
        const submitButton = form.querySelector('button[type="submit"], input[type="submit"]');
        if (submitButton) {
            submitButton.disabled = true;
            submitButton.textContent = '提交中...';
            
            // 模拟提交完成
            setTimeout(() => {
                submitButton.disabled = false;
                submitButton.textContent = '提交';
            }, 2000);
        }
    }
    
    // 处理表单重置
    handleFormReset(event) {
        const form = event.target;
        const formId = form.id;
        const formData = this.forms.get(formId);
        
        if (formData) {
            // 重新生成CSRF令牌
            if (formData.options.enableCSRF) {
                this.addCSRFToken(form);
            }
            
            // 清除验证错误
            const errors = form.querySelectorAll('.validation-error');
            errors.forEach(error => error.remove());
            
            // 清除字段样式
            const fields = form.querySelectorAll('input, textarea, select');
            fields.forEach(field => {
                field.classList.remove('valid', 'invalid');
            });
        }
    }
}

// 令牌生成器
class TokenGenerator {
    generateCSRFToken() {
        const array = new Uint8Array(32);
        crypto.getRandomValues(array);
        return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
    }
}

// 速率限制器
class RateLimiter {
    constructor() {
        this.attempts = new Map();
    }
    
    isAllowed(identifier, maxAttempts = 5, windowMs = 60000) {
        const now = Date.now();
        const windowStart = now - windowMs;
        
        if (!this.attempts.has(identifier)) {
            this.attempts.set(identifier, []);
        }
        
        const userAttempts = this.attempts.get(identifier);
        
        // 移除过期的尝试
        const validAttempts = userAttempts.filter(timestamp => timestamp > windowStart);
        
        if (validAttempts.length >= maxAttempts) {
            return false;
        }
        
        // 记录当前尝试
        validAttempts.push(now);
        this.attempts.set(identifier, validAttempts);
        
        return true;
    }
}

// 使用示例
const formManager = new SecureFormManager();

// 注册表单
formManager.registerForm('loginForm', {
    enableCSRF: true,
    enableRateLimit: true,
    maxSubmissions: 3,
    timeWindow: 60000,
    validateOnSubmit: true
});

formManager.registerForm('profileForm', {
    enableCSRF: true,
    encryptSensitiveData: true,
    validateOnSubmit: true
});
</script>

13.3.4 安全的文件上传

文件上传安全处理

html
<!-- 安全的文件上传 -->
<script>
// 安全文件上传管理器
class SecureFileUploadManager {
    constructor() {
        this.allowedTypes = {
            image: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
            document: ['application/pdf', 'text/plain', 'application/msword'],
            archive: ['application/zip', 'application/x-rar-compressed']
        };
        
        this.maxFileSizes = {
            image: 5 * 1024 * 1024, // 5MB
            document: 10 * 1024 * 1024, // 10MB
            archive: 50 * 1024 * 1024 // 50MB
        };
        
        this.uploadQueue = [];
        this.uploadProgress = new Map();
        
        this.init();
    }
    
    init() {
        // 设置文件拖拽处理
        this.setupDragAndDrop();
        
        // 设置文件选择处理
        this.setupFileInput();
    }
    
    // 设置拖拽上传
    setupDragAndDrop() {
        const dropZones = document.querySelectorAll('.drop-zone');
        
        dropZones.forEach(zone => {
            zone.addEventListener('dragover', (event) => {
                event.preventDefault();
                zone.classList.add('drag-over');
            });
            
            zone.addEventListener('dragleave', (event) => {
                event.preventDefault();
                zone.classList.remove('drag-over');
            });
            
            zone.addEventListener('drop', (event) => {
                event.preventDefault();
                zone.classList.remove('drag-over');
                
                const files = Array.from(event.dataTransfer.files);
                this.handleFiles(files, zone);
            });
        });
    }
    
    // 设置文件输入处理
    setupFileInput() {
        const fileInputs = document.querySelectorAll('input[type="file"]');
        
        fileInputs.forEach(input => {
            input.addEventListener('change', (event) => {
                const files = Array.from(event.target.files);
                this.handleFiles(files, input);
            });
        });
    }
    
    // 处理文件
    handleFiles(files, element) {
        const uploadType = element.dataset.uploadType || 'image';
        
        files.forEach(file => {
            const validation = this.validateFile(file, uploadType);
            
            if (validation.valid) {
                this.queueFile(file, uploadType, element);
            } else {
                this.showError(element, validation.errors.join(', '));
            }
        });
        
        // 开始上传队列
        this.processUploadQueue();
    }
    
    // 验证文件
    validateFile(file, uploadType) {
        const errors = [];
        
        // 检查文件是否存在
        if (!file || !(file instanceof File)) {
            errors.push('无效的文件');
            return { valid: false, errors };
        }
        
        // 检查文件类型
        const allowedTypes = this.allowedTypes[uploadType] || this.allowedTypes.image;
        if (!allowedTypes.includes(file.type)) {
            errors.push('文件类型不被允许');
        }
        
        // 检查文件大小
        const maxSize = this.maxFileSizes[uploadType] || this.maxFileSizes.image;
        if (file.size > maxSize) {
            errors.push(`文件大小超过限制 (${this.formatFileSize(maxSize)})`);
        }
        
        // 检查文件名
        if (this.hasUnsafeFileName(file.name)) {
            errors.push('文件名包含不安全的字符');
        }
        
        // 检查文件扩展名
        if (!this.hasValidExtension(file.name, uploadType)) {
            errors.push('文件扩展名不被允许');
        }
        
        // 检查文件魔数(如果是图片)
        if (uploadType === 'image') {
            return this.validateImageFile(file, errors);
        }
        
        return {
            valid: errors.length === 0,
            errors: errors
        };
    }
    
    // 验证图片文件
    async validateImageFile(file, errors) {
        return new Promise((resolve) => {
            const reader = new FileReader();
            
            reader.onload = (event) => {
                const arrayBuffer = event.target.result;
                const bytes = new Uint8Array(arrayBuffer);
                
                // 检查图片魔数
                if (!this.isValidImageMagicNumber(bytes, file.type)) {
                    errors.push('文件类型与实际内容不符');
                }
                
                // 检查图片尺寸
                this.checkImageDimensions(file).then((dimensions) => {
                    if (dimensions.width > 5000 || dimensions.height > 5000) {
                        errors.push('图片尺寸过大');
                    }
                    
                    resolve({
                        valid: errors.length === 0,
                        errors: errors,
                        dimensions: dimensions
                    });
                });
            };
            
            reader.onerror = () => {
                errors.push('文件读取失败');
                resolve({ valid: false, errors });
            };
            
            reader.readAsArrayBuffer(file.slice(0, 1024)); // 只读取前1024字节
        });
    }
    
    // 检查图片魔数
    isValidImageMagicNumber(bytes, mimeType) {
        const magicNumbers = {
            'image/jpeg': [0xFF, 0xD8, 0xFF],
            'image/png': [0x89, 0x50, 0x4E, 0x47],
            'image/gif': [0x47, 0x49, 0x46, 0x38],
            'image/webp': [0x52, 0x49, 0x46, 0x46]
        };
        
        const expectedMagic = magicNumbers[mimeType];
        if (!expectedMagic) {
            return false;
        }
        
        for (let i = 0; i < expectedMagic.length; i++) {
            if (bytes[i] !== expectedMagic[i]) {
                return false;
            }
        }
        
        return true;
    }
    
    // 检查图片尺寸
    checkImageDimensions(file) {
        return new Promise((resolve) => {
            const img = new Image();
            
            img.onload = () => {
                resolve({
                    width: img.width,
                    height: img.height
                });
            };
            
            img.onerror = () => {
                resolve({ width: 0, height: 0 });
            };
            
            img.src = URL.createObjectURL(file);
        });
    }
    
    // 检查文件名安全性
    hasUnsafeFileName(fileName) {
        const unsafePatterns = [
            /\.\./g, // 路径遍历
            /[<>:"|?*]/g, // 不安全字符
            /^(con|prn|aux|nul|com[0-9]|lpt[0-9])$/i, // Windows保留名
            /^\./g, // 隐藏文件
            /\s$/g, // 末尾空格
            /\0/g // null字符
        ];
        
        return unsafePatterns.some(pattern => pattern.test(fileName));
    }
    
    // 检查文件扩展名
    hasValidExtension(fileName, uploadType) {
        const validExtensions = {
            image: ['.jpg', '.jpeg', '.png', '.gif', '.webp'],
            document: ['.pdf', '.txt', '.doc', '.docx'],
            archive: ['.zip', '.rar']
        };
        
        const allowed = validExtensions[uploadType] || validExtensions.image;
        const extension = fileName.toLowerCase().split('.').pop();
        
        return allowed.includes('.' + extension);
    }
    
    // 加入上传队列
    queueFile(file, uploadType, element) {
        const fileId = this.generateFileId();
        const uploadItem = {
            id: fileId,
            file: file,
            type: uploadType,
            element: element,
            status: 'queued',
            progress: 0,
            error: null
        };
        
        this.uploadQueue.push(uploadItem);
        this.uploadProgress.set(fileId, uploadItem);
        
        // 显示上传项
        this.showUploadItem(uploadItem);
    }
    
    // 生成文件ID
    generateFileId() {
        return Date.now().toString(36) + Math.random().toString(36).substr(2);
    }
    
    // 显示上传项
    showUploadItem(uploadItem) {
        const container = document.getElementById('upload-progress') || this.createUploadContainer();
        
        const itemDiv = document.createElement('div');
        itemDiv.className = 'upload-item';
        itemDiv.id = `upload-${uploadItem.id}`;
        
        itemDiv.innerHTML = `
            <div class="upload-info">
                <span class="file-name">${uploadItem.file.name}</span>
                <span class="file-size">${this.formatFileSize(uploadItem.file.size)}</span>
            </div>
            <div class="upload-progress">
                <div class="progress-bar">
                    <div class="progress-fill" style="width: 0%"></div>
                </div>
                <span class="progress-text">0%</span>
            </div>
            <div class="upload-actions">
                <button class="cancel-btn" onclick="uploadManager.cancelUpload('${uploadItem.id}')">取消</button>
            </div>
        `;
        
        container.appendChild(itemDiv);
    }
    
    // 创建上传容器
    createUploadContainer() {
        const container = document.createElement('div');
        container.id = 'upload-progress';
        container.className = 'upload-progress-container';
        document.body.appendChild(container);
        return container;
    }
    
    // 处理上传队列
    async processUploadQueue() {
        const maxConcurrentUploads = 3;
        const uploading = [];
        
        while (this.uploadQueue.length > 0 || uploading.length > 0) {
            // 启动新的上传
            while (uploading.length < maxConcurrentUploads && this.uploadQueue.length > 0) {
                const uploadItem = this.uploadQueue.shift();
                const uploadPromise = this.uploadFile(uploadItem);
                uploading.push(uploadPromise);
            }
            
            // 等待一个上传完成
            if (uploading.length > 0) {
                await Promise.race(uploading);
                // 移除已完成的上传
                uploading.splice(0, uploading.length);
            }
        }
    }
    
    // 上传文件
    async uploadFile(uploadItem) {
        try {
            uploadItem.status = 'uploading';
            
            // 创建FormData
            const formData = new FormData();
            formData.append('file', uploadItem.file);
            formData.append('type', uploadItem.type);
            formData.append('fileId', uploadItem.id);
            
            // 添加CSRF令牌
            const csrfToken = sessionStorage.getItem('csrf_token');
            if (csrfToken) {
                formData.append('csrf_token', csrfToken);
            }
            
            // 上传请求
            const response = await fetch('/api/upload', {
                method: 'POST',
                body: formData,
                credentials: 'same-origin'
            });
            
            if (!response.ok) {
                throw new Error(`上传失败: ${response.status}`);
            }
            
            const result = await response.json();
            
            uploadItem.status = 'completed';
            uploadItem.result = result;
            
            this.updateUploadProgress(uploadItem, 100);
            this.showUploadSuccess(uploadItem);
            
        } catch (error) {
            uploadItem.status = 'error';
            uploadItem.error = error.message;
            
            this.showUploadError(uploadItem);
        }
    }
    
    // 更新上传进度
    updateUploadProgress(uploadItem, progress) {
        const itemElement = document.getElementById(`upload-${uploadItem.id}`);
        if (itemElement) {
            const progressFill = itemElement.querySelector('.progress-fill');
            const progressText = itemElement.querySelector('.progress-text');
            
            if (progressFill) {
                progressFill.style.width = `${progress}%`;
            }
            
            if (progressText) {
                progressText.textContent = `${progress}%`;
            }
        }
    }
    
    // 显示上传成功
    showUploadSuccess(uploadItem) {
        const itemElement = document.getElementById(`upload-${uploadItem.id}`);
        if (itemElement) {
            itemElement.classList.add('upload-success');
            
            const actionsDiv = itemElement.querySelector('.upload-actions');
            if (actionsDiv) {
                actionsDiv.innerHTML = '<span class="success-message">上传成功</span>';
            }
        }
    }
    
    // 显示上传错误
    showUploadError(uploadItem) {
        const itemElement = document.getElementById(`upload-${uploadItem.id}`);
        if (itemElement) {
            itemElement.classList.add('upload-error');
            
            const actionsDiv = itemElement.querySelector('.upload-actions');
            if (actionsDiv) {
                actionsDiv.innerHTML = `
                    <span class="error-message">${uploadItem.error}</span>
                    <button onclick="uploadManager.retryUpload('${uploadItem.id}')">重试</button>
                `;
            }
        }
    }
    
    // 取消上传
    cancelUpload(fileId) {
        const uploadItem = this.uploadProgress.get(fileId);
        if (uploadItem) {
            uploadItem.status = 'cancelled';
            
            // 从队列中移除
            const queueIndex = this.uploadQueue.findIndex(item => item.id === fileId);
            if (queueIndex !== -1) {
                this.uploadQueue.splice(queueIndex, 1);
            }
            
            // 移除UI元素
            const itemElement = document.getElementById(`upload-${fileId}`);
            if (itemElement) {
                itemElement.remove();
            }
            
            this.uploadProgress.delete(fileId);
        }
    }
    
    // 重试上传
    retryUpload(fileId) {
        const uploadItem = this.uploadProgress.get(fileId);
        if (uploadItem) {
            uploadItem.status = 'queued';
            uploadItem.error = null;
            
            this.uploadQueue.push(uploadItem);
            this.processUploadQueue();
        }
    }
    
    // 格式化文件大小
    formatFileSize(bytes) {
        const sizes = ['B', 'KB', 'MB', 'GB'];
        if (bytes === 0) return '0 B';
        const i = Math.floor(Math.log(bytes) / Math.log(1024));
        return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
    }
    
    // 显示错误消息
    showError(element, message) {
        const errorDiv = document.createElement('div');
        errorDiv.className = 'upload-error-message';
        errorDiv.textContent = message;
        
        element.parentNode.insertBefore(errorDiv, element.nextSibling);
        
        setTimeout(() => {
            errorDiv.remove();
        }, 5000);
    }
}

// 初始化文件上传管理器
const uploadManager = new SecureFileUploadManager();
</script>

<!-- 上传界面HTML -->
<div class="upload-container">
    <div class="drop-zone" data-upload-type="image">
        <p>拖拽图片文件到此处或点击选择</p>
        <input type="file" accept="image/*" multiple>
    </div>
    
    <div class="drop-zone" data-upload-type="document">
        <p>拖拽文档文件到此处或点击选择</p>
        <input type="file" accept=".pdf,.txt,.doc,.docx" multiple>
    </div>
</div>

<style>
.upload-container {
    max-width: 800px;
    margin: 20px auto;
    padding: 20px;
}

.drop-zone {
    border: 2px dashed #ccc;
    border-radius: 8px;
    padding: 40px;
    text-align: center;
    margin-bottom: 20px;
    cursor: pointer;
    transition: border-color 0.3s;
}

.drop-zone:hover,
.drop-zone.drag-over {
    border-color: #007cba;
    background-color: #f0f8ff;
}

.upload-progress-container {
    margin-top: 20px;
}

.upload-item {
    border: 1px solid #ddd;
    border-radius: 4px;
    padding: 15px;
    margin-bottom: 10px;
    background: #fff;
}

.upload-item.upload-success {
    border-color: #4caf50;
    background-color: #f8fff8;
}

.upload-item.upload-error {
    border-color: #f44336;
    background-color: #fff8f8;
}

.upload-info {
    display: flex;
    justify-content: space-between;
    margin-bottom: 10px;
}

.progress-bar {
    width: 100%;
    height: 20px;
    background-color: #f0f0f0;
    border-radius: 10px;
    overflow: hidden;
    margin-bottom: 10px;
}

.progress-fill {
    height: 100%;
    background-color: #007cba;
    transition: width 0.3s ease;
}

.upload-actions {
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.success-message {
    color: #4caf50;
    font-weight: bold;
}

.error-message {
    color: #f44336;
    font-weight: bold;
}

.upload-error-message {
    color: #f44336;
    font-size: 14px;
    margin-top: 5px;
    padding: 5px;
    border-radius: 3px;
    background-color: #fff8f8;
}
</style>

13.3.5 安全的第三方集成

第三方服务安全集成

javascript
// 第三方集成安全管理器
class SecureThirdPartyManager {
    constructor() {
        this.trustedDomains = new Set([
            'apis.google.com',
            'cdn.jsdelivr.net',
            'cdnjs.cloudflare.com',
            'unpkg.com'
        ]);
        
        this.loadedScripts = new Map();
        this.apiKeys = new Map();
        this.rateLimits = new Map();
        
        this.init();
    }
    
    init() {
        // 监控脚本加载
        this.monitorScriptLoading();
        
        // 设置API密钥管理
        this.setupAPIKeyManagement();
        
        // 监控网络请求
        this.monitorNetworkRequests();
    }
    
    // 安全加载第三方脚本
    async loadSecureScript(url, options = {}) {
        const defaultOptions = {
            integrity: null,
            crossorigin: 'anonymous',
            timeout: 10000,
            retries: 3
        };
        
        const mergedOptions = { ...defaultOptions, ...options };
        
        try {
            // 验证URL
            if (!this.isValidScriptURL(url)) {
                throw new Error('不可信的脚本URL');
            }
            
            // 检查是否已加载
            if (this.loadedScripts.has(url)) {
                return this.loadedScripts.get(url);
            }
            
            // 加载脚本
            const script = await this.loadScript(url, mergedOptions);
            this.loadedScripts.set(url, script);
            
            return script;
        } catch (error) {
            console.error('脚本加载失败:', error);
            throw error;
        }
    }
    
    // 验证脚本URL
    isValidScriptURL(url) {
        try {
            const urlObj = new URL(url);
            
            // 检查协议
            if (!['https:', 'http:'].includes(urlObj.protocol)) {
                return false;
            }
            
            // 检查域名
            if (!this.trustedDomains.has(urlObj.hostname)) {
                console.warn('不可信的域名:', urlObj.hostname);
                return false;
            }
            
            // 检查路径
            if (urlObj.pathname.includes('..')) {
                return false;
            }
            
            return true;
        } catch (error) {
            return false;
        }
    }
    
    // 加载脚本
    loadScript(url, options) {
        return new Promise((resolve, reject) => {
            const script = document.createElement('script');
            script.src = url;
            script.async = true;
            
            // 设置安全属性
            if (options.integrity) {
                script.integrity = options.integrity;
            }
            
            if (options.crossorigin) {
                script.crossOrigin = options.crossorigin;
            }
            
            // 设置超时
            const timeoutId = setTimeout(() => {
                script.remove();
                reject(new Error('脚本加载超时'));
            }, options.timeout);
            
            script.onload = () => {
                clearTimeout(timeoutId);
                resolve(script);
            };
            
            script.onerror = () => {
                clearTimeout(timeoutId);
                script.remove();
                reject(new Error('脚本加载失败'));
            };
            
            document.head.appendChild(script);
        });
    }
    
    // 安全API调用
    async secureAPICall(endpoint, options = {}) {
        const defaultOptions = {
            method: 'GET',
            headers: {
                'Content-Type': 'application/json'
            },
            timeout: 30000,
            retries: 3,
            rateLimit: true
        };
        
        const mergedOptions = { ...defaultOptions, ...options };
        
        try {
            // 验证端点
            if (!this.isValidAPIEndpoint(endpoint)) {
                throw new Error('不可信的API端点');
            }
            
            // 检查速率限制
            if (mergedOptions.rateLimit && !this.checkRateLimit(endpoint)) {
                throw new Error('API调用过于频繁');
            }
            
            // 添加认证
            if (mergedOptions.requireAuth) {
                this.addAuthentication(mergedOptions, endpoint);
            }
            
            // 发送请求
            const response = await this.makeRequest(endpoint, mergedOptions);
            
            // 验证响应
            this.validateResponse(response);
            
            return response;
        } catch (error) {
            console.error('API调用失败:', error);
            throw error;
        }
    }
    
    // 验证API端点
    isValidAPIEndpoint(endpoint) {
        try {
            const url = new URL(endpoint);
            
            // 检查协议
            if (url.protocol !== 'https:') {
                return false;
            }
            
            // 检查域名白名单
            const trustedAPIs = [
                'api.github.com',
                'jsonplaceholder.typicode.com',
                'httpbin.org'
            ];
            
            return trustedAPIs.includes(url.hostname);
        } catch (error) {
            return false;
        }
    }
    
    // 检查速率限制
    checkRateLimit(endpoint) {
        const now = Date.now();
        const windowMs = 60000; // 1分钟
        const maxRequests = 100;
        
        if (!this.rateLimits.has(endpoint)) {
            this.rateLimits.set(endpoint, []);
        }
        
        const requests = this.rateLimits.get(endpoint);
        
        // 移除过期的请求
        const validRequests = requests.filter(timestamp => now - timestamp < windowMs);
        
        if (validRequests.length >= maxRequests) {
            return false;
        }
        
        // 记录当前请求
        validRequests.push(now);
        this.rateLimits.set(endpoint, validRequests);
        
        return true;
    }
    
    // 添加认证
    addAuthentication(options, endpoint) {
        const apiKey = this.apiKeys.get(endpoint);
        if (apiKey) {
            options.headers['Authorization'] = `Bearer ${apiKey}`;
        }
    }
    
    // 发送请求
    async makeRequest(endpoint, options) {
        const controller = new AbortController();
        const timeoutId = setTimeout(() => controller.abort(), options.timeout);
        
        try {
            const response = await fetch(endpoint, {
                ...options,
                signal: controller.signal
            });
            
            clearTimeout(timeoutId);
            
            if (!response.ok) {
                throw new Error(`HTTP错误: ${response.status}`);
            }
            
            return response;
        } catch (error) {
            clearTimeout(timeoutId);
            throw error;
        }
    }
    
    // 验证响应
    validateResponse(response) {
        // 检查响应头
        const contentType = response.headers.get('Content-Type');
        if (!contentType || !contentType.includes('application/json')) {
            console.warn('响应内容类型不是JSON');
        }
        
        // 检查安全头
        const securityHeaders = [
            'X-Content-Type-Options',
            'X-Frame-Options',
            'X-XSS-Protection'
        ];
        
        securityHeaders.forEach(header => {
            if (!response.headers.has(header)) {
                console.warn(`缺少安全头: ${header}`);
            }
        });
    }
    
    // 安全设置API密钥
    setAPIKey(endpoint, apiKey) {
        // 验证API密钥格式
        if (!this.isValidAPIKey(apiKey)) {
            throw new Error('无效的API密钥格式');
        }
        
        this.apiKeys.set(endpoint, apiKey);
        
        // 设置定期轮换提醒
        this.scheduleKeyRotation(endpoint);
    }
    
    // 验证API密钥
    isValidAPIKey(apiKey) {
        // 检查长度
        if (apiKey.length < 32) {
            return false;
        }
        
        // 检查格式
        if (!/^[a-zA-Z0-9_-]+$/.test(apiKey)) {
            return false;
        }
        
        return true;
    }
    
    // 监控脚本加载
    monitorScriptLoading() {
        const originalCreateElement = document.createElement;
        
        document.createElement = function(tagName) {
            const element = originalCreateElement.call(this, tagName);
            
            if (tagName.toLowerCase() === 'script') {
                // 监控脚本属性设置
                const observer = new MutationObserver((mutations) => {
                    mutations.forEach((mutation) => {
                        if (mutation.type === 'attributes' && mutation.attributeName === 'src') {
                            const src = element.getAttribute('src');
                            if (src && !this.isValidScriptURL(src)) {
                                console.warn('检测到不可信的脚本加载:', src);
                            }
                        }
                    });
                });
                
                observer.observe(element, { attributes: true });
            }
            
            return element;
        };
    }
    
    // 监控网络请求
    monitorNetworkRequests() {
        const originalFetch = window.fetch;
        
        window.fetch = async function(resource, options = {}) {
            const url = typeof resource === 'string' ? resource : resource.url;
            
            // 记录请求
            console.log('网络请求:', url);
            
            // 检查请求安全性
            if (!this.isSecureRequest(url, options)) {
                console.warn('不安全的网络请求:', url);
            }
            
            return originalFetch.call(this, resource, options);
        };
    }
    
    // 检查请求安全性
    isSecureRequest(url, options) {
        try {
            const urlObj = new URL(url);
            
            // 检查协议
            if (urlObj.protocol !== 'https:' && urlObj.hostname !== 'localhost') {
                return false;
            }
            
            // 检查凭据
            if (options.credentials === 'include' && urlObj.origin !== window.location.origin) {
                console.warn('跨域请求包含凭据');
            }
            
            return true;
        } catch (error) {
            return false;
        }
    }
    
    // 定期轮换API密钥
    scheduleKeyRotation(endpoint) {
        const rotationInterval = 30 * 24 * 60 * 60 * 1000; // 30天
        
        setTimeout(() => {
            console.warn(`API密钥需要轮换: ${endpoint}`);
            // 这里可以触发密钥轮换流程
        }, rotationInterval);
    }
}

// 使用示例
const thirdPartyManager = new SecureThirdPartyManager();

// 安全加载第三方脚本
async function loadGoogleMaps() {
    try {
        await thirdPartyManager.loadSecureScript(
            'https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY',
            {
                integrity: 'sha384-...',
                crossorigin: 'anonymous'
            }
        );
        
        // 初始化地图
        initializeMap();
    } catch (error) {
        console.error('Google Maps加载失败:', error);
    }
}

// 安全API调用
async function fetchUserData() {
    try {
        const response = await thirdPartyManager.secureAPICall(
            'https://jsonplaceholder.typicode.com/users/1',
            {
                method: 'GET',
                requireAuth: false,
                rateLimit: true
            }
        );
        
        const data = await response.json();
        console.log('用户数据:', data);
    } catch (error) {
        console.error('用户数据获取失败:', error);
    }
}

本节要点回顾

  • 输入验证:实施全面的输入验证策略,包括类型检查、格式验证和安全过滤
  • 输出编码:根据不同上下文进行适当的输出编码,防止XSS攻击
  • 表单安全:使用CSRF令牌、速率限制、实时验证等技术保护表单
  • 文件上传:验证文件类型、大小、内容和名称,防止恶意文件上传
  • 第三方集成:安全地加载和使用第三方服务,控制权限和访问

相关学习资源

常见问题FAQ

Q: 客户端验证是否足够安全?

A: 客户端验证只能提供用户体验,不能替代服务器端验证。所有安全关键的验证必须在服务器端进行。

Q: 如何选择合适的输出编码方式?

A: 根据输出上下文选择:HTML内容用HTML编码,属性值用属性编码,JavaScript中用JavaScript编码。

Q: 文件上传有哪些主要安全风险?

A: 主要风险包括恶意文件执行、路径遍历、文件类型伪造、存储空间耗尽等。

Q: 如何安全地集成第三方服务?

A: 验证第三方URL、使用HTTPS、设置CSP、限制权限、监控API调用等。

Q: CSRF令牌如何保证安全性?

A: CSRF令牌应该是随机生成、唯一的、有时效性的,并且只能通过安全的方式传输。


本章总结:本章详细介绍了HTML5安全性的各个方面,包括Web安全基础、HTML5安全特性和安全最佳实践。通过学习这些内容,您将能够构建更加安全的Web应用程序。