godot+c#操作godot-sqlite并加解密
前言
在前面一篇文章,我已经写好了如何
godot+c#使用godot-sqlite连接数据库
但是这会有一个问题,当对这个数据库有了解的人,很容易就可以实现篡改游戏数据.
因此,本次将对其进行加解密访问
步骤
加解密
依赖安装
- 和前面文章一样,我们需要引入加解密的依赖。
- 同样,找到你建立的godot游戏项目,找到有csproj格式文件的目录
- 使用cmd命令行执行如下指令
dotnet add package Microsoft.Data.Sqlite.Core
dotnet add package SQLitePCLRaw.bundle_e_sqlcipher
- 假如有看我前一个文章的进行安装的,需要卸载掉旧的引用防止程序抓的包异常,卸载命令行如下
dotnet remove package System.Data.SQLite.Core
- 执行安装之后,你会看到csproj文件内容应该是这样的
<Project Sdk="Godot.NET.Sdk/4.4.1"><PropertyGroup><TargetFramework>net8.0</TargetFramework><EnableDynamicLoading>true</EnableDynamicLoading></PropertyGroup><ItemGroup><PackageReference Include="Microsoft.Data.Sqlite.Core" Version="9.0.9" /><PackageReference Include="SQLitePCLRaw.bundle_e_sqlcipher" Version="2.1.11" /></ItemGroup>
</Project>
代码调整
注意:关于怎么引入godot-sqlite本文章不再额外缀诉,如需要请访问我前一个文章
- 同样,我们有四个文件,采取和java的jdbc一样的模式,来访问数据库
DBConnection.cs
该代码是关于sqlite的数据库连接配置代码,里面设置了加密密钥
_encryptionKey
,该值根据自己来设定,相当于访问密码
- 代码如下
using System;
using Microsoft.Data.Sqlite;
using Godot;
using System.IO;public static class DBConnection
{private static SqliteConnection _connection;private static string _dbPath = "user://test.db";public static string _encryptionKey = "ABC123";// 获取数据库连接:cite[7]public static SqliteConnection GetConnection(){if (_connection == null){string userDir = OS.GetUserDataDir();string fullPath = Path.Combine(userDir, "test.db");// 创建连接字符串var connectionString = new SqliteConnectionStringBuilder{DataSource = fullPath,Mode = SqliteOpenMode.ReadWriteCreate,Password = _encryptionKey // 添加加密密钥}.ToString();_connection = new SqliteConnection(connectionString);try{_connection.Open();InitializeTables();GD.Print("成功打开加密数据库连接");}catch (Exception e){GD.PrintErr($"打开数据库失败: {e.Message}");_connection = null;throw;}}return _connection;}// 初始化数据库表:cite[1]private static void InitializeTables(){string createTableSQL = @"CREATE TABLE IF NOT EXISTS students (id INTEGER PRIMARY KEY AUTOINCREMENT,name TEXT NOT NULL,age INTEGER NOT NULL,grade TEXT)";using (var command = new SqliteCommand(createTableSQL, _connection)){command.ExecuteNonQuery();}GD.Print("数据库表初始化成功");}// 关闭数据库连接public static void CloseConnection(){if (_connection != null){_connection.Close();_connection = null;GD.Print("数据库连接已经关闭");}}// 更改加密密钥的方法public static bool ChangeEncryptionKey(string newKey){try{using (var command = _connection.CreateCommand()){command.CommandText = $"PRAGMA rekey = '{newKey}'";command.ExecuteNonQuery();_encryptionKey = newKey;GD.Print("加密密钥已更改");return true;}}catch (Exception e){GD.PrintErr($"更改密钥失败: {e.Message}");return false;}}public static void EncryptExistingDatabase(string unencryptedPath, string encryptedPath, string key){// 连接到未加密数据库var unencryptedConnectionString = new SqliteConnectionStringBuilder{DataSource = unencryptedPath}.ToString();// 连接到新加密数据库var encryptedConnectionString = new SqliteConnectionStringBuilder{DataSource = encryptedPath,Password = key}.ToString();using (var unencryptedConn = new SqliteConnection(unencryptedConnectionString))using (var encryptedConn = new SqliteConnection(encryptedConnectionString)){unencryptedConn.Open();encryptedConn.Open();// 将未加密数据库的内容复制到加密数据库using (var command = unencryptedConn.CreateCommand()){command.CommandText = $"ATTACH DATABASE '{encryptedPath}' AS encrypted KEY '{key}'";command.ExecuteNonQuery();command.CommandText = "SELECT sqlcipher_export('encrypted')";command.ExecuteNonQuery();command.CommandText = "DETACH DATABASE encrypted";command.ExecuteNonQuery();}}// 备份原始未加密数据库File.Move(unencryptedPath, unencryptedPath + ".backup");File.Move(encryptedPath, unencryptedPath);GD.Print("数据库加密完成");}
}
Student.cs
和前一个文章一样,我因为采用了jdbc的模式访问数据库,所以有一个实体作为映射,一个实体对应一个表结构,这个是学生信息表,那么实际游戏开发可以替换成自己的玩家数据表,物品信息表等
- 代码如下
using Godot;
using System;public class Student
{public int Id { get; set; }public string Name { get; set; }public int Age { get; set; }public string Grade { get; set; }public Student() { }public Student(string name, int age, string grade){Name = name;Age = age;Grade = grade;}// 重写ToString方法便于输出public override string ToString(){return $"Student [ID: {Id}, Name: {Name}, Age: {Age}, Grade: {Grade}]";}
}
StudentDAO.cs
该文件是学生信息表的DAO层,用于操作学生信息表进行增删改查
- 代码如下
using System;
using Microsoft.Data.Sqlite;
using System.Collections.Generic;
using Godot;
using System.IO;public class StudentDAO
{private static string GetDbPath(){string userDir = OS.GetUserDataDir();return Path.Combine(userDir, "test.db");}private static string GetConnectionString(){return new SqliteConnectionStringBuilder{DataSource = GetDbPath(),Password = DBConnection._encryptionKey // 与DBConnection中相同的密钥}.ToString();}// 添加学生:cite[1]public static bool AddStudent(Student student){// string dbPath = GetDbPath();//string connectionString = $"Data Source={dbPath};Version=3;";// 使用 using 语句确保连接在使用后会被正确关闭和释放:cite[10]var connection = DBConnection.GetConnection();try{connection.Open();string sql = @"INSERT INTO students (name, age, grade) VALUES (@name, @age, @grade)";using (var command = new SqliteCommand(sql, connection)){command.Parameters.AddWithValue("@name", student.Name);command.Parameters.AddWithValue("@age", student.Age);command.Parameters.AddWithValue("@grade", student.Grade);int rowsAffected = command.ExecuteNonQuery();GD.Print($"新增学生: {student.Name}, 操作结果: {rowsAffected}");return rowsAffected > 0;}}catch (Exception e){GD.PrintErr($"添加学生失败: {e.Message}");return false;}}// 根据ID删除学生:cite[5]public static bool DeleteStudent(int id){//string dbPath = GetDbPath();//string connectionString = $"Data Source={dbPath};Version=3;";try{var connection = DBConnection.GetConnection();connection.Open();string sql = "DELETE FROM students WHERE id = @id";using (var command = new SqliteCommand(sql, connection)){command.Parameters.AddWithValue("@id", id);int rowsAffected = command.ExecuteNonQuery();GD.Print($"使用ID删除学生: {id}, 操作结果: {rowsAffected}");return rowsAffected > 0;}}catch (Exception e){GD.PrintErr($"删除学生失败: {e.Message}");return false;}}// 更新学生信息:cite[5]public static bool UpdateStudent(Student student){//string dbPath = GetDbPath();//string connectionString = $"Data Source={dbPath};Version=3;";try{var connection = DBConnection.GetConnection();connection.Open();string sql = @"UPDATE students SET name = @name, age = @age, grade = @grade WHERE id = @id";using (var command = new SqliteCommand(sql, connection)){command.Parameters.AddWithValue("@name", student.Name);command.Parameters.AddWithValue("@age", student.Age);command.Parameters.AddWithValue("@grade", student.Grade);command.Parameters.AddWithValue("@id", student.Id);int rowsAffected = command.ExecuteNonQuery();GD.Print($"更新学生信息: {student.Name}, 操作结果: {rowsAffected}");return rowsAffected > 0;}}catch (Exception e){GD.PrintErr($"更新学生信息失败: {e.Message}");return false;}}// 获取所有学生:cite[1]public static List<Student> GetAllStudents(){
/* string dbPath = GetDbPath();string connectionString = $"Data Source={dbPath};Version=3;";*/var students = new List<Student>();try{var connection = DBConnection.GetConnection();connection.Open();string sql = "SELECT * FROM students";using (var command = new SqliteCommand(sql, connection))using (var reader = command.ExecuteReader()){while (reader.Read()){students.Add(new Student{Id = Convert.ToInt32(reader["id"]),Name = reader["name"].ToString(),Age = Convert.ToInt32(reader["age"]),Grade = reader["grade"].ToString()});}}GD.Print($"已获取 {students.Count} 数量学生信息");}catch (Exception e){GD.PrintErr($"获取学生信息失败: {e.Message}");}return students;}// 根据ID查询学生:cite[7]public static Student GetStudentById(int id){
/* string dbPath = GetDbPath();string connectionString = $"Data Source={dbPath};Version=3;";*/try{var connection = DBConnection.GetConnection();connection.Open();string sql = "SELECT * FROM students WHERE id = @id";using (var command = new SqliteCommand(sql, connection)){command.Parameters.AddWithValue("@id", id);using (var reader = command.ExecuteReader()){if (reader.Read()){GD.Print($"使用ID获取学生信息: {id}");return new Student{Id = Convert.ToInt32(reader["id"]),Name = reader["name"].ToString(),Age = Convert.ToInt32(reader["age"]),Grade = reader["grade"].ToString()};}}}}catch (Exception e){GD.PrintErr($"获取学生信息失败: {e.Message}");}GD.Print($"未找到学生信息: {id}");return null;}// 根据姓名查询学生(模糊查询):cite[7]public static List<Student> GetStudentsByName(string name){
/* string dbPath = GetDbPath();string connectionString = $"Data Source={dbPath};Version=3;";*/var students = new List<Student>();try{var connection = DBConnection.GetConnection();connection.Open();string sql = "SELECT * FROM students WHERE name LIKE @name";using (var command = new SqliteCommand(sql, connection)){command.Parameters.AddWithValue("@name", $"%{name}%");using (var reader = command.ExecuteReader()){while (reader.Read()){students.Add(new Student{Id = Convert.ToInt32(reader["id"]),Name = reader["name"].ToString(),Age = Convert.ToInt32(reader["age"]),Grade = reader["grade"].ToString()});}}}GD.Print($"已获取 {students.Count} 行数据包含该名称的学生: {name}");}catch (Exception e){GD.PrintErr($"根据名称获取学生失败: {e.Message}");}return students;}
}
SQLiteBridge.cs
该文件是用于演示的执行表的增删改查的逻辑代码,使用该代码,我们需要建立一个node2d或者node节点,将代码挂载上去,并编译完成之后启动游戏
using Godot;
using System;
using System.Collections.Generic;
using SQLitePCL;public partial class SQLiteBridge : Node2D
{public override void _Ready(){Batteries_V2.Init(); // 此方法用于初始化 SQLitePCLRaw 的包提供程序GD.Print("Starting SQLite database operations...");// 测试数据库操作TestDatabaseOperations();GD.Print("All database operations completed");}private void TestDatabaseOperations(){// 1. 添加学生GD.Print("\n=== 添加学生 ===");var student1 = new Student("张三", 20, "大三");var student2 = new Student("李四", 19, "大二");var student3 = new Student("王五", 21, "大三");StudentDAO.AddStudent(student1);StudentDAO.AddStudent(student2);StudentDAO.AddStudent(student3);// 2. 查询所有学生GD.Print("\n=== 查询所有学生 ===");List<Student> allStudents = StudentDAO.GetAllStudents();foreach (var student in allStudents){GD.Print(student.ToString());}// 3. 根据ID查询学生GD.Print("\n=== 根据ID查询学生 ===");Student retrievedStudent = StudentDAO.GetStudentById(1);if (retrievedStudent != null){GD.Print($"找到学生: {retrievedStudent}");}// 4. 根据姓名查询学生GD.Print("\n=== 根据姓名查找学生 ===");List<Student> studentsByName = StudentDAO.GetStudentsByName("张");foreach (var student in studentsByName){GD.Print($"找到学生: {student}");}// 5. 更新学生信息GD.Print("\n=== 更新学生信息 ===");if (retrievedStudent != null){retrievedStudent.Age = 22;retrievedStudent.Grade = "大四";StudentDAO.UpdateStudent(retrievedStudent);}// 6. 删除学生GD.Print("\n=== 删除学生 ===");StudentDAO.DeleteStudent(2);// 7. 验证删除结果GD.Print("\n=== 验证删除结果 ===");List<Student> remainingStudents = StudentDAO.GetAllStudents();foreach (var student in remainingStudents){GD.Print($"学生: {student}");}}public override void _ExitTree(){}
}
执行结果
执行启动游戏之后,就会看到如下信息
Starting SQLite database operations...=== 添加学生 ===
数据库表初始化成功
成功打开加密数据库连接
新增学生: 张三, 操作结果: 1
新增学生: 李四, 操作结果: 1
新增学生: 王五, 操作结果: 1=== 查询所有学生 ===
已获取 3 数量学生信息
Student [ID: 1, Name: 张三, Age: 20, Grade: 大三]
Student [ID: 2, Name: 李四, Age: 19, Grade: 大二]
Student [ID: 3, Name: 王五, Age: 21, Grade: 大三]=== 根据ID查询学生 ===
使用ID获取学生信息: 1
找到学生: Student [ID: 1, Name: 张三, Age: 20, Grade: 大三]=== 根据姓名查找学生 ===
已获取 1 行数据包含该名称的学生: 张
找到学生: Student [ID: 1, Name: 张三, Age: 20, Grade: 大三]=== 更新学生信息 ===
更新学生信息: 张三, 操作结果: 1=== 删除学生 ===
使用ID删除学生: 2, 操作结果: 1=== 验证删除结果 ===
已获取 2 数量学生信息
学生: Student [ID: 1, Name: 张三, Age: 22, Grade: 大四]
学生: Student [ID: 3, Name: 王五, Age: 21, Grade: 大三]
All database operations completed
和前一个文章所说一样,我们的数据库是保存在我们的user://路径,因为这个路径是可读写的,所以我们可以在指定文件夹找到该数据库db格式文件
注意:如果需要知道数据库文件存在哪里,请查看我前一篇文章
图形化工具访问数据库
根据如上,我们执行完毕之后,就有了db数据库文件
-
但是因为经过加密了,我们原本是可以直接打开数据库看到表资料的,现在是无法直接打开的。
-
然后我们是在代码里面进行执行解密以达到可以进行表的增删改查操作,外部人员如果想简单修改破解是不容易的
-
如我使用dbeaver尝试打开加密后的db数据库
-
可以看到它是报错了
[SQLITE_NOTADB] File opened that is not a database file (file is not a database)
- 这个时候我们需要用上工具了,我有找到一个工具
DB Browser for SQLCipher3.11.2绿色便携版.7z
该工具是免安装的
解压之后找到DB Browser for SQLCipher.exe
,然后双击启动
- 然后我们点击[打开数据库],找到我们需要解密的db数据库文件
- 接着会让我们输入密码,密码就是我们前面设置的密钥,如我设置的ABC123
- 如果连接成功会看到表
- 并且我们可以右键【浏览表】可以看到我们游戏运行代码时候保存的游戏数据
结语
根据如上,我们就可以实现【godot+c#使用godot-sqlite连接数据库并加解密】,以此实现可以用在游戏存档和读档,并且防止篡改游戏数据