目录
前言:
正题:
Json
序列化与反序列化
数据加密
顺便一提
最近用json保存角色属性数据,保存在本地,可以被随意修改,这就很蛋疼了,随随便便将什么攻击力、防御力改一改,这跟开挂没啥区别了......后来还试了序列化与反序列化的方式来保存数据。虽然可读性不如json,但实际上还是可以修改。后来找了一下微软的加密数据,有分对称加密与非对称加密。本文使用的是对称加密,unity版本是2020.3.16。
测试数据类:
///
/// 玩家类
///
[System.Serializable]
public class Player
{
public string playerName; //玩家名字
public int attack; //玩家攻击力
public int armor; //护甲
}
保存或读取Json数据的方法:
///
/// 写入json数据
///
/// 目标类型
/// 目标数据类
/// 路径
public void WriteJsonData(T _object, string filePath)
{
string _content = JsonUtility.ToJson(_object);
File.WriteAllText(filePath, _content);
}
///
/// 读取json数据
///
/// 目标类型
/// 路径
/// 目标数据类
public T ReadJsonData(string filePath)
{
string jsonStr = File.ReadAllText(filePath);
T target = JsonUtility.FromJson(jsonStr);
return target;
}
下面是我们平常使用json保存的数据:
很明显,每个数据清晰明了,玩家都会修改了,这不是我们想要的。
于是我换另一种方式,使用序列化与反序列化来实现,又有怎样的结果呢?
关于序列化与反序列化,可以自行查阅资料。
微软API:BinaryFormatter 类 (System.Runtime.Serialization.Formatters.Binary) | Microsoft Docs以二进制格式序列化和反序列化对象或连接对象的整个图形。Serializes and deserializes an object, or an entire graph of connected objects, in binary format.https://docs.microsoft.com/zh-cn/dotnet/api/system.runtime.serialization.formatters.binary.binaryformatter?view=netframework-4.7.1
代码如下:
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using UnityEngine;
///
///
/// * Writer:June
///
/// * Data:2021.11.6
///
/// * Function:序列化数据管理器
///
/// * Remarks:
///
///
public class SerializeDataManager : MonoBehaviour
{
///
/// 保存数据
///
/// 文件路径
/// 目标对象
public static void SaveData(string filePath, object obj)
{
//判断是否存在储存数据的文件,不存在则创建
DirectoryInfo directoryInfo = new DirectoryInfo(Application.streamingAssetsPath);
//是否存在文件夹,不存在则创建
if (!directoryInfo.Exists) Directory.CreateDirectory(directoryInfo.ToString());
//数据序列化
byte[] data = Serialize(obj);
//将数据写入到文件中
File.WriteAllBytes(filePath, data);
}
///
/// 加载数据
///
/// 类型
/// 文件路径
///
public static T LoadData(string filePath) where T : class
{
//文件是否存在
if (!File.Exists(filePath))
{
Debug.LogError("文件不存在,请检查路径是否有误!");
return null;
}
//读取加密数据
byte[] readDataBt = File.ReadAllBytes(filePath);
//文件数据是否为空
if (readDataBt.Length <= 0)
{
Debug.LogError("无数据!");
return null;
}
//反序列化
T target = Deserialize(readDataBt);
return target;
}
///
/// 序列化
///
private static byte[] Serialize(object targetObj)
{
//判断是否可序列化
if (targetObj == null || !targetObj.GetType().IsSerializable)
{
Debug.LogError("目标类为空,或者无法序列化,如无法序列化,请检查类是否添加[Serializable]特性");
return null;
}
//序列化
using (MemoryStream stream = new MemoryStream())
{
BinaryFormatter binaryFormatter = new BinaryFormatter();
binaryFormatter.Serialize(stream, targetObj);
byte[] serBt = stream.ToArray();
return serBt;
}
}
///
/// 反序列化
///
private static T Deserialize(byte[] data) where T : class
{
using (MemoryStream stream = new MemoryStream(data))
{
BinaryFormatter binaryFormatter = new BinaryFormatter();
object deserObj = binaryFormatter.Deserialize(stream);
T tmpClass = deserObj as T;
return tmpClass;
}
}
}
///
/// 玩家类
///
[System.Serializable]
public class Player
{
public string playerName; //玩家名字
public int attack; //玩家攻击力
public int armor; //护甲
}
测试代码:
using System.IO;
using UnityEngine;
///
///
/// * Writer:June
///
/// * Data:2021.11.9
///
/// * Function:序列化数据测试类
///
/// * Remarks:
///
///
public class SerializeDataTest : MonoBehaviour
{
private string filePath;
private void Start() => filePath = Path.Combine(Application.streamingAssetsPath, "Data.txt");
private void Update()
{
//保存
if (Input.GetKeyDown(KeyCode.S))
{
//实例化玩家
Player player = new Player()
{
playerName = "June",
attack = 20,
armor = 50
};
SerializeDataManager.SaveData(filePath, player);
}
//加载
if (Input.GetKeyDown(KeyCode.L))
{
Player player = SerializeDataManager.LoadData(filePath);
Debug.Log($"玩家名字:{player.playerName} 攻击力:{player.attack} 护甲:{player.armor}");
}
}
}
运行后打开数据文件Data.txt(如果没事先创建的话,要刷新一下project窗口下的列表,就会出现此文件):
使用记事本打开与Notepad++打开,修改字符串后重新加载数据会有不同效果,前者报错,后者可以运行
虽然可读性没有json好,但依然可以看到裸露的数据,比如玩家名字,我存的是June,现在显示出来了。但是修改后,再去读数据,就会出现以下报错,无法被反序列化。
这就意味着,玩家要是使用记事本打开来修改数据的话,会没法反序列化。但是,若使用Notepad++,情况就有所不同了。
哦豁~加载出来了。这样的话,凡是字符串类型的数据 ,一样是可以修改的。
那么总的来说,序列化与反序列化的方式虽然可读性不如Json,减少数据暴露,但实际上也是不安全的。于是就引出了数据加密。
对于数据加密,我这里使用的是微软自带的API,属于对称加密的方式,即Aes加密算法。
微软API:对数据加密 | Microsoft Docshttps://docs.microsoft.com/zh-cn/dotnet/standard/security/encrypting-data
额外扩展资料:
GoLang:有趣的密码学之加密 - 知乎写在前面笔者在下定决心要学习密码学相关知识之前,一直对加密解密感到无力:数学公式也太难了,定理未免也太多了!加上工作中就算不清楚原理,只需要知道对称加密、非对称加密大概是什么;会调用相关的API就可以…https://zhuanlan.zhihu.com/p/123221394我这里的思路是:
思路也是很简单的,废话少说,直接上代码吧:
using System.IO;
using System.Security.Cryptography;
using UnityEngine;
///
///
/// * Writer:June
///
/// * Data:2021.11.9
///
/// * Function:Json数据管理器
///
/// * Remarks:
///
///
public class JsonDataManager : MonoBehaviour
{
///
/// 当前创建的密钥
///
private static byte[] currentKey;
///
/// 当前创建的初始化向量
///
private static byte[] currentIV;
///
/// 保存密钥的文件名
///
private const string KEYFILENAME = "KeyData.txt";
///
/// 保存初始化向量的文件名
///
private const string IVFILENAME = "IVData.txt";
///
/// 保存数据
///
/// 文件路径
/// 需要保存的目标
public static void SaveData(string filePath, object obj)
{
//判断是否存在储存数据的文件,不存在则创建
DirectoryInfo directoryInfo = new DirectoryInfo(Application.streamingAssetsPath);
//是否存在文件夹,不存在则创建
if (!directoryInfo.Exists) Directory.CreateDirectory(directoryInfo.ToString());
//数据序列化
string dataStr = JsonUtility.ToJson(obj);
//实例化算法类
using (Aes aes = Aes.Create())
{
currentKey = aes.Key;
currentIV = aes.IV;
//将当前密钥、初始化向量保存在数据文件中
SaveCurrentKeyAndIv(currentKey, currentIV);
byte[] EncryptionData = AesEncryption(dataStr, aes.Key, aes.IV);
//将数据写入到文件中
File.WriteAllBytes(filePath, EncryptionData);
}
}
///
/// 加载数据
///
/// 类型
/// 文件路径
///
public static T LoadData(string filePath) where T : class
{
//文件是否存在
if (!File.Exists(filePath))
{
Debug.LogError("文件不存在,请检查路径是否有误!");
return null;
}
//读取加密数据
byte[] readDataBt = File.ReadAllBytes(filePath);
//文件数据是否为空
if (readDataBt.Length <= 0)
{
Debug.LogError("无数据!");
return null;
}
//如果当前密钥、初始化向量为空,则从配置文件中读取加密时,使用的密钥、初始化向量
if (currentKey == null || currentKey.Length <= 0 || currentIV == null || currentIV.Length <= 0)
{
currentKey = LoadCurrentKey();
currentIV = LoadCurrentIV();
}
if (currentKey.Length <= 0 || currentIV.Length <= 0)
{
Debug.LogError("当前创建的密钥或初始化向量为空!");
return null;
}
string decryptData = AesDecrypt(readDataBt, currentKey, currentIV);
T target = JsonUtility.FromJson(decryptData);
return target;
}
///
/// 使用对称算法加密(Aes算法)
///
/// 目标内容
/// 对称算法的密钥
/// 对称算法的初始化向量
///
private static byte[] AesEncryption(string targetStr, byte[] key, byte[] iv)
{
#region 检测参数
//判断内容是否为空
if (targetStr == null || targetStr.Length <= 0)
{
Debug.LogError("目标内容为空!");
return null;
}
//判断密钥与初始化向量
if (key == null || key.Length <= 0)
{
Debug.LogError("输入密钥为空!");
return null;
}
if (iv == null || iv.Length <= 0)
{
Debug.LogError("输入初始化向量为空!");
return null;
}
#endregion
#region 加密
byte[] encryptionData; //将数据加密后得到的字节数组
using (Aes aes = Aes.Create()) //新建密钥
{
aes.Key = key;
aes.IV = iv;
//创建加密程序以执行流转换
ICryptoTransform cryptoTF = aes.CreateEncryptor(aes.Key, aes.IV);
//创建用于加密的流
using (MemoryStream memoryStream =new MemoryStream())
{
using (CryptoStream cryptoStream = new CryptoStream(memoryStream, cryptoTF, CryptoStreamMode.Write))
{
using (StreamWriter streamWriter = new StreamWriter(cryptoStream))
{
streamWriter.Write(targetStr); //将数据写入到流中
}
encryptionData = memoryStream.ToArray();
}
}
return encryptionData;
}
#endregion
}
///
/// 对称算法解密
///
/// 需要解密的数据
/// 密钥
/// 初始化向量
///
private static string AesDecrypt(byte[] targetBtData, byte[] key, byte[] iv)
{
#region 检测参数
if (targetBtData == null || targetBtData.Length <= 0)
{
Debug.LogError("解密数据为空!");
return null;
}
if (key == null || key.Length <= 0)
{
Debug.LogError("输入密钥为空!");
return null;
}
if (iv == null || iv.Length <= 0)
{
Debug.LogError("输入初始化向量为空!");
return null;
}
#endregion
#region 解密
string decryptStr;
using (Aes aes = Aes.Create()) //实例化算法类
{
aes.Key = key;
aes.IV = iv;
//创建解密程序以执行流转换
ICryptoTransform cryptoTF = aes.CreateDecryptor(aes.Key, aes.IV);
using (MemoryStream memoryStream = new MemoryStream(targetBtData))
{
using (CryptoStream cryptoStream = new CryptoStream(memoryStream, cryptoTF, CryptoStreamMode.Read))
{
using (StreamReader streamReader = new StreamReader(cryptoStream))
{
decryptStr = streamReader.ReadToEnd();
}
}
}
return decryptStr;
}
#endregion
}
///
/// 保存当前密钥以及初始化向量
///
/// 密钥
/// 初始化向量
private static void SaveCurrentKeyAndIv(byte[] keyBt, byte[] ivBt)
{
//保存key
string keyDataPath = Path.Combine(Application.streamingAssetsPath, KEYFILENAME);
File.WriteAllBytes(keyDataPath, keyBt);
//保存iv
string ivDataPath = Path.Combine(Application.streamingAssetsPath, IVFILENAME);
File.WriteAllBytes(ivDataPath, ivBt);
}
///
/// 加载当前密钥
///
///
private static byte[] LoadCurrentKey()
{
//从数据文件中读取
byte[] keyData = File.ReadAllBytes(Path.Combine(Application.streamingAssetsPath, KEYFILENAME));
return keyData;
}
///
/// 加载当前初始化向量
///
///
private static byte[] LoadCurrentIV()
{
byte[] ivData = File.ReadAllBytes(Path.Combine(Application.streamingAssetsPath, IVFILENAME));
return ivData;
}
}
///
/// 玩家类
///
[System.Serializable]
public class PlayerData
{
public string playerName; //玩家名字
public int attack; //玩家攻击力
public int armor; //护甲
}
测试代码:
using System.IO;
using UnityEngine;
///
///
/// * Writer:June
///
/// * Data:2021.11.9
///
/// * Function:测试数据保存与加载
///
/// * Remarks:
///
///
public class JsonDataTest : MonoBehaviour
{
private string filePath;
private void Start() => filePath = Path.Combine(Application.streamingAssetsPath, "PlayerData.txt");
private void Update()
{
//保存
if (Input.GetKeyDown(KeyCode.S))
{
//实例化玩家
PlayerData player = new PlayerData()
{
playerName = "June",
attack = 20,
armor = 50
};
JsonDataManager.SaveData(filePath, player);
}
//加载
if (Input.GetKeyDown(KeyCode.L))
{
PlayerData player = JsonDataManager.LoadData(filePath);
Debug.Log($"玩家名字:{player.playerName} 攻击力:{player.attack} 护甲:{player.armor}");
}
}
}
打开保存的数据文本:
好的,已经成功加密了! (不管在什么编码格式下,都呈现乱码~)
注:每次因为是随机创建的一组密钥与初始化向量,所以每次加密后写入的文本内容也是不一样的。这意味着,每次都需要记录下当前用于加密的Key、IV,当解密时,再从记录的文件中读取。我这里使用两个文件(KeyData.txt、IVData.txt)分别保存,而代码中出现的currentKey、currentIV,则是用于存储临时的,当然,与两个文件中的内容是一样的。这两个临时变量的作用在于:当玩家没有退出程序,又需要加载的时候,解密会直接从这两个变量中读取Key、IV,而不需要再次从文件中读取,节省了一点性能。
这里使用的是unity封装的JsonUtility,而不是使用的LitJson,论功能的话,LitJson无疑是更强的,但JsonUtility的好处是,效率高(性能),并且不需要导入第三方库,方便。至于普遍说的不能序列化List的问题,其实是可以序列化的,只不过不像LitJson可以直接序列化,JsonUtility序列化List,需要用类(class)或结构(struct)封装。字典的话,这种方法确实是没办法序列化的。
参考资料: Unity JsonUtility的局限性-腾讯游戏学堂