☰
C# 2012 直连 SQLite 并加密:老项目数据库安全实战
2026/10/23 17:50:13 网站建设 项目流程

简介:这份源码资源面向使用 VS2012 与 C# 2012 开发 64 位 Windows 桌面应用的开发者,聚焦 SQLite 嵌入式数据库的调用与安全实践,解决轻量级数据存储及连接密码设置的问题。资源包共 42 个文件,约 2.48MB,以 cs 源码、config 配置、resx 资源、csproj 与 sln 工程文件为主,另含 dll、exe 及 pdb 调试文件,构成可直接编译运行的完整解决方案。已有 851 人学习下载,适合初、中级开发者对照练习。读者可从中获得 SQLiteConnection 连接字符串配置、建表与增删改查的完整代码示例,以及通过密码参数实现数据库加密保护的实现思路,并借助工程结构理解 64 位环境下 System.Data.SQLite 的引用与部署方式,为实际项目中的数据管理模块提供可复用的参考。

1. 一个老项目里最实用的数据库组合:C# 2012 直连 SQLite 并给库加密码

接手一个 2012 年前后的桌面工具维护任务时,最怕看到的就是“数据库文件裸奔”——.db文件拷走就能用任意工具打开,客户资料一览无余。这个源码包解决的正是这个痛点:在 VS2012 / C# 2012 环境下,用原生 ADO.NET 方式连接 SQLite,建库、建表、增删改查一条龙,并且支持给数据库文件设置密码。它适合两类人:一是手上还有 .NET Framework 4.0/4.5 老项目要维护的开发者,二是想找一个不依赖 ORM、看得见 SQL 语句的最小可运行范例的新手。整套代码没有花哨的架构,就是窗体 + 连接字符串 + 命令对象,跑通之后你能清楚看到每一步数据是怎么落盘的。

2. 技术选型与工程结构:为什么是 System.Data.SQLite 而不是别的

2.1 选型逻辑:轻量、免安装、可加密

SQLite 本身是嵌入式的,不需要单独装数据库服务,一个文件就是一个库,这对桌面工具来说几乎是刚需。C# 侧访问 SQLite 常见有三条路:一是官方维护的System.Data.SQLite(也叫 SQLite.NET provider),二是微软的Microsoft.Data.Sqlite,三是各种 ORM 封装。这个源码包走的是第一条,原因很实际:System.Data.SQLite在 .NET Framework 时代成熟度最高,直接实现DbConnection、DbCommand这套 ADO.NET 接口,写法和你连 SQL Server 几乎一样,学习成本低;更关键的是它自带加密扩展,设置密码只需要在连接字符串里加一个Password参数,不用自己写加解密逻辑。

提示:Microsoft.Data.Sqlite在新版 .NET 上是主流,但它默认不带加密能力,老项目迁移时这一点要提前确认。

选型定了,接下来要理解它和普通 ADO.NET 的差别其实很小:你依然用SQLiteConnection、SQLiteCommand、SQLiteDataReader,只是命名空间从System.Data.SqlClient换成了System.Data.SQLite。这意味着任何熟悉 ADO.NET 的人几乎零门槛上手,这也是这个源码包适合当教学范例的原因。

2.2 工程目录与依赖文件

一个典型的 VS2012 解决方案里,这个项目大致是下面这个结构。不同人拿到的压缩包可能略有出入,但核心文件跑不掉:

SqliteDemo/ ├── SqliteDemo.sln # VS2012 解决方案文件 ├── SqliteDemo/ │ ├── Program.cs # 控制台入口(或 Form1.cs 窗体入口) │ ├── DbHelper.cs # 数据库操作封装类 │ ├── App.config # 连接字符串配置 │ ├── Properties/ │ │ └── AssemblyInfo.cs │ └── SqliteDemo.csproj ├── lib/ │ ├── System.Data.SQLite.dll # 托管程序集 │ └── SQLite.Interop.dll # 原生互操作库(x86/x64 各一份) └── data/ └── demo.db # 运行时生成的数据库文件

这里有两个文件必须重点盯:System.Data.SQLite.dll是托管层,SQLite.Interop.dll是原生层。很多人第一次跑就报“无法加载 DLL SQLite.Interop.dll”,八成就是原生库没放对位置或者位数不匹配。VS2012 默认编译平台是 AnyCPU,而原生库分 x86 和 x64,必须让两者对上。

2.3 引用配置与平台目标

在 VS2012 里右键项目 → 添加引用 → 浏览到lib/System.Data.SQLite.dll。然后打开项目属性 → 生成 → 目标平台,建议直接固定成x86或x64,不要留 AnyCPU。原因是 AnyCPU 在 64 位系统上会以 64 位进程运行,此时它会去找 x64 的SQLite.Interop.dll,如果你只拷了 x86 版本,运行时就会抛BadImageFormatException或找不到模块。

<!-- SqliteDemo.csproj 中与平台相关的关键片段 --> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x86' "> <PlatformTarget>x86</PlatformTarget> <OutputPath>bin\Debug\</OutputPath> </PropertyGroup>

上面这段是项目文件里平台目标的写法,PlatformTarget决定进程位数,OutputPath决定输出目录。把平台固定后,再把对应位数的SQLite.Interop.dll放到输出目录下(通常放在bin\Debug\x86\或与 exe 同级),加载问题基本就解决了。这一步是血泪经验,后面避坑章节还会展开。

3. 核心代码拆解:连接、建表、增删改查与密码设置

3.1 连接字符串与打开连接

所有数据库操作的起点都是连接字符串。这个源码包把连接信息放在App.config里,方便改路径和密码:

<!-- App.config --> <configuration> <connectionStrings> <add name="SqliteConn" connectionString="Data Source=|DataDirectory|\demo.db;Version=3;Password=123456;" providerName="System.Data.SQLite" /> </connectionStrings> </configuration>

Data Source指定库文件路径,|DataDirectory|是 .NET 的占位符,运行时会被替换成程序的数据目录;Version=3表示 SQLite 3 格式;Password=123456就是加密口令,第一次打开不存在的库时会用这个密码创建加密库。读取配置并打开连接的代码如下:

using System.Data.SQLite; using System.Configuration; public static SQLiteConnection GetConnection() { // 从 App.config 读取连接字符串 string connStr = ConfigurationManager .ConnectionStrings["SqliteConn"].ConnectionString; var conn = new SQLiteConnection(connStr); conn.Open(); // 打开时若库不存在则按连接串参数创建 return conn; }

ConfigurationManager.ConnectionStrings按名字取配置,SQLiteConnection构造后调用Open()才真正建立连接。注意Password参数只在建库或打开加密库时生效,如果库已经存在且密码不对,Open()会直接抛SQLiteException,错误信息里通常带“file is encrypted or is not a database”,这就是密码校验失败的表现。

3.2 建表与参数化插入

建表和插入都通过SQLiteCommand执行。下面这段是封装类里最常见的两个方法:

public static void CreateUserTable() { using (var conn = GetConnection()) using (var cmd = conn.CreateCommand()) { // IF NOT EXISTS 保证重复执行不报错 cmd.CommandText = @" CREATE TABLE IF NOT EXISTS Users ( Id INTEGER PRIMARY KEY AUTOINCREMENT, Name TEXT NOT NULL, Age INTEGER, Created TEXT );"; cmd.ExecuteNonQuery(); } } public static int InsertUser(string name, int age) { using (var conn = GetConnection()) using (var cmd = conn.CreateCommand()) { // 用参数占位,避免拼接 SQL 带来的注入风险 cmd.CommandText = "INSERT INTO Users(Name, Age, Created) VALUES(@name, @age, @created);"; cmd.Parameters.AddWithValue("@name", name); cmd.Parameters.AddWithValue("@age", age); cmd.Parameters.AddWithValue("@created", DateTime.Now.ToString("s")); cmd.ExecuteNonQuery(); // 取刚插入行的自增主键 cmd.CommandText = "SELECT last_insert_rowid();"; return Convert.ToInt32(cmd.ExecuteScalar()); } }

ExecuteNonQuery()用于建表、插入、更新、删除这类不返回结果集的操作,返回受影响行数。AddWithValue把参数绑定到@name这类占位符上,SQLite 驱动会负责转义,这是防注入的标准做法,别图省事用字符串拼接。last_insert_rowid()是 SQLite 的内置函数,返回当前连接最后一次插入的 rowid,配合ExecuteScalar()就能拿到自增主键。

3.3 查询与读取结果集

查询用ExecuteReader()拿到SQLiteDataReader,逐行读取:

public static List<User> QueryUsers(int minAge) { var list = new List<User>(); using (var conn = GetConnection()) using (var cmd = conn.CreateCommand()) { cmd.CommandText = "SELECT Id, Name, Age FROM Users WHERE Age >= @age ORDER BY Id;"; cmd.Parameters.AddWithValue("@age", minAge); using (var reader = cmd.ExecuteReader()) { while (reader.Read()) { list.Add(new User { Id = reader.GetInt32(0), // 按列序号取,性能略好 Name = reader.GetString(1), Age = reader.GetInt32(2) }); } } } return list; }

reader.Read()每调用一次前进一行,返回false表示读完。GetInt32(0)按列序号取值,比按列名GetOrdinal再取要快一点,但可读性差,列顺序变了容易翻车,团队里一般约定按列名取更稳妥。using包住 reader 和 command 是为了及时释放底层句柄,SQLite 对连接和语句句柄比较敏感,忘了释放在高频操作下会出问题。

3.4 设置与修改数据库密码

密码设置分两种场景。第一种是建库时就在连接字符串里带Password,上面已经演示。第二种是给一个已经存在的明文库补上密码,这时要用ChangePassword方法:

public static void SetPassword(string dbPath, string newPassword) { // 先用无密码连接打开明文库 string connStr = $"Data Source={dbPath};Version=3;"; using (var conn = new SQLiteConnection(connStr)) { conn.Open(); // 给当前库设置新密码 conn.ChangePassword(newPassword); } }

ChangePassword是System.Data.SQLite提供的扩展方法,内部会重新加密整个库文件。执行成功后,再用旧的无密码连接串就打不开了,必须带上Password=新密码。这里有个容易忽略的点:ChangePassword对已经加密的库也能用,传入新密码即可完成改密,但传空字符串则是清除密码。改密操作会重写整个文件,库大的时候要留出足够磁盘空间,别在空间紧张的机器上操作。

4. 从零跑起来:环境准备、编译与首次运行

4.1 环境与依赖清单

跑这个项目不需要装数据库服务,但下面几样得备齐:

项目要求说明
开发工具Visual Studio 2012更高版本可打开但需重定向
运行时.NET Framework 4.0 及以上与 VS2012 默认目标一致
数据库驱动System.Data.SQLite.dll源码包 lib 目录自带
原生库SQLite.Interop.dll分 x86/x64,按平台选
操作系统Windows 7 及以上桌面工具常见环境

如果手上没有 VS2012,用 VS2019/2022 打开.sln时它会提示重定向项目,点确定即可,代码本身是兼容的,只是目标框架可能被升到 4.7.2 之类,问题不大。

4.2 编译与放置原生库

打开解决方案后先做三件事:确认引用里System.Data.SQLite没有黄色感叹号;把目标平台固定为 x86;把SQLite.Interop.dll拷到输出目录。一个省事的做法是在项目文件里加生成后事件,自动把原生库复制过去:

:: 生成后事件命令行(项目属性 → 生成事件) xcopy /Y "$(ProjectDir)lib\x86\SQLite.Interop.dll" "$(TargetDir)"

$(ProjectDir)是项目目录,$(TargetDir)是输出目录,/Y表示覆盖不提示。这样每次编译都会把正确的原生库放到 exe 旁边,省得手动拷来拷去。如果你用的是 x64 平台,把路径里的x86换成x64即可。

4.3 首次运行与验证

编译通过后直接 F5 运行。程序第一次启动时会在数据目录下创建demo.db,因为连接字符串带了Password,这个库一出生就是加密的。验证是否真的加密,可以拿一个普通的 SQLite 查看工具去打开它,正常情况下会提示无法识别或需要密码,这就说明加密生效了。

运行后建议按这个顺序自测一遍:先插入两条用户记录,再按年龄条件查询,确认结果条数和内容对得上;然后关掉程序重新打开,数据还在,说明落盘没问题;最后用SetPassword改一次密码,再用旧密码连接,应该报错,用新密码连接正常。这一圈走下来,连接、建表、增删改查、加密四个环节就都验证过了。

5. 避坑与排查:那些第一次跑必踩的坑

5.1 报“无法加载 DLL SQLite.Interop.dll”

现象是程序一启动、第一次Open()就抛异常,提示找不到SQLite.Interop.dll或其依赖项。原因通常是原生库没被复制到输出目录,或者位数和进程不匹配——32 位进程加载了 64 位 DLL,反之亦然。解决办法是先确认项目平台目标(x86/x64),再把对应位数的SQLite.Interop.dll放到 exe 同级目录,用 4.2 节的生成后事件自动复制最稳。排查时可以用任务管理器看进程是不是带*32标记,快速判断当前进程位数。

5.2 密码对了却报“file is encrypted or is not a database”

现象是连接字符串里明明写了正确密码,Open()还是报这个错。原因多半是库文件在设置密码之前就已经被别的工具以明文方式打开过,或者密码里含有分号、等号这类连接字符串分隔符,导致解析时被截断。解决办法是密码尽量用字母数字组合;如果必须用特殊字符,考虑用SQLiteConnectionStringBuilder来构造连接串,它会自动处理转义:

var builder = new SQLiteConnectionStringBuilder { DataSource = @"data\demo.db", Version = 3, Password = "p@ss;word" // 特殊字符交给 builder 转义 }; using (var conn = new SQLiteConnection(builder.ToString())) { conn.Open(); }

SQLiteConnectionStringBuilder的各个属性对应连接串里的键,拼出来的字符串会自动加引号转义,比手写字符串安全。

5.3 插入中文变乱码或问号

现象是插入的中文姓名,查出来变成???或乱码。原因一般是建表时字段类型写成了VARCHAR之类,或者连接时编码没对上。SQLite 本身对文本统一按 UTF-8 存储,System.Data.SQLite默认也是 UTF-8,所以更常见的原因是源文件编码或控制台输出编码问题。解决办法是建表字段统一用TEXT,C# 源文件保存为 UTF-8,控制台输出前设置Console.OutputEncoding = Encoding.UTF8;。如果是从别的库导数据进来,确认导出时就是 UTF-8。

5.4 并发写入报“database is locked”

现象是多线程或多进程同时写同一个库时,抛SQLiteException: database is locked。原因是 SQLite 默认的锁机制在同一时刻只允许一个写者,写事务没提交前其他写操作会等待,超时就报错。解决办法有三条:一是把连接字符串加上Pooling=True;并合理设置busy_timeout,让驱动自动重试等待;二是写操作尽量短,用完立刻using释放;三是如果确实有高并发写需求,考虑用 WAL 模式(PRAGMA journal_mode=WAL;),它能显著改善读写并发。桌面工具一般单用户,前两条就够用。

5.5 改密后旧连接仍能访问

现象是调用了ChangePassword,但程序里已经打开的旧连接还能继续读写。原因是密码变更只影响新建立的连接,已打开的连接持有的是旧的文件句柄和密钥。解决办法是改密后主动关闭所有旧连接,再重新用新密码打开。工程上一般把改密做成一个独立操作,改完提示用户重启程序或重新登录,避免新旧连接混用导致数据状态不一致。

6. 二次开发改造:把 Demo 变成能进项目的 DbHelper

拿到这个源码包,直接拿去用肯定不够,它更像一个“能跑的最小骨架”。真正落地时我一般会做三件事:把连接管理收敛、把异常处理补上、把密码策略做成可配置。下面这个DbHelper是我在这个骨架基础上改出来的常用形态:

public sealed class DbHelper { private readonly string _connStr; public DbHelper(string dbPath, string password) { var builder = new SQLiteConnectionStringBuilder { DataSource = dbPath, Version = 3, Password = password, Pooling = true, FailIfMissing = false // 库不存在时自动创建 }; _connStr = builder.ToString(); } // 统一执行入口:传入 SQL 和参数,返回受影响行数 public int Execute(string sql, params SQLiteParameter[] args) { using (var conn = new SQLiteConnection(_connStr)) using (var cmd = new SQLiteCommand(sql, conn)) { if (args.Length > 0) cmd.Parameters.AddRange(args); conn.Open(); return cmd.ExecuteNonQuery(); } } // 查询入口:用委托把 reader 映射成实体,避免每个查询都写一遍循环 public List<T> Query<T>(string sql, Func<SQLiteDataReader, T> map, params SQLiteParameter[] args) { var result = new List<T>(); using (var conn = new SQLiteConnection(_connStr)) using (var cmd = new SQLiteCommand(sql, conn)) { if (args.Length > 0) cmd.Parameters.AddRange(args); conn.Open(); using (var reader = cmd.ExecuteReader()) while (reader.Read()) result.Add(map(reader)); } return result; } }

这个改造的关键点在于:连接字符串用SQLiteConnectionStringBuilder统一构造,密码、池化、缺库自动创建都变成构造参数;Execute和Query两个泛化入口把“打开—执行—释放”的样板代码收掉;Query用Func<SQLiteDataReader,T>委托做行映射,调用方只关心“一行怎么变成对象”,不用重复写while(reader.Read())。这样改完,业务代码里基本看不到SQLiteConnection了,换库或加日志都只动一个地方。

验证改造是否成功,我习惯跑一个固定回归:建一个临时库,插入 100 条记录,按条件查回 30 条,改密后再查一次,最后删库。这套动作写成单元测试或控制台自检都行,关键是每次改DbHelper都跑一遍,防止连接串参数改动引入回归。密码策略上,我一般把密码从配置文件挪到用户登录时输入,或者用 DPAPI 加密后存注册表,避免明文躺在App.config里——这一点是这个 Demo 没做、但真实项目必须补的。

说个自己的教训:早些年我图省事,把Password直接硬编码在连接字符串里提交进了代码库,后来换环境部署时忘了改,连了半天连不上,排查了大半天才反应过来是密码不对。从那以后我每次碰 SQLite 加密,都强制先跑一遍“建库—写入—改密—旧密码应失败—新密码应成功”的闭环,确认加密真的生效再往下写业务。希望这个闭环习惯也能帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询