C++密码加密异常:仅需密码前两位正确即可通过验证
首先,咱们先拆解一下你遇到的诡异问题:输入密码前两位就能通过验证,这说明你的哈希验证逻辑肯定在某个环节出现了「字符串不匹配但哈希意外相等」的情况,核心问题大概率出在盐的字符串处理和哈希存储/比较的类型转换上,下面一步步分析:
1. 最可能的根源:GUID字符串的不一致与截断
你存储密码时用了 Sysutils::GUIDToString(gSalt),这个函数在C++Builder里默认会生成带大括号的GUID字符串(比如 {12345678-1234-1234-1234-1234567890AB}),长度是38个字符。但你紧接着用了 CAST(...) AS NVARCHAR(36),这会直接把38字符的字符串截断成36位——也就是去掉最后一个大括号,变成 {12345678-1234-1234-1234-1234567890AB(少了结尾的 })。
而验证时,你从数据库取的 PasswordSalt 是 uniqueidentifier 类型,用 AsVariant 会返回不带大括号的36位GUID字符串(比如 12345678-1234-1234-1234-1234567890AB)。这就导致:
- 存储时拼接的是:
正确密码 + 带{且缺}的36位盐 - 验证时拼接的是:
输入密码 + 不带{}的36位盐
理论上这两个拼接字符串完全不同,哈希不该相等,但你遇到了意外匹配——这大概率是因为第二个问题:
2. PasswordHash字段类型错误导致的隐式转换问题
你把 HASHBYTES('SHA2_512') 的结果(varbinary(64) 类型的二进制哈希)直接存入了 nvarchar(MAX) 字段。SQL Server会自动把二进制数据隐式转换成Unicode字符串存储,而验证时又要把这个字符串再转成二进制和新计算的哈希比较。这个双向转换过程中,极有可能出现编码错乱,导致不同的二进制哈希被当成相同的字符串存储/比较,最终出现诡异的匹配结果。
3. 额外风险:SQL字符串拼接的隐患
你直接用字符串拼接生成SQL语句,不仅有严重的SQL注入风险,还可能因为字符串编码(比如是否加 N 前缀)、特殊字符截断等问题,进一步加剧哈希计算的不一致性。
解决步骤
第一步:修复盐的处理逻辑
统一盐的字符串表示,避免截断和格式不一致:
// 存储密码时,直接用数据库里的uniqueidentifier类型转成标准36位字符串 // 建议用参数化查询,避免字符串拼接问题 asSQLCommand = "UPDATE YCR_User_Names SET " "PasswordHash = HASHBYTES('SHA2_512', N'" + editPassword->Text + "' + CAST(@Salt AS NVARCHAR(36))), " "PasswordSalt = @Salt " "WHERE ID = " + sqlGetUser->FieldByName("ID")->AsInteger; // 给SQLCommand添加参数@Salt,类型设为uniqueidentifier,值传入gSalt
验证时直接用数据库中的 PasswordSalt 字段转换,不要依赖前端的字符串处理:
asSQLCommand = "SELECT * FROM YCR_User_Names " "WHERE PasswordHash = HASHBYTES('SHA2_512', N'" + editPassword->Text + "' + CAST(PasswordSalt AS NVARCHAR(36))) " "AND ID = " + sqlGetUser->FieldByName("ID")->AsInteger;
注意:所有字符串前加 N 前缀,确保是Unicode字符串,避免varchar/nvarchar的隐式转换。
第二步:修改PasswordHash字段类型
把 PasswordHash 从 nvarchar(MAX) 改成 varbinary(64)——SHA2_512的哈希结果正好是64字节,用二进制类型存储最准确,彻底避免类型转换的坑。
第三步:改用参数化查询(强烈推荐)
彻底放弃字符串拼接生成SQL,用参数传递密码和盐,比如:
// 存储示例 TADOCommand* cmd = new TADOCommand(this); cmd->Connection = yourDBConnection; cmd->CommandText = "UPDATE YCR_User_Names SET PasswordHash = HASHBYTES('SHA2_512', N@Password + CAST(@Salt AS NVARCHAR(36))), PasswordSalt = @Salt WHERE ID = @UserID"; cmd->Parameters->CreateParameter("@Password", ftString, pdInput, 255, editPassword->Text); cmd->Parameters->CreateParameter("@Salt", ftGuid, pdInput, 0, gSalt); cmd->Parameters->CreateParameter("@UserID", ftInteger, pdInput, 0, sqlGetUser->FieldByName("ID")->AsInteger); cmd->Execute();
这样既避免了SQL注入,又彻底解决了字符串转换和截断的问题。
内容的提问来源于stack exchange,提问作者Derrick Hobson

