资讯动态

C# 2012 直连 SQLite 并加密:老项目数据库安全实战

发布时间:2026/10/23 17:50:36 来源:尧图企业网站定制
简介这份源码资源面向使用 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 PlatformTargetx86/PlatformTarget OutputPathbin\Debug\/OutputPath /PropertyGroup上面这段是项目文件里平台目标的写法PlatformTarget决定进程位数OutputPath决定输出目录。把平台固定后再把对应位数的SQLite.Interop.dll放到输出目录下通常放在bin\Debug\x86\或与 exe 同级加载问题基本就解决了。这一步是血泪经验后面避坑章节还会展开。3. 核心代码拆解连接、建表、增删改查与密码设置3.1 连接字符串与打开连接所有数据库操作的起点都是连接字符串。这个源码包把连接信息放在App.config里方便改路径和密码!-- App.config -- configuration connectionStrings add nameSqliteConn connectionStringData Source|DataDirectory|\demo.db;Version3;Password123456; providerNameSystem.Data.SQLite / /connectionStrings /configurationData Source指定库文件路径|DataDirectory|是 .NET 的占位符运行时会被替换成程序的数据目录Version3表示 SQLite 3 格式Password123456就是加密口令第一次打开不存在的库时会用这个密码创建加密库。读取配置并打开连接的代码如下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 ListUser QueryUsers(int minAge) { var list new ListUser(); 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};Version3;; 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 pss;word // 特殊字符交给 builder 转义 }; using (var conn new SQLiteConnection(builder.ToString())) { conn.Open(); }SQLiteConnectionStringBuilder的各个属性对应连接串里的键拼出来的字符串会自动加引号转义比手写字符串安全。5.3 插入中文变乱码或问号现象是插入的中文姓名查出来变成???或乱码。原因一般是建表时字段类型写成了VARCHAR之类或者连接时编码没对上。SQLite 本身对文本统一按 UTF-8 存储System.Data.SQLite默认也是 UTF-8所以更常见的原因是源文件编码或控制台输出编码问题。解决办法是建表字段统一用TEXTC# 源文件保存为 UTF-8控制台输出前设置Console.OutputEncoding Encoding.UTF8;。如果是从别的库导数据进来确认导出时就是 UTF-8。5.4 并发写入报“database is locked”现象是多线程或多进程同时写同一个库时抛SQLiteException: database is locked。原因是 SQLite 默认的锁机制在同一时刻只允许一个写者写事务没提交前其他写操作会等待超时就报错。解决办法有三条一是把连接字符串加上PoolingTrue;并合理设置busy_timeout让驱动自动重试等待二是写操作尽量短用完立刻using释放三是如果确实有高并发写需求考虑用 WAL 模式PRAGMA journal_modeWAL;它能显著改善读写并发。桌面工具一般单用户前两条就够用。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 ListT QueryT(string sql, FuncSQLiteDataReader, T map, params SQLiteParameter[] args) { var result new ListT(); 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用FuncSQLiteDataReader,T委托做行映射调用方只关心“一行怎么变成对象”不用重复写while(reader.Read())。这样改完业务代码里基本看不到SQLiteConnection了换库或加日志都只动一个地方。验证改造是否成功我习惯跑一个固定回归建一个临时库插入 100 条记录按条件查回 30 条改密后再查一次最后删库。这套动作写成单元测试或控制台自检都行关键是每次改DbHelper都跑一遍防止连接串参数改动引入回归。密码策略上我一般把密码从配置文件挪到用户登录时输入或者用 DPAPI 加密后存注册表避免明文躺在App.config里——这一点是这个 Demo 没做、但真实项目必须补的。说个自己的教训早些年我图省事把Password直接硬编码在连接字符串里提交进了代码库后来换环境部署时忘了改连了半天连不上排查了大半天才反应过来是密码不对。从那以后我每次碰 SQLite 加密都强制先跑一遍“建库—写入—改密—旧密码应失败—新密码应成功”的闭环确认加密真的生效再往下写业务。希望这个闭环习惯也能帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑