Unity | 本地保存角色数据并加密

目录

前言:

正题:

Json

序列化与反序列化

数据加密

顺便一提


前言:

        最近用json保存角色属性数据,保存在本地,可以被随意修改,这就很蛋疼了,随随便便将什么攻击力、防御力改一改,这跟开挂没啥区别了......后来还试了序列化与反序列化的方式来保存数据。虽然可读性不如json,但实际上还是可以修改。后来找了一下微软的加密数据,有分对称加密与非对称加密。本文使用的是对称加密,unity版本是2020.3.16。

正题:

Json

测试数据类:

/// 
/// 玩家类
/// 
[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++打开,修改字符串后重新加载数据会有不同效果,前者报错,后者可以运行

Unity | 本地保存角色数据并加密_第1张图片

 虽然可读性没有json好,但依然可以看到裸露的数据,比如玩家名字,我存的是June,现在显示出来了。但是修改后,再去读数据,就会出现以下报错,无法被反序列化。

Unity | 本地保存角色数据并加密_第2张图片

 这就意味着,玩家要是使用记事本打开来修改数据的话,会没法反序列化。但是,若使用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我这里的思路是:

Unity | 本地保存角色数据并加密_第3张图片

 思路也是很简单的,废话少说,直接上代码吧:

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}");
        }
    }
}

打开保存的数据文本:

Unity | 本地保存角色数据并加密_第4张图片

好的,已经成功加密了! (不管在什么编码格式下,都呈现乱码~)

注:每次因为是随机创建的一组密钥与初始化向量,所以每次加密后写入的文本内容也是不一样的。这意味着,每次都需要记录下当前用于加密的Key、IV,当解密时,再从记录的文件中读取。我这里使用两个文件(KeyData.txt、IVData.txt)分别保存,而代码中出现的currentKey、currentIV,则是用于存储临时的,当然,与两个文件中的内容是一样的。这两个临时变量的作用在于:当玩家没有退出程序,又需要加载的时候,解密会直接从这两个变量中读取Key、IV,而不需要再次从文件中读取,节省了一点性能。

顺便一提

        这里使用的是unity封装的JsonUtility,而不是使用的LitJson,论功能的话,LitJson无疑是更强的,但JsonUtility的好处是,效率高(性能),并且不需要导入第三方库,方便。至于普遍说的不能序列化List的问题,其实是可以序列化的,只不过不像LitJson可以直接序列化,JsonUtility序列化List,需要用类(class)或结构(struct)封装。字典的话,这种方法确实是没办法序列化的。

参考资料: Unity JsonUtility的局限性-腾讯游戏学堂

你可能感兴趣的:(笔记,unity)