OCCI 连接失败并报 ORA-01017:用户名或密码无效
结论
ORA-01017 表示数据库拒绝了本次认证请求,不能直接说明源码中的用户名或密码有误。SQL*Plus 和 ODBC 可以连接,而 OCCI 在 19c、21c 环境中都失败,建议优先检查两个方面:
- OCCI 程序运行时加载了另一套 Oracle Client DLL,导致
occi.h、oraocci19.lib、oraocci19.dll、oci.dll或安全组件来自不同的 Oracle Home。 - OCCI 连接到了其他数据库服务或 PDB,而对应容器中没有相同的用户或密码验证器。
OCI_LIB_DIR 和 OCI_INC_DIR 通常只影响编译和链接,不能决定 Windows 运行时加载哪个 oci.dll。即使 ORACLE_HOME 设置正确,只要应用程序目录或 PATH 中还有另一套 Oracle DLL,Windows 就可能先加载错误的版本。
当前凭据均为 ASCII 字符,AL32UTF8 通常不会引发本例中的认证失败。通过普通 TCP 使用用户名和密码认证也不需要 Oracle Wallet。
先确认 OCCI 实际加载了哪套 DLL
在 Developer Command Prompt 或 PowerShell 中执行:
where.exe sqlplus
where.exe oci.dll
where.exe oraocci19.dll
如果返回了多个路径,不要只检查第一项。可以使用 Process Explorer、ListDLLs 或 Visual Studio 的 Modules 窗口查看进程实际加载的模块,重点确认以下文件:
oraocci19.dll
oci.dll
orannzsbb19.dll
这些 DLL 应来自同一套 Oracle Home,并与编译时使用的头文件和 .lib 文件匹配。应用程序与 Oracle Client 的位数也必须一致,例如均为 x64。
还可以在程序中直接打印已加载 DLL 的路径:
#include <windows.h>
#include <iostream>
void printModulePath(const wchar_t* moduleName)
{
HMODULE module = GetModuleHandleW(moduleName);
if (module == nullptr) {
std::wcerr << moduleName << L" is not loaded" << std::endl;
return;
}
wchar_t path[32768] = {};
DWORD length = GetModuleFileNameW(
module,
path,
static_cast<DWORD>(std::size(path))
);
if (length != 0) {
std::wcout << moduleName << L": " << path << std::endl;
}
}
在 createEnvironment() 之后调用:
printModulePath(L"oraocci19.dll");
printModulePath(L"oci.dll");
printModulePath(L"orannzsbb19.dll");
如果这些文件来自不同目录,先清理应用程序目录中的旧 DLL,再调整 PATH。不要将其他 Oracle 安装目录中的单个 DLL 复制到可执行文件旁边。OCCI 和 OCI 应作为一整套客户端组件使用。
修改环境后,需要重新启动 Visual Studio、命令行和应用程序。已经启动的进程不会自动读取新的环境变量。
用同一套 Oracle Client 进行对照测试
只有 SQLPlus 与 OCCI 加载同一套客户端组件时,两者的连接结果才适合直接比较。可以显式启动目标 Oracle Home 下的 SQLPlus:
$env:PATH = "C:\oracle\product\19c\dbhome_1\bin;" + $env:PATH
& "C:\oracle\product\19c\dbhome_1\bin\sqlplus.exe" "TESTING@//localhost:1521/ORCL"
这里没有把密码写入命令参数。SQL*Plus 会提示输入密码,避免密码出现在命令历史或进程参数中。
如果这套 SQLPlus 也返回 ORA-01017,问题应在该 Oracle Client、认证协议或目标数据库,而非 OCCI C++ 接口本身。如果 SQLPlus 可以连接,再比较它与 OCCI 进程加载的 DLL,以及两边使用的 TNS_ADMIN、NLS_LANG 和 sqlnet.ora。
确认连接的是同一个数据库和 PDB
Easy Connect 字符串的最后一段是 service name:
//localhost:1521/ORCL
它不是用户名,也不一定与数据库 SID 相同。在 CDB/PDB 环境中,同一个监听器可以根据不同的 service name 将连接路由到不同容器。如果连接到了错误的 PDB,而该 PDB 中不存在对应的本地用户,也可能收到 ORA-01017。
在可以成功连接的 SQL*Plus 会话中查询实际目标:
SELECT
SYS_CONTEXT('USERENV', 'DB_NAME') AS db_name,
SYS_CONTEXT('USERENV', 'SERVICE_NAME') AS service_name,
SYS_CONTEXT('USERENV', 'CON_NAME') AS container_name
FROM dual;
然后使用有权限的管理账号,在对应容器中检查用户:
SHOW CON_NAME;
SELECT username, account_status, password_versions
FROM dba_users
WHERE username = 'TESTING';
如果使用多租户数据库,还需要确认服务对应的 PDB。可查询的视图和所需权限取决于当前容器及账号权限。例如,管理账号可以在合适的容器中查看 CDB_SERVICES。
可以分别测试 TNS alias 和 Easy Connect,每次只改一个变量:
std::string connStr = "ORCL";
或者:
std::string connStr = "//localhost:1521/ORCL";
使用 "ORCL" 时,TNS_ADMIN 和 tnsnames.ora 会参与名称解析。使用完整的 Easy Connect 字符串时,tnsnames.ora 通常不会决定连接目标。
检查密码验证器和认证策略
不同版本的客户端可能使用不同的认证协议。服务器端的 SQLNET.ALLOWED_LOGON_VERSION_SERVER、客户端的 SQLNET.ALLOWED_LOGON_VERSION_CLIENT,以及用户现有的 password verifier 都可能影响认证结果。
先查询:
SELECT username, password_versions
FROM dba_users
WHERE username = 'TESTING';
如果账号只保留旧式 password verifier,而 OCCI 加载了另一套客户端,或读取了不同的 sqlnet.ora,其认证行为可能与 SQL*Plus、ODBC 不同。协议不兼容时通常会返回其他 Oracle 错误,但仅凭错误号还不能排除验证器问题。
可以对非生产测试账号重新设置密码,让数据库根据当前配置重新生成验证器:
ALTER USER TESTING IDENTIFIED BY "TESTING";
不要仅为排查问题而直接修改生产账号密码。修改前应确认密码策略、应用依赖和回滚方案。题目中已经出现“新建用户仍然失败”的情况,所以这一项的排查优先级低于 DLL 混用和服务路由错误。
使用更完整的 OCCI 测试代码
现有连接代码没有明显的认证参数错误。测试时可以输出数值错误码,并确保连接失败后仍能释放 Environment:
#include <occi.h>
#include <iostream>
#include <string>
using oracle::occi::Connection;
using oracle::occi::Environment;
using oracle::occi::SQLException;
int main()
{
const std::string user = "TESTING";
const std::string pass = "TESTING";
const std::string connStr = "//localhost:1521/ORCL";
Environment* env = nullptr;
Connection* conn = nullptr;
int result = 0;
try {
env = Environment::createEnvironment(Environment::DEFAULT);
conn = env->createConnection(user, pass, connStr);
std::cout << "Connected to Oracle Database!" << std::endl;
}
catch (const SQLException& e) {
std::cerr
<< "Oracle error code: " << e.getErrorCode() << '\n'
<< "Oracle error message: " << e.getMessage()
<< std::endl;
result = 1;
}
if (env != nullptr && conn != nullptr) {
env->terminateConnection(conn);
}
if (env != nullptr) {
Environment::terminateEnvironment(env);
}
return result;
}
如果用户名或密码来自配置文件,还要检查字符串长度和十六进制字节,排除 BOM、换行、尾随空格或错误转码。当前示例直接使用 "TESTING" 字面量,出现这些问题的可能性较低。
使用引号创建的 Oracle 用户名和密码区分大小写。例如,"Testing" 与 TESTING 不是同一个用户名。管理工具将用户名显示为大写,并不能证明该账号创建时没有使用引号。
OCCI 是否有 SQLDriverConnect 的等效接口
OCCI 没有与 ODBC SQLDriverConnect 完全等价的交互式接口。
普通连接使用:
env->createConnection(user, pass, connStr);
这是 OCCI 传入用户名、密码和连接标识的标准入口。OCCI 不使用 ODBC connection string,也没有 DSN 属性对话框。
底层连接过程由 OCI 完成,大致包括服务器附加和会话认证。如果需要确定失败发生在哪个 OCI 阶段,可以编写最小 OCI 测试,分别观察 OCIServerAttach 和 OCISessionBegin 的返回结果。
OCCI 已明确返回 ORA-01017 时,通常意味着网络连接和服务定位已经完成,错误发生在会话认证阶段。此时应检查运行时客户端、目标容器和服务器认证日志,无需继续寻找其他连接 API。
UTF-8 是否需要额外设置
示例使用 TESTING/TESTING,不需要额外增加 UTF-8 flag。这些字符都属于 ASCII,在 UTF-8、AL32UTF8 和常见单字节字符集中具有相同的字节值。
如果实际用户名或密码包含非 ASCII 字符,需要确认:
- C++ 字符串中的字节采用预期编码。
- Oracle Client 的客户端字符集配置与字符串编码一致。
- 源文件与 MSVC 的执行字符集一致,必要时使用
/utf-8。 NLS_LANG等客户端设置没有将输入解释为其他字符集。
如果确实需要将 AL32UTF8 设为客户端字符集,可以在启动程序前设置:
$env:NLS_LANG = "AMERICAN_AMERICA.AL32UTF8"
部分 OCCI 版本还提供了创建环境时指定字符集的重载,具体函数签名应以当前安装版本的 occi.h 和对应文档为准。对于 ASCII 凭据,不要随意切换字符集,以免引入新的排查变量。
Oracle Wallet 是否相关
以下普通 TCP 连接不需要 Wallet:
//localhost:1521/ORCL
Wallet 主要用于以下场景:
- 配置 TCPS 连接所需的证书和信任关系。
- 通过 Secure External Password Store 保存数据库凭据。
使用 TCPS 或外部密码存储时,需要在 sqlnet.ora 中配置对应的 Wallet 位置,并使用匹配的 TNS alias 或 connect descriptor。外部密码存储还要求按照相应的认证方式调用 OCCI。传入普通用户名和密码时,不能同时期待 Wallet 替换这些凭据。
Wallet 配置错误通常会伴随证书、网络或外部凭据相关错误,也无法解决普通密码认证产生的 ORA-01017。当前问题中没有必要临时加入 Wallet。
建议的排查顺序
- 通过进程模块列表确认
oraocci19.dll、oci.dll和安全组件的实际加载路径。 - 确认头文件、导入库、运行时 DLL 和位数均来自同一套 Oracle Client。
- 使用该 Oracle Home 中的
sqlplus.exe连接完全相同的 Easy Connect 地址。 - 在成功会话中查询
DB_NAME、SERVICE_NAME和CON_NAME,确认 OCCI 目标服务没有被路由到其他 PDB。 - 检查目标容器中的账号状态和
PASSWORD_VERSIONS。 - 检查
sqlnet.ora、认证协议设置、客户端跟踪和非 ASCII 编码。 - 仅在实际使用 TCPS 或外部密码存储时检查 Wallet。
根据现有信息,继续反复修改密码、FIPS 设置或连接字符串的排查价值不大。先核对运行时加载的 Oracle DLL 和服务对应的容器,更容易快速缩小问题范围。
备注:内容仅供参考。
