License检查的那些坑我替你踩过了

设计师燕伟 安全 阅读 1,451
赞 18 收藏
二维码
手机扫码查看
反馈

License验证这破事,折腾了我三天

最近接了一个客户项目,需要做个License验证的功能,看起来挺简单的事情,结果被各种安全绕过搞得很头疼。本来想着就是简单的接口验证,没想到客户那边还有各种特殊需求,比如离线验证、缓存机制、防篡改等等。

License检查的那些坑我替你踩过了

最初的想法太天真了

最开始我想法很简单,就是前端发个请求验证License,返回成功就继续执行,失败就提示购买。代码大概是这样:

async function checkLicense(licenseKey) {
    try {
        const response = await fetch('https://api.example.com/verify', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                license: licenseKey,
                machineId: getMachineId()
            })
        });
        
        const result = await response.json();
        return result.valid;
    } catch (error) {
        console.error('License check failed:', error);
        return false;
    }
}

结果测试的时候发现,这种简单的验证方式很容易被绕过。用户直接修改响应数据或者拦截网络请求就能破解,根本起不到保护作用。

第一次升级:本地缓存加加密存储

后来试了下加入本地缓存机制,避免每次都去服务器验证:

class LicenseManager {
    constructor() {
        this.cacheKey = 'license_cache';
        this.encryptionKey = 'your-secret-key';
    }

    async validateLicense(licenseKey) {
        // 先检查本地缓存
        const cachedResult = this.getCachedValidation();
        if (cachedResult && !this.isExpired(cachedResult)) {
            return cachedResult.valid;
        }

        // 调用远程验证
        const remoteResult = await this.remoteValidate(licenseKey);
        
        if (remoteResult.valid) {
            // 缓存验证结果
            this.cacheValidation(remoteResult);
        }
        
        return remoteResult.valid;
    }

    getCachedValidation() {
        try {
            const encryptedData = localStorage.getItem(this.cacheKey);
            if (!encryptedData) return null;
            
            const decrypted = this.decrypt(encryptedData, this.encryptionKey);
            return JSON.parse(decrypted);
        } catch (e) {
            return null;
        }
    }

    cacheValidation(data) {
        data.timestamp = Date.now();
        const encrypted = this.encrypt(JSON.stringify(data), this.encryptionKey);
        localStorage.setItem(this.cacheKey, encrypted);
    }

    isExpired(cachedData) {
        const maxAge = 24 * 60 * 60 * 1000; // 24小时
        return Date.now() - cachedData.timestamp > maxAge;
    }
}

但是这里我踩了个坑,加密算法选择不当会导致性能问题。一开始用了AES-CBC,结果在低配设备上运行很慢,后来换成更轻量级的加密方式才解决。

安全对抗升级:指纹识别和多重验证

客户反映还是有人能破解,我就意识到需要更强的安全措施。开始研究设备指纹识别和多重验证机制:

class AdvancedLicenseManager {
    constructor() {
        this.maxRetries = 3;
        this.retryDelay = 2000;
    }

    getDeviceFingerprint() {
        const canvas = document.createElement('canvas');
        const ctx = canvas.getContext('2d');
        ctx.textBaseline = 'top';
        ctx.font = '14px Arial';
        ctx.fillText('License Test', 2, 2);
        
        return {
            userAgent: navigator.userAgent,
            language: navigator.language,
            platform: navigator.platform,
            hardwareConcurrency: navigator.hardwareConcurrency,
            memory: navigator.deviceMemory || 'unknown',
            screenResolution: ${screen.width}x${screen.height},
            timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
            canvasHash: canvas.toDataURL(),
            timestamp: Date.now()
        };
    }

    async robustValidate(licenseKey) {
        const fingerprint = this.getDeviceFingerprint();
        let lastError;

        for (let i = 0; i < this.maxRetries; i++) {
            try {
                const response = await fetch('https://jztheme.com/api/license/verify', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'X-Requested-With': 'XMLHttpRequest'
                    },
                    body: JSON.stringify({
                        license: this.obfuscateLicense(licenseKey),
                        fingerprint: this.hashFingerprint(fingerprint),
                        timestamp: Date.now()
                    }),
                    credentials: 'include'
                });

                if (!response.ok) {
                    throw new Error(HTTP ${response.status});
                }

                const data = await response.json();
                
                if (data.signature && this.verifySignature(data)) {
                    return this.processResponse(data);
                }

            } catch (error) {
                lastError = error;
                if (i < this.maxRetries - 1) {
                    await this.delay(this.retryDelay);
                }
            }
        }

        throw lastError;
    }

    obfuscateLicense(license) {
        // 简单的混淆处理
        return btoa(license.split('').reverse().join(''));
    }

    hashFingerprint(fingerprint) {
        // 这里应该用更安全的哈希算法
        return this.simpleHash(JSON.stringify(fingerprint));
    }

    simpleHash(str) {
        let hash = 0;
        for (let i = 0; i < str.length; i++) {
            const char = str.charCodeAt(i);
            hash = ((hash << 5) - hash) + char;
            hash = hash & hash; // 转换为32位整数
        }
        return hash.toString();
    }
}

签名验证防止中间人攻击

光有基本的验证还不够,还需要防止中间人攻击。我加了签名验证机制:

// 后端返回的数据结构示例
const expectedResponse = {
    valid: true,
    expiresAt: 1700000000,
    features: ['feature1', 'feature2'],
    signature: 'server-generated-signature', // 这是关键
    nonce: 'random-nonce-value'
};

// 前端验证签名
class SignatureValidator {
    constructor(publicKey) {
        this.publicKey = publicKey;
    }

    async verifySignature(data) {
        try {
            const { signature, ...dataWithoutSig } = data;
            const stringToVerify = JSON.stringify(dataWithoutSig);
            
            const encoder = new TextEncoder();
            const dataBuffer = encoder.encode(stringToVerify);
            const sigBuffer = Uint8Array.from(atob(signature), c => c.charCodeAt(0));

            // 使用Web Crypto API验证签名
            return await crypto.subtle.verify(
                'RSASSA-PKCS1-v1_5',
                this.publicKey,
                sigBuffer,
                dataBuffer
            );
        } catch (error) {
            console.error('Signature verification failed:', error);
            return false;
        }
    }
}

离线验证的实现

客户还要求支持离线环境,这就更复杂了。离线验证的核心思想是在在线验证通过后,生成一个本地有效的许可证文件:

class OfflineLicenseManager {
    constructor() {
        this.offlineCacheKey = 'offline_license_data';
        this.gracePeriod = 7 * 24 * 60 * 60 * 1000; // 7天宽限期
    }

    async handleOfflineScenario(licenseKey) {
        const onlineResult = await this.onlineValidate(licenseKey);
        
        if (onlineResult.valid) {
            // 在线验证成功,保存离线数据
            const offlineData = {
                license: licenseKey,
                validUntil: onlineResult.expiresAt,
                generatedAt: Date.now(),
                signature: await this.generateLocalSignature(onlineResult)
            };
            
            localStorage.setItem(this.offlineCacheKey, JSON.stringify(offlineData));
            return onlineResult;
        }

        // 尝试离线验证
        return this.validateOffline();
    }

    validateOffline() {
        try {
            const offlineData = JSON.parse(localStorage.getItem(this.offlineCacheKey));
            
            if (!offlineData) {
                return { valid: false, reason: 'no_cached_data' };
            }

            // 检查是否在宽限期内
            const now = Date.now();
            const graceEnds = offlineData.generatedAt + this.gracePeriod;

            if (now > graceEnds) {
                return { valid: false, reason: 'grace_period_expired' };
            }

            // 验证本地签名(防止篡改)
            if (!this.verifyLocalSignature(offlineData)) {
                return { valid: false, reason: 'signature_mismatch' };
            }

            return {
                valid: Date.now() <= offlineData.validUntil,
                expiresAt: offlineData.validUntil
            };

        } catch (error) {
            console.error('Offline validation error:', error);
            return { valid: false, reason: 'parse_error' };
        }
    }
}

最后的优化和注意事项

折腾了几天之后,最终的方案基本稳定了。不过这里有几个需要注意的地方:

  • 不要把所有验证逻辑都放在前端,关键的License信息还是要依赖服务端验证
  • 缓存策略要合理,不能让用户一直用着过期的License
  • 错误处理要做好,网络异常时不能让程序崩溃
  • 性能要考虑,复杂的加密计算不能影响用户体验

还有一个细节,就是定时更新缓存。我在应用启动时会检查License状态,定期刷新缓存数据,避免用户在关键时刻发现License过期的问题。

// 定时检查License状态
setInterval(async () => {
    try {
        const isValid = await this.checkCachedLicense();
        if (!isValid) {
            this.notifyUserAboutInvalidLicense();
        }
    } catch (error) {
        console.warn('License check failed:', error.message);
    }
}, 30 * 60 * 1000); // 每30分钟检查一次

虽然现在的方案还不是100%完美(总有技术高手能破解),但至少能满足大部分正常使用场景,也能防止一些简单的破解行为。改完后偶尔还会有个别用户反映验证失败,但基本都是网络问题,整体稳定性还可以。

以上是我踩坑后的总结,如果你有更好的方案欢迎评论区交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论