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立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。

暂无评论