简介:这份资源面向在64位Windows平台上使用C#与VS2012进行桌面开发的程序员,重点解决SQLite数据库的调用与密码保护问题。SQLite作为轻量级嵌入式数据库无需独立服务进程,资源围绕System.Data.SQLite提供程序展开,演示连接字符串配置、建表、增删改查及密码参数设置等核心操作,适合需要为本地数据增加安全机制的初中级开发者参考。压缩包共42个文件,约2.48MB,以cs源码、config配置、dll程序集、pdb调试符号、resx资源及sln解决方案为主,另含少量exe与txt说明,结构完整可直接在VS2012中打开运行。目前已有850人学习下载。通过阅读源码,读者可掌握SQLiteConnection、SQLiteCommand、SQLiteDataReader等类的实际用法,理解连接字符串中密码参数的作用与局限,并借鉴其工程组织方式,快速将数据库读写与加密思路迁移到自己的项目中。
1. 64位C#2012调用SQLite数据库源码:一个被低估的桌面端数据方案
如果你手上有一个用 C# 2012 写的 WinForm 或控制台工具,需要本地存数据,又不想让用户装 SQL Server 那一套,SQLite 几乎是唯一解。但很多人卡在第一步:64 位环境下引用 System.Data.SQLite 报“未能加载文件或程序集”或者“试图加载格式不正确的程序”。这不是玄学,是位数没对齐。这个标题讲的就是:在 64 位目标平台上,用 C# 2012 调通 SQLite,并且给数据库文件加上密码。它解决的是桌面工具本地持久化 + 轻量加密的需求,适合做单机管理软件、采集工具、离线客户端的开发者。下面按“能跑起来 → 能加密 → 不翻车”的顺序拆。
2. 环境与引用:64 位下把 SQLite 接进 C# 2012 的最小闭环
2.1 为什么 64 位下最容易翻车的是引用方式
C# 2012 对应 .NET Framework 4.5,这个年代的 SQLite 托管库主要有两种形态:一种是纯托管的 System.Data.SQLite.dll,另一种是混合程序集,里面嵌了原生 sqlite3 的 x86/x64 二进制。混合程序集在 64 位下必须让进程位数、程序集位数、原生 DLL 位数三者一致,否则运行时直接抛 BadImageFormatException。很多人只把 System.Data.SQLite.dll 拖进引用,却忘了它旁边还有 x64 和 x86 两个子目录,结果在 32 位机器上跑得好好的,一到 64 位就崩。常见做法是:项目平台目标显式设为 x64,引用混合程序集,并把 x64 子目录下的 SQLite.Interop.dll 复制到输出目录。这一步不做,后面所有代码都是白写。
2.2 用 NuGet 还是手动引用:两条路的取舍
C# 2012 的 NuGet 支持已经可用,但版本要选对。手动引用更可控,适合离线环境。下面给出手动引用的目录结构和复制命令,这是我在多个离线项目里验证过的做法。
# 假设解压后的 SQLite 包目录结构如下 # packages/System.Data.SQLite/ # lib/System.Data.SQLite.dll # runtimes/x64/SQLite.Interop.dll # runtimes/x86/SQLite.Interop.dll # 在项目输出目录下建立 x64 和 x86 子目录 mkdir -p bin/Debug/x64 mkdir -p bin/Debug/x86 # 把对应位数的原生库复制进去 cp packages/System.Data.SQLite/runtimes/x64/SQLite.Interop.dll bin/Debug/x64/ cp packages/System.Data.SQLite/runtimes/x86/SQLite.Interop.dll bin/Debug/x86/逻辑说明:System.Data.SQLite.dll 是托管入口,SQLite.Interop.dll 是原生实现。运行时托管层会根据当前进程位数去 x64 或 x86 子目录加载对应的 Interop。参数说明:目录名必须是 x64 和 x86,不能改成 amd64 或 win64,这是库内部硬编码的查找路径。如果项目输出目录结构不对,报错信息通常是“无法加载 DLL ‘SQLite.Interop.dll’”,而不是找不到托管程序集,这个区别可以用来快速定位问题。
2.3 连接字符串与最小可运行代码
引用通了之后,连接字符串是第二个容易写错的地方。SQLite 的连接字符串不像 SQL Server 那样有 Server 和 Database,它用 Data Source 指向文件路径,用 Version 指定文件格式版本。
using System.Data.SQLite; // 连接字符串:Data Source 指向数据库文件,Version=3 表示 SQLite3 格式 string connStr = @"Data Source=.\appdata.db;Version=3;"; // 使用 using 确保连接释放,SQLite 文件锁在 Windows 上比较敏感 using (var conn = new SQLiteConnection(connStr)) { conn.Open(); // 建一张最小表,验证读写通路 string ddl = "CREATE TABLE IF NOT EXISTS t_user (" + "id INTEGER PRIMARY KEY AUTOINCREMENT," + "name TEXT NOT NULL," + "created_at TEXT DEFAULT (datetime('now','localtime'))" + ");"; using (var cmd = new SQLiteCommand(ddl, conn)) { cmd.ExecuteNonQuery(); } // 插入一条并读回,确认不是只建表不落盘 using (var cmd = new SQLiteCommand("INSERT INTO t_user(name) VALUES(@n);", conn)) { cmd.Parameters.AddWithValue("@n", "test"); cmd.ExecuteNonQuery(); } using (var cmd = new SQLiteCommand("SELECT COUNT(*) FROM t_user;", conn)) { long count = (long)cmd.ExecuteScalar(); Console.WriteLine("rows=" + count); } }逻辑说明:先建表再插入再查询,三步走完才能确认托管层、原生层、文件系统三层都通了。参数说明:Data Source 用相对路径时,基准是当前工作目录,不是 exe 所在目录,这是很多人部署后找不到数据库文件的原因。Version=3 是必须的,不写在某些版本下会默认到旧格式。AddWithValue 在这里够用,但如果字段类型严格,建议显式指定 DbType,避免 TEXT 和 INTEGER 的隐式转换问题。
3. 给 SQLite 数据库加密码:从连接字符串到密钥管理的完整链路
3.1 SQLite 加密的两种实现路径
原生 SQLite 本身不提供加密,加密能力来自扩展。主流做法有两种:一是使用带加密扩展的 System.Data.SQLite 包,连接字符串里加 Password 参数;二是用 SQLCipher 这类独立加密库。在 C# 2012 环境下,前者集成成本最低,因为 API 完全兼容,只是连接字符串多一个键值。后者需要额外引入原生库,配置更复杂。我一般会优先选带加密扩展的 System.Data.SQLite,因为代码改动最小,团队里其他人不需要重新学一套 API。
3.2 设置密码与后续连接的正确写法
设置密码不是单独一个 API,而是在首次创建数据库时通过连接字符串传入,或者在已有数据库上执行 PRAGMA 重加密。下面给出首次创建带密码数据库的写法。
using System.Data.SQLite; // 首次创建:连接字符串里直接带 Password,库文件会以加密格式写入 string connStr = @"Data Source=.\secure.db;Version=3;Password=MySecret123;"; using (var conn = new SQLiteConnection(connStr)) { conn.Open(); // 建表语句和普通库完全一致,加密对 SQL 层透明 using (var cmd = new SQLiteCommand( "CREATE TABLE IF NOT EXISTS t_secret(id INTEGER PRIMARY KEY, val TEXT);", conn)) { cmd.ExecuteNonQuery(); } using (var cmd = new SQLiteCommand( "INSERT INTO t_secret(val) VALUES(@v);", conn)) { cmd.Parameters.AddWithValue("@v", "hello"); cmd.ExecuteNonQuery(); } } // 后续连接:必须带同样的 Password,否则打开时报“文件已加密或不是数据库” string connStr2 = @"Data Source=.\secure.db;Version=3;Password=MySecret123;"; using (var conn = new SQLiteConnection(connStr2)) { conn.Open(); using (var cmd = new SQLiteCommand("SELECT val FROM t_secret LIMIT 1;", conn)) { Console.WriteLine(cmd.ExecuteScalar()); } }逻辑说明:加密对 SQL 层完全透明,建表、插入、查询的代码不需要任何改动,唯一区别是连接字符串多了 Password。参数说明:Password 的值就是密钥,一旦设定,用不带 Password 的连接字符串打开会直接失败,报错信息通常是“file is encrypted or is not a database”。这个报错和文件损坏的报错很像,排查时先确认是不是密码漏了。
3.3 已有明文库如何转成加密库
如果手上已经有一个明文数据库,不想重建,可以用 SQLite 的 ATTACH 加导出方式,或者用带加密扩展提供的 ReKey 能力。下面给出一个通用的迁移思路:新建加密库,把明文库的表结构和数据搬过去。
using System.Data.SQLite; // 打开明文库 using (var plain = new SQLiteConnection(@"Data Source=.\plain.db;Version=3;")) using (var encrypted = new SQLiteConnection(@"Data Source=.\encrypted.db;Version=3;Password=NewKey456;")) { plain.Open(); encrypted.Open(); // 在加密库中重建表结构:这里以 t_user 为例,实际项目建议遍历 sqlite_master using (var cmd = new SQLiteCommand( "CREATE TABLE IF NOT EXISTS t_user(id INTEGER PRIMARY KEY, name TEXT);", encrypted)) { cmd.ExecuteNonQuery(); } // 从明文库读,往加密库写 using (var readCmd = new SQLiteCommand("SELECT id, name FROM t_user;", plain)) using (var reader = readCmd.ExecuteReader()) using (var tran = encrypted.BeginTransaction()) { while (reader.Read()) { using (var insertCmd = new SQLiteCommand( "INSERT INTO t_user(id, name) VALUES(@id, @name);", encrypted, tran)) { insertCmd.Parameters.AddWithValue("@id", reader.GetInt64(0)); insertCmd.Parameters.AddWithValue("@name", reader.GetString(1)); insertCmd.ExecuteNonQuery(); } } tran.Commit(); } }逻辑说明:迁移的核心是“读明文、写密文”,表结构需要提前在加密库里建好。参数说明:事务包住批量插入,避免每条都刷盘导致迁移慢。如果表多,建议先查 sqlite_master 拿到所有 CREATE 语句,再逐表搬数据。注意自增主键的序列值不会自动带过来,迁移后如果需要保持 id 连续,要手动更新 sqlite_sequence。
4. 避坑与排查:64 位 SQLite 加密场景下最容易踩的五个坑
4.1 报“试图加载格式不正确的程序”
现象:项目在开发机上跑得好好的,换一台 64 位机器或者发布后直接崩,异常信息是 BadImageFormatException。原因:进程是 64 位,但加载到的 SQLite.Interop.dll 是 32 位版本,或者反过来。解决:确认项目平台目标设为 x64,确认输出目录下 x64 子目录里的 Interop 是 64 位版本。可以用 dumpbin /headers 查看 PE 头,Machine 字段是 x64 还是 x86 一目了然。
4.2 设了密码后自己都打不开
现象:第一次用带 Password 的连接字符串建库成功,第二次用同样的字符串却报“file is encrypted or is not a database”。原因:Password 参数在连接字符串里的位置或大小写被改动,或者中间有人用不带密码的工具打开过导致文件头被破坏。解决:把连接字符串固化到配置文件里,不要手拼。如果怀疑文件头坏了,用十六进制工具看前 16 个字节,加密库不是“SQLite format 3”开头。
4.3 数据库文件被锁住无法删除
现象:程序退出后想删掉 db 文件,提示“文件正在被另一进程使用”。原因:SQLiteConnection 没有 Dispose,或者有未关闭的 DataReader 持有连接。解决:所有连接、命令、读取器都用 using 包住。如果用了连接池,可以在连接字符串加 Pooling=false 临时排查,但生产环境不建议关池。
4.4 加密后写入性能明显下降
现象:同样的批量插入,加密库比明文库慢好几倍。原因:加密是对每个数据页做加解密,页越大、写入越频繁,开销越明显。解决:批量操作包事务,把默认的同步模式从 FULL 改成 NORMAL,连接字符串加 Journal Mode=WAL。这三项调完,加密带来的额外开销通常能压到可接受范围。
4.5 发布时漏拷 x64 目录
现象:开发机正常,客户机上一点查询就报“无法加载 DLL ‘SQLite.Interop.dll’”。原因:发布脚本只拷了 exe 和托管 dll,漏了 x64 子目录。解决:在项目文件里把 SQLite.Interop.dll 的复制到输出目录属性设为“始终复制”,或者用生成后事件统一拷贝。这个坑血泪经验最多,因为开发机上往往因为之前手动拷过而看不出来。
5. 进阶技巧:把加密 SQLite 做成可配置、可验证的本地存储层
走到这里,基本功能已经通了。但要让这个方案在项目里长期可用,还需要做两件事:把连接字符串和密钥从代码里挪出去,以及给加密库加一个自检入口。
先说配置化。不要把 Password 硬编码在 C# 里,反编译一眼就能看到。常见做法是放在 app.config 的 connectionStrings 节,但密码仍然明文。更稳妥的是用 DPAPI 做机器绑定加密,把密文存配置文件,运行时用 ProtectedData.Unprotect 解出真实密码。这样即使配置文件被拷走,换一台机器也解不开。
using System.Security.Cryptography; using System.Text; // 加密:把明文密码转成机器绑定的密文,存入配置 byte[] plain = Encoding.UTF8.GetBytes("MySecret123"); byte[] cipher = ProtectedData.Protect(plain, null, DataProtectionScope.LocalMachine); string stored = Convert.ToBase64String(cipher); // 解密:运行时还原 byte[] cipher2 = Convert.FromBase64String(stored); byte[] plain2 = ProtectedData.Unprotect(cipher2, null, DataProtectionScope.LocalMachine); string password = Encoding.UTF8.GetString(plain2);参数说明:DataProtectionScope.LocalMachine 表示同一台机器上任何用户都能解,适合服务类程序;如果只给当前用户用,改成 CurrentUser。注意这个 API 在 .NET Framework 4.5 里可用,不需要额外引用。
再说自检。加密库最怕的是“以为加密了,其实没有”。可以在程序启动时做一次探测:用带密码的连接打开,执行 PRAGMA quick_check,再用不带密码的连接尝试打开,确认后者失败。两步都符合预期,才认为加密生效。
// 自检:确认加密生效 bool IsEncrypted(string path, string pwd) { // 带密码能打开 using (var conn = new SQLiteConnection( string.Format(@"Data Source={0};Version=3;Password={1};", path, pwd))) { conn.Open(); using (var cmd = new SQLiteCommand("PRAGMA quick_check;", conn)) { string result = cmd.ExecuteScalar() as string; if (result != "ok") return false; } } // 不带密码打不开 try { using (var conn = new SQLiteConnection( string.Format(@"Data Source={0};Version=3;", path))) { conn.Open(); return false; // 居然打开了,说明没加密 } } catch (SQLiteException) { return true; // 预期内的失败 } }这个自检函数我一般放在安装后的首次启动里跑一次,结果写日志。如果返回 false,说明加密配置没生效,宁可让程序拒绝启动,也不要让用户以为数据被保护了。
最后说一个习惯:每次改连接字符串相关代码,先跑一遍自检,再看业务查询。顺序反了,很容易把加密问题误判成业务逻辑 bug。希望帮到你。
本文还有配套的精品资源,点击获取