在小游戏开发过程中,数据存储是绕不开的环节。做demo的初期为了快速迭代,我直接采用明码 CSV 文件存储游戏配置(比如关卡数据、道具属性、角色数值),虽然开发效率高,但明码数据的安全性问题显而易见 ------ 玩家可轻易修改文件篡改游戏数据,破坏游戏平衡。本想通过二进制文件存储提升安全性,但是我在 Cocos3.8 读取二进制文件需要非常绕圈子的配置,因为缓存需要不断的重启相当烦恼,最终不搞纯二进制而是选择了加密后保存为 TXT 文件的方案,本文就聊聊这个过程中的踩坑与解决方案。
winform 版本地址
https://gitee.com/wgc2k/csv_encrypter
一、初期痛点:明码 CSV 的安全隐患
小游戏开发初期,CSV 因 "易编辑、易解析" 成为我的首选:用 Excel 整理数据,导出为 CSV 后,Cocos 通过简单的文件读取和字符串分割就能解析。但上线前的安全自测让我发现问题:玩家只需用记事本打开 CSV 文件,就能随意修改关卡奖励、道具数量等核心数据,甚至能直接解锁所有关卡。
举个简单的 CSV 示例(game_config.csv):
level,gold,reward
1,100,coin_10
2,200,coin_20
3,300,diamond_5
这种明码存储方式对小游戏而言,虽不至于引发严重安全事故,但会彻底破坏游戏体验,数据加密势在必行。
二、踩坑:Cocos3.8 读取二进制文件的 "水土不服"
最初的优化思路是将 CSV 转为二进制文件(比如自定义格式的 bin 文件),二进制文件本身具备一定的 "隐蔽性",且读取效率更高。但在 Cocos3.8 中实现时,却遇到了一系列问题:
1. 二进制文件读取的核心问题
Cocos3.8 基于 TypeScript/JavaScript 开发,其文件读取 API(cc.assetManager.loadAny、fs.readFile)对文本文件的支持非常友好,但处理二进制文件时存在明显短板:
- 跨平台兼容性差:在 Web 端、微信小游戏端、原生端读取二进制文件时,数据解析格式不一致(比如 Uint8Array 和 ArrayBuffer 的转换差异);
- 解析成本高:自定义二进制格式需要编写复杂的序列化 / 反序列化逻辑,对小游戏轻量化开发而言过于繁琐;
- 缓存机制干扰:Cocos 的资源缓存会对二进制文件做额外处理,调试时候会出现读取到旧数据的情况。
2. 尝试后的放弃
我曾编写过简单的二进制文件读写工具,将 CSV 数据转为二进制流存储,但在多端测试时,Web 端能正常读取,微信小游戏端却出现数据截断,原生端有时报文件解析错误。反复调试后发现,Cocos3.8 对不同平台的二进制文件编码、字节序处理存在未统一的细节,修复成本远超预期,最终决定放弃纯二进制方案。
三、折衷方案:AES 加密 + TXT 文件存储
既然二进制方案落地困难,我转向 "文本文件 + 加密" 的思路:用 AES 对称加密算法对 CSV 数据加密,将加密后的密文保存为 TXT 文件,游戏运行时读取 TXT 密文,解密后再解析为可用数据。这个方案兼顾了安全性和开发效率,且 Cocos3.8 对 TXT 文件的读取支持完善。
1. 核心思路
- 开发离线工具:将 Excel/CSV 中的明码数据,通过 AES 加密生成密文;
- 密文存储:将加密后的密文保存为 TXT 文件,放入 Cocos 资源目录;
- 游戏内解密:运行时读取 TXT 密文,通过 AES 解密得到原始 CSV 数据,再解析使用。
2. 实现代码(Node.js 离线加密工具 + Cocos3.8 解密逻辑)
第一步:Node.js 离线加密工具(处理 CSV 生成加密 TXT)
先通过 Node.js 编写加密脚本,将明码 CSV 转为加密 TXT(需安装crypto-js依赖):
bash运行
npm install crypto-js --save
javascript
// encrypt-csv.js
const CryptoJS = require('crypto-js');
const fs = require('fs');
const path = require('path');
// 配置AES密钥(建议16/24/32位,对应AES-128/AES-192/AES-256)
const AES_KEY = CryptoJS.enc.Utf8.parse('1234567890abcdef'); // 16位密钥
const AES_IV = CryptoJS.enc.Utf8.parse('abcdef1234567890'); // 16位偏移量
/**
* AES加密函数(CBC模式,PKCS7填充)
* @param {string} plainText 明文(CSV文本)
* @returns {string} 加密后的密文
*/
function aesEncrypt(plainText) {
const encrypted = CryptoJS.AES.encrypt(
plainText,
AES_KEY,
{
iv: AES_IV,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}
);
return encrypted.toString();
}
// 读取CSV明码文件
const csvPath = path.join(__dirname, 'game_config.csv');
const txtOutputPath = path.join(__dirname, 'game_config_encrypt.txt');
fs.readFile(csvPath, 'utf8', (err, data) => {
if (err) {
console.error('读取CSV失败:', err);
return;
}
// 加密CSV数据
const cipherText = aesEncrypt(data);
// 保存为TXT文件
fs.writeFile(txtOutputPath, cipherText, 'utf8', (err) => {
if (err) {
console.error('保存加密TXT失败:', err);
return;
}
console.log(`加密完成!密文已保存至:${txtOutputPath}`);
});
});
运行脚本生成加密 TXT:
bash
node encrypt-csv.js
第二步:Cocos3.8 中解密并解析 TXT
在 Cocos3.8 项目中,先引入crypto-js(可通过 npm 安装后拷贝到项目,或直接引入 CDN 版本),然后编写解密逻辑:
typescript
// GameDataManager.ts
import { _decorator, Component, Node, assetManager, resources } from 'cc';
const { ccclass, property } = _decorator;
// 引入crypto-js(需将crypto-js的包放入项目,或通过import映射)
import CryptoJS from './crypto-js.min';
// 与加密端一致的密钥和偏移量
const AES_KEY = CryptoJS.enc.Utf8.parse('1234567890abcdef');
const AES_IV = CryptoJS.enc.Utf8.parse('abcdef1234567890');
/**
* AES解密函数
* @param {string} cipherText 密文
* @returns {string} 解密后的明文
*/
function aesDecrypt(cipherText: string): string {
const decrypt = CryptoJS.AES.decrypt(
cipherText,
AES_KEY,
{
iv: AES_IV,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}
);
return CryptoJS.enc.Utf8.stringify(decrypt).toString();
}
@ccclass('GameDataManager')
export class GameDataManager extends Component {
// 游戏配置数据
private gameConfig: any[] = [];
onLoad() {
this.loadAndDecryptData();
}
/**
* 加载加密TXT并解密解析
*/
private async loadAndDecryptData() {
try {
// 读取加密TXT文件(放入resources目录)
const res = await resources.loadAsync('game_config_encrypt', { type: cc.TextAsset });
const cipherText = res.text;
// 解密得到CSV明文
const csvPlainText = aesDecrypt(cipherText);
// 解析CSV数据(简单分割,复杂场景可引入csv-parser)
this.parseCsv(csvPlainText);
console.log('数据解密解析完成:', this.gameConfig);
} catch (err) {
console.error('数据加载解密失败:', err);
}
}
/**
* 解析CSV明文为数组
* @param {string} csvText CSV明文
*/
private parseCsv(csvText: string) {
const lines = csvText.split('\n').filter(line => line.trim());
const headers = lines[0].split(',');
for (let i = 1; i < lines.length; i++) {
const values = lines[i].split(',');
const row: any = {};
headers.forEach((header, index) => {
row[header] = values[index];
});
this.gameConfig.push(row);
}
}
}
3. 关键注意事项
- 密钥安全:AES 密钥和偏移量不要直接写在代码里!可通过混淆、分段拼接等方式隐藏,避免反编译后被提取;
- 跨平台适配:Cocos3.8 的
resources.loadAsync读取 TXT 在各端表现一致,无需额外适配; - 性能优化:解密操作建议在游戏启动时执行一次,将解析后的数据缓存,避免重复解密;
- 加密模式:选择 CBC 模式(需偏移量 IV)而非 ECB 模式,ECB 模式安全性较低,易被破解。
四、方案对比与效果
| 存储方式 | 安全性 | 开发成本 | 跨平台兼容性 | 读取效率 |
|---|---|---|---|---|
| 明码 CSV | 极低 | 极低 | 极好 | 较高 |
| 自定义二进制 | 中等 | 极高 | 较差 | 最高 |
| AES 加密 TXT | 较高 | 中等 | 极好 | 中等 |
实际测试中,AES 加密 TXT 方案在 Web、微信小游戏、原生端均能稳定运行,解密耗时在毫秒级,对小游戏性能几乎无影响;同时,玩家即使找到 TXT 文件,也无法直接修改密文(AES 加密无密钥无法破解),有效解决了数据篡改问题。
五、进阶优化方向思路
- 数据压缩:加密前先对 CSV 数据进行 GZIP 压缩,减少 TXT 文件体积;
- 密钥动态生成:通过服务器下发密钥(小游戏首次启动时请求),彻底避免本地密钥泄露;
- 混合加密:对核心数据(如玩家等级、金币)采用 "非对称加密 + 对称加密" 混合方案,进一步提升安全性;
- 数据校验:解密后增加 MD5 校验,防止密文被篡改后解密失败导致游戏崩溃。
总结
在 Cocos3.8 小型游戏开发中,数据加密不必追求 "纯二进制" 的极致方案,结合自身开发成本和跨平台需求,选择 "AES 加密 + TXT 存储" 的折衷方案是更务实的选择。这个方案既解决了明码 CSV 的安全隐患,又避开了 Cocos3.8 读取二进制文件的兼容性问题,兼顾了安全性、开发效率和跨平台适配性。我最终使用了winform实现了这个加密小工具示例,最终未作混淆实现,如果追求安全性可以自行实现。另外各位有没有二进制加密比较好的方案,可以在评论区指点一下。