
写Excel VBA调用SQL语句时最让我头疼的一件事就是密码被明晃晃写在代码里。每次把做好的xlsm工具发给同事或客户总要犹豫一下连接字符串里的数据库账号密码会不会被顺手打开VBA工程的人看到。确实会。这个事不是危言耸听我见过好几回工具发出去之后对方打开工程一看服务器地址、用户名、密码清清楚楚躺在那里连注释都不用加。今天就把我处理这个问题的思路和具体做法完整梳理一遍覆盖常见场景——自己用、内部同事用、对外发布演示版——每一种情况都有对应的隐藏或替换方案顺便把那些你以为藏着其实早就漏了的地方也一并查清楚。1. 为什么VBA里的数据库密码总是“裸奔”在代码中1.1 连接字符串的组成和暴露原理VBA操作SQL Server通常走的是ADO连接字符串大致长这样ProviderSQLOLEDB.1;Data Source192.168.1.100\SQLEXPRESS;Initial CatalogSalesDB;User IDsa;PasswordWinter2024!Key;这串东西一旦出现在VBA代码里就等于把钥匙挂在门口。问题在于大量VBA项目根本没有足够的安全意识认为“Excel文件我自己用没人看代码”等到需要把文件发出去的时候才傻眼。更麻烦的是VBA工程本身并不安全。你可以在VBE里设置“工具→VBAProject属性→保护→查看时锁定工程”填一个密码但这层保护在实际操作中非常脆弱。网上有大量工具可以在几十秒内移除VBA工程密码保护也能直接解包xlsm文件读取其中的vbaProject.bin。换句话说只要别人拿到了你的Excel文件能看到模块代码只是时间问题。1.2 常见的“假隐藏”为什么字符串变量名绕不开问题有人觉得我把密码塞进变量里比如Dim a As String : a Winter2024!Key然后在连接字符串里引用a别人就看不到了。这其实只是一种心理安慰。凡是最终要拼进连接字符串的明文密码不管中间经过多少层变量当对方在VBE里看到那一行字的时候密码还是明文的。唯一有效的思路是让密码不以完整明文的形式出现在代码文件的任何位置或者压根不使用密码认证。我见过更离谱的做法把密码放在工作表某个单元格里比如Range(A1) 密码连接时读出来用。结果就是打开Excel第一眼看到密码。所以任何存储在文件内部的“秘密”都算不上秘密。真正能用的方法要么把认证方式整体换掉要么让密码以不可直接识别的形式存在要么把密码移到代码文件之外。下面这几套方案就是沿着这三个方向展开的。2. 首选方案切换到Windows集成认证把密码从连接串里彻底拿掉2.1 Windows认证连接字符串的样子先说结论如果你的SQL Server运行在公司内网并且所有人都是Windows域账号登录电脑那直接用Windows集成认证是最干净的答案。连接字符串变成这样Sub TestConnWindowsAuth() Dim conn As Object Set conn CreateObject(ADODB.Connection) conn.ConnectionString _ ProviderSQLOLEDB.1; _ Integrated SecuritySSPI; _ Persist Security InfoFalse; _ Data Source192.168.1.100\SQLEXPRESS; _ Initial CatalogSalesDB; conn.Open Debug.Print 连接成功当前身份: conn.ConnectionString conn.Close Set conn Nothing End Sub关键点就两处Integrated SecuritySSPI和Persist Security InfoFalse。前者告诉ADO使用当前Windows登录身份去完成认证代码里不需要也不需要出现任何密码后者是防止连接成功之后ADO把密码信息缓存在连接对象里面。虽然我们没写密码但习惯上保留Persist Security InfoFalse是防后续出错。这一段代码配合前面的解释要突出一点不是“隐藏密码”而是“没有密码”。Windows认证的所有密码交换都在操作系统层面完成客户端代码里根本没有可泄露的口令。2.2 为VBA调用者配置SQL登录授权Windows认证不是代码改了就能直接用SQL Server那边要为使用这个功能的Windows用户创建对应的登录名和数据库用户。假设你的VBA文件会被DEV\zhangsan和DEV\lisi这两个域账号使用那么在SQL Server Management Studio里执行下面两段SQL或者用图形界面操作USE [master]; GO CREATE LOGIN [DEV\zhangsan] FROM WINDOWS WITH DEFAULT_DATABASE[SalesDB]; CREATE LOGIN [DEV\lisi] FROM WINDOWS WITH DEFAULT_DATABASE[SalesDB]; GO USE [SalesDB]; GO CREATE USER [DEV\zhangsan] FOR LOGIN [DEV\zhangsan]; CREATE USER [DEV\lisi] FOR LOGIN [DEV\lisi]; GO ALTER ROLE db_datareader ADD MEMBER [DEV\zhangsan]; ALTER ROLE db_datareader ADD MEMBER [DEV\lisi]; GO注意如果用户在域里登录名必须是域名\用户名如果SQL Server和客户端在同一台机器上也可以使用机器名\用户名。VBA调用时用的就是Excel当前运行所在的Windows身份所以用户必须已经登录Windows并且权限要提前分配。只读报表类的工具甚至只给db_datareader就够了不要顺手给db_owner。2.3 Windows认证方案的限制和坑这套方法好归好实际落地有几个真坑。先说最常见的一个SQL Server安装时的身份验证模式。如果安装SQL Server时选择了“仅Windows身份验证”那你直接用即可如果选择的是“混合模式(SQL Server和Windows身份验证)”没问题也可以最怕的是有些人安装时把两种都关了或设置得不彻底导致Windows认证登录时报错。安装之后可以确认一下实例右键属性→安全性→服务器身份验证确认是Windows身份验证模式或混合模式。第二个坑是双击打开Excel文件时Excel以当前用户身份运行但如果SQL Server在另一台机器上跨机认证需要Kerberos或NTLM协议在同一个域内通常没问题如果客户端和服务器不在同一个域即使能ping通Windows认证也可能失败。这种情况就别硬扛Windows认证直接用下面提到的方案。第三个坑更隐蔽许多人用VBA时顺手加了一句Debug.Print conn.ConnectionString用来调试。一旦改成Windows认证这行不会输出密码但代码习惯要保留Persist Security InfoFalse。前面提到过这句话就是给连接对象用的防止后续代码在某个属性里读取到敏感字段。第四个坑如果你只是自己偶尔用不想给SQL Server配置登录账号也建议先在本地库上把Windows认证跑通因为这段代码逻辑最简单出问题最少测试时不容易被密码问题干扰。3. 必须用SQL账号密码时用运行时拼接把密码“打散”如果Windows认证确实不可行比如数据库在云端、客户只给你一个sa之类的SQL账号那就要想办法让密码不以完整明文出现在代码里。注意区分一个现实问题我们做不到密码学级别的防破解因为VBA代码最终会被看到但我们可以做到让一眼看过去“看不到密码”。这已经能挡住90%随手打开工程查看的人。3.1 最低成本方案分拆字符串再拼起来最简单直接的做法是把一个密码拆成几段分别放在不同位置甚至用Chr()把其中几个字符转成ASCII码。这样整个代码文件里就没有一行能直接读懂的完整密码了。Function BuildConnectionString() As String Dim srv As String, db As String, uid As String Dim pwdPart1 As String, pwdPart2 As String srv 192.168.1.100\SQLEXPRESS db SalesDB uid svc_report 密码实际是 Winter2024!Key 但代码中看不见这个完整字符串 pwdPart1 Win ter pwdPart2 Chr(64) 2024 !Key Chr(64) BuildConnectionString _ ProviderSQLOLEDB.1; _ Data Source srv ; _ Initial Catalog db ; _ User ID uid ; _ Password pwdPart1 pwdPart2 ; _ Persist Security InfoFalse; End Function这类的核心是“增加阅读成本”。密码Winter2024!Key被拆成了三段普通字符串、Chr(64)、普通字符串。直接在模块里翻找的人不会一眼认出密码因为他要还原的话得自己把所有断点拼起来。对于内部工具这已经足够。但要注意Chr(64)只是一层很薄的掩蔽稍微懂VBA的人花两分钟也能拼出来。它防的是“无意间泄露”不是“有心破解”。如果你希望更稳一点继续看下面两种。3.2 用异或或Base64预先编码密码串比拆字符串更进一步是把密码先做一层编码代码里只存放编码后的密文。最常见的是Base64但VBA没有内置Base64函数要自己写或者抄一段。也可以选择一个更短的思路Xor。思路是这样写一个小工具函数把明文密码和某个密钥做异或得到一串乱码这串乱码放到正式代码里。正式代码运行时再用同样的密钥做一次异或还原出明文密码并用它连接数据库。 实际使用中只需要下面这个函数 Public Function XorDecode(ByVal cipherText As String, ByVal key As String) As String Dim i As Long Dim kLen As Long Dim result As String kLen Len(key) If kLen 0 Then Exit Function For i 1 To Len(cipherText) result result Chr(AscW(Mid$(cipherText, i, 1)) Xor AscW(Mid$(key, ((i - 1) Mod kLen) 1, 1))) Next i XorDecode result End Sub平时你在另一个临时文件里生成密文Sub GenerateCipher() Dim plain As String Dim key As String plain Winter2024!Key key my-secret-key-2024 Debug.Print XorEncode(plain, key) End Sub这里 XorEncode 和 XorDecode 逻辑一样因为异或的对称性。实际项目中你只需要把GenerateCipher生成的乱码字符串写进正式模块。正式模块里没有明文密码只有密文和密钥。写着是这样Sub OpenDBWithCipher() Dim conn As Object Dim cipher As String Dim key As String cipher …这里放生成好的乱码串… key my-secret-key-2024 Set conn CreateObject(ADODB.Connection) conn.ConnectionString _ ProviderSQLOLEDB.1;Data Source192.168.1.100\SQLEXPRESS; _ Initial CatalogSalesDB;User IDsvc_report; _ Password XorDecode(cipher, key) ;Persist Security InfoFalse; conn.Open 业务代码… conn.Close Set conn Nothing End Sub优点很明显别人在代码里搜password只能搜到函数名直接看到的字符串是一堆肉眼不可读的乱码。缺点也很清楚密钥就写在旁边有心人运行一遍这段代码用Debug.Print就能把还原后的密码盯出来。不过还是那句话它防的是“默默打开VBE看到明文”的场景已经比直接写死密码强太多了。Base64的优势是字符串更像一篇普通文本不太容易被误认为乱码导致引起好奇但VBA实现Base64要写一长段代码性价比一般。如果不想自己维护可以只保留Xor这一套够用了。3.3 更加稳妥首次运行弹窗输入密码密码只存内存如果你的工具面向的用户不多而且每次启动都需要人为确认权限那有个更干净的思路把密码留在用户的大脑里而不是代码里。实现方式是用一个用户窗体或者直接用InputBox在第一次连接时询问密码。拿到的密码放到模块级全局变量中后续连接直接从内存里取。Private m_dbPwd As String Private Function GetDbPassword() As String If m_dbPwd Then m_dbPwd InputBox(请输入数据库访问密码, 数据库连接, , True) End If GetDbPassword m_dbPwd End Function Sub OpenDB() Dim conn As Object Set conn CreateObject(ADODB.Connection) 连接字符串里不出现任何密码字面量 conn.ConnectionString _ ProviderSQLOLEDB.1;Data Source192.168.1.100\SQLEXPRESS; _ Initial CatalogSalesDB;User IDsvc_report; _ Password GetDbPassword() ;Persist Security InfoFalse; conn.Open 业务代码… conn.Close Set conn Nothing End Sub注意InputBox的第4个参数传了True含义是通过星号掩码来输入密码。但InputBox的掩码效果有限某些Office版本里它甚至不生效。如果想做得专业建议放一个UserForm里面放一个TextBox把它的PasswordChar属性设为*上面再放一个说明标签确定按钮回调里把文本框的内容写入模块级变量。这个方案的主要缺点是用户每次打开Excel可能都要输入一次密码你可以加一个“本次会话只输入一次”的变量控制。绝对不要把密码写死在工作表或模块里。采用这个思路还有一个好处即便对方拿到xlsm文件没有密码他也连不上数据库。对需要分发给陌生客户的演示版来说这是最保险的方案。4. 把密码移出VBA代码配置文件和注册表方案如果说上面的方案是“混淆”和“运行时获取”那更符合工程化习惯的做法是把密码放在VBA项目之外。这样代码文件本身干干净净密码所在的位置可以单独设置访问权限。4.1 读INI/TXT配置文件最常用、也最好理解的方法是让VBA从一个外部配置文件读取连接参数。配置文件可以放在服务器共享目录、用户AppData目录等单独控制权限的地方而不是跟Excel文件放在一起。如果是发给外部人员的文件配置文件就更不该打包进去。下面是一个从简单键值对文件读取参数的示例配置文件格式; db.ini server192.168.1.100\SQLEXPRESS databaseSalesDB uidsvc_report passwordWinter2024!KeyVBA读取函数Function GetIniValue(ByVal filePath As String, ByVal key As String) As String Dim fso As Object, f As Object Dim line As String, k As String, v As String, p As Long Set fso CreateObject(Scripting.FileSystemObject) If Not fso.FileExists(filePath) Then Exit Function Set f fso.OpenTextFile(filePath, 1) Do While Not f.AtEndOfStream line Trim$(f.ReadLine) If Left$(line, 1) ; And InStr(line, ) 0 Then p InStr(line, ) k Trim$(Left$(line, p - 1)) If LCase$(k) LCase$(key) Then v Trim$(Mid$(line, p 1)) Exit Do End If End If Loop f.Close GetIniValue v End Function连接时这样拼Sub OpenDBWithIni() Dim cfgPath As String Dim conn As Object cfgPath \\192.168.1.10\shared\config\db.ini Set conn CreateObject(ADODB.Connection) conn.ConnectionString _ ProviderSQLOLEDB.1; _ Data Source GetIniValue(cfgPath, server) ; _ Initial Catalog GetIniValue(cfgPath, database) ; _ User ID GetIniValue(cfgPath, uid) ; _ Password GetIniValue(cfgPath, password) ; _ Persist Security InfoFalse; conn.Open 业务代码… conn.Close Set conn Nothing End Sub这里有个非常关键的部署细节不要把db.ini和Excel放在同一个文件夹就直接发给别人。配置文件应该放在只有目标用户能访问的共享目录里或者在分发之前把文件里的密码清空等到目标环境再写入。有人图省事把配置文件一起放到压缩包发给客户——那密码等于还是跟着文件走了。实际操作中配置文件方案最适合内部信息系统服务器共享目录权限可控VBA只负责读谁改了配置里的密码不需要改动任何代码。4.2 注册表读写方案注册表是另一个“代码之外”的存放位置。好处是普通用户不会主动去看注册表比堆在文件目录里更不起眼。用WScript.Shell读写注册表非常简单Sub SaveDbConfig() Dim ws As Object Set ws CreateObject(WScript.Shell) ws.RegWrite HKEY_CURRENT_USER\Software\MyExcelTool\DB\Server, 192.168.1.100\SQLEXPRESS, REG_SZ ws.RegWrite HKEY_CURRENT_USER\Software\MyExcelTool\DB\Database, SalesDB, REG_SZ ws.RegWrite HKEY_CURRENT_USER\Software\MyExcelTool\DB\UID, svc_report, REG_SZ ws.RegWrite HKEY_CURRENT_USER\Software\MyExcelTool\DB\Password, Winter2024!Key, REG_SZ End Sub Function GetDbConfig(ByVal itemName As String) As String Dim ws As Object Set ws CreateObject(WScript.Shell) On Error Resume Next GetDbConfig ws.RegRead(HKEY_CURRENT_USER\Software\MyExcelTool\DB\ itemName) End Function使用起来和配置文件一样连接字符串里全部引用GetDbConfig(server)、GetDbConfig(password)。把密码放在HKEY_CURRENT_USER下意味着只有当前登录用户能读系统管理员可以设置权限比放在Excel文件里强很多。缺点也很明显换电脑就要重新写一遍注册表另外注册表对普通用户是透明的如果对方会看注册表编辑器依然能看到明文。对于公司内部固定工位、不换设备的人来说注册表方案很实用。我把第一次运行的初始化写成一个单独的宏在部署时由管理员执行一次。工具运行的时候代码里没有密码日志里也不会有。4.3 外部存储方案的选择要点配置文件和注册表这两条路线核心思路一致把“密码”从“代码”里剥离出来。选择哪个主要看你的分发范围。自己做的小工具、几台电脑使用推荐注册表不产生额外文件路径固定不容易被误删。给部门内部使用、多人访问推荐共享目录配置文件因为可以把配置文件放在某个管理共享文件夹中改密码只需要改一处工具本身不用重新发。给外部客户但客户技术能力参差推荐弹窗输入密码方案或者配置文件单独分发并明确告诉对方配置文件不要外传。不管你选哪个都要记住配置文件/注册表里如果存的是明文密码它的安全边界就限于“访问控制”和“环境信任”。配置文件放在共享目录不代表全公司都能读可以在共享目录权限里只给需要的人读写权限注册表则可以通过组策略锁住某个项的访问权限。安全级别取决于你能控制的系统层面而不是VBA代码本身。5. 除了代码密码还会从这些地方漏出来很多人改完代码觉得连接字符串里没有明文密码了就以为大功告成。实际上我在排查类似问题的时候经常发现密码从另外几条意想不到的路径泄露出去。下面这几条是我在真实项目里踩过的坑一条一条说。5.1 错误处理弹窗和立即窗口VBA连接数据库失败时有些人直接写MsgBox Err.Description。如果连接字符串里包含了密码某些OLEDB驱动在报错信息里会原样带出部分连接参数尤其是当服务器名和账号信息拼进错误文本时密码可能不会出现但也有一些驱动组件会在调试信息里打印整个连接字符串。即使密码不出现也尽量不要把底层错误信息直接抛给用户看。更隐蔽的是Debug.Print。你自己调试时会写Debug.Print conn.ConnectionString检查拼好的连接字符串。一旦这句留在代码里文件发出去之后别人打开VBE按一下CtrlG打开立即窗口再执行一次打开连接的操作完整连接串就在眼前。所以项目上线前全局搜索Debug.Print把所有调试输出清理干净。如果确实需要日志也只在日志里记录运行结果不记录连接串细节。建议的错误处理风格On Error Resume Next conn.Open If Err.Number 0 Then MsgBox 数据库连接失败请确认网络和数据库服务状态。, vbExclamation, 连接错误 真正的错误详情写到日志文件不显示给用户 LogErrorErr.Description End IfLogError是你自定义的写日志函数把Err.Description写到一个只有你自己能访问的位置这样既不向用户泄露细节又能保留排查信息。5.2 版本控制和工作表残留这是特别常见的泄露路径。某个老版本的工具代码里直接写着Password123456后来你升级成了配置文件方案新版本清理了代码。但如果你把旧文件也放在共享盘、邮件附件或者Git仓库里别人打开旧文件一样能看到。我在帮客户整理VBA工具时经常发现共享文件夹里同一个工具3个版本其中一个还是远古的明文密码版。所以你不仅要把当前版本的代码清理干净还要把旧版本文件一并处理掉或者做成无密码的演示版再存档。另外如果你用过“把工作表导出”的方式分发数据模板注意有些工具会把VBA模块一起带过去。比如复制一个Sheet到另一个工作簿如果源工作簿带了模块虽然默认不会直接复制但如果你用的是“移动或复制工作表”并且勾选了“建立副本”目标工作簿出现Sheet的同时还会出现对应的工作表和类模块。这些模块里如果有连接字符串密码也跟着走了。5.3 VBA工程“保护密码”并不是加密每次聊到“密码会不会显示”总有新人说“那我给VBA工程加上查看锁定密码不就行了。”这里必须强调VBA工程保护只是一种访问控制不是加密。没有它有它别人用工具一样能拿到模块代码。我在前面也说了网上专门有解除VBA工程密码的工具。所以工程保护可以作为防误操作的辅助手段但绝对不能作为密码保密的依据。如果你打算把文件发给同事还不想让同事看到代码最彻底的办法不是锁工程而是把工具做成加载宏xlam或者编译成Excel的加载项部署。即便如此加载宏里的VBA一览无余除了减少误操作没有防泄露作用。真正不想让代码被看到常用做法是转成Office Add-in但那个已经是另一个技术栈了不在本文讨论范围。5.4 文件分享渠道里的“二次传播”很多时候密码不是你泄露的是被看到的人转发出去的。某个工具只要内部传过一次Python群里流转一圈里面的每个细节都会被放大。所以在设计阶段你就要假设文件一定会到不相关的人手里。这也是为什么我一直建议如果你做的是给外部看的工具里面用的数据库账号最好是一个最小权限账号。即使密码真被看到了他能干的坏事也只有查几张报表破坏不了核心数据。不要把生产库的sa或管理员账号写进Excel工具里不管代码里是否做了掩藏。6. 发布前的一次体检密码残留排查流程收尾部分给你一条能直接落地的排查流程。每次准备把带VBA代码的Excel文件发出去之前按这个顺序检查一遍能把90%的密码泄露隐患扫掉。6.1 在VBE里全工程搜索敏感关键字打开VBA编辑器AltF11按CtrlF在“查找范围”里选择“当前工程”然后逐个搜索以下关键词PasswordpwdPWDUser IDUIDProviderConnectionStringPersist Security Info每次搜索结果双击跳转到命中行判断是否需要清理。这个方法最直接很多临时写死的连接字符串经不起一轮搜索。注意如果你现在使用Windows认证代码里不应该出现Password、UID这种字段至少不应该出现在连接字符串拼接处。搜索时看到Integrated SecuritySSPI才是干净的样子。6.2 导出所有模块做二次字符串扫描VBE里的搜索有一个盲区它只搜代码模块不搜用户窗体中特殊控件的属性也不搜工作簿XML里的隐藏数据。更稳妥的做法是把所有模块导出成.bas/.frm文件然后用Notepad或命令行搜索工具再扫一遍。导出方法在VBE工程资源管理器里右键模块选择“导出文件”存到一个临时文件夹。导完之后如果电脑装了PowerShell可以直接执行Select-String -Path C:\temp\export\*.bas, C:\temp\export\*.frm -Pattern password,pwd,uid,connectionstring -CaseSensitive:$false看到命中的地方逐条审查。.bas文件是纯文本任何敏感字符串都会暴露在这一步里。同时建议对二进制工作簿本身做一个简单排渣先把xlsm文件复制一份手动改名成zip用记事本或搜索工具在解压后的xl/目录里搜一遍。尤其是工作簿如果存在自定义文档属性File → Info → Properties → Advanced Properties有人会把数据库地址和账号写在“自定义属性”里搜索时也要覆盖。6.3 清理替换优先级清单从我的经验看真正的干净是分层次达成的按以下优先级动手最优先全部改用Windows集成认证代码里根本不出现账号密码概念。这条能直接解决“不要显示密码”的问题。次优先必须用SQL账号时不管混合还是别的场景优先使用弹窗输入密码密码只存在内存变量里。再其次把密码移到注册表或共享配置文件中禁止走代码字面量路径。最后坚持要写死连接串至少要拆散、编码并给SQL账号做最小权限和数据脱敏处理。还有一个容易被忽略的小习惯连接字符串拼接完成之后立刻把局部变量清空尤其是保存密码的那个字符串变量。虽然VBA的变量生命周期很短但在长时间运行的应用程序里字符串内容可能会在内存中驻留一段时间。代码结尾把变量置空或者重新赋空字符串能减少密码长时间留在内存中的概率。另外如果代码里用了InputBox拿密码拿到后用一个Boolean标志控制是否再次弹出而不是把密码写进任何工作簿属性里。最后分享一个我自己的实践习惯每次做这类带数据库连接的Excel工具我都会养成一个固定动作写完连接代码后第一轮自查不是去看能不能连上库而是CtrlF搜索Password。如果搜到任何一处无论这里的密码是不是测试库的先停下来重新设计。我自己踩过的最深刻教训是有一次给客户做报表工具数据库密码用的是客户生产库账号我把临时调试用的Debug.Print conn.ConnectionString留在了模块底部文件发出去当天就被客户的IT从Excel里找到一串生产库密码场面极其尴尬。那次之后我给自己立了两个规矩一是任何工作簿里不让出现真实生产库密码二是发布前必须按上面6.1和6.2的流程跑一遍搜索不检查完不发文件。这套流程看起来繁琐但比起密码被翻出来的风险花的那十分钟实在太划算了。