Windows Server下服务读取Windows凭据管理器凭据失败求助
.NET 8 Windows后台服务在Windows Server上无法读取凭据管理器凭据的问题排查
我开发了一个以管理员身份运行的.NET 8 Windows后台服务,用于通过目标名称读取Windows凭据管理器中的凭据。该服务在Windows 11本地运行正常,但部署到Windows Server后,两种读取方式均无法找到凭据:
- 方式1:调用CredMan.ps1脚本返回退出码0,但脚本输出错误:
Credential for '$Target' as '$CredType' type was not found. - 方式2:调用Advapi32.dll的
CredRead函数返回错误码1168(ERROR_NOT_FOUND)
调用CredRead的代码
private static string? ReadCredential(string target, Func<CredentialEnum.CREDENTIAL, string> readFunc) { if (string.IsNullOrWhiteSpace(target)) { throw new ArgumentException($"{nameof(target)} cannot be null or empty.", nameof(target)); } nint credPtr = nint.Zero; try { target = target.Trim(); bool success = CredRead(target, CredentialTypeEnum.CRED_TYPE.GENERIC, 0, out credPtr); if (success) { CredentialEnum.CREDENTIAL credential = Marshal.PtrToStructure<CredentialEnum.CREDENTIAL>(credPtr); return readFunc(credential); } } catch (Exception ex) { Log.Error(ex, "Error reading credential"); } finally { if (credPtr != nint.Zero) { CredFree(credPtr); credPtr = nint.Zero; } } return null; }
CredRead的DllImport声明
[DllImport("Advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] public static extern bool CredRead(string target, CredentialTypeEnum.CRED_TYPE type, int reservedFlag, out nint credentialPtr);
调用CredMan.ps1的代码
private async Task ExecuteCredManCommand(string psCommand, CustomerHost host) { string scriptPath = GetScriptFullPath("CredMan.ps1"); psCommand = $"&{scriptPath + " " + $"-GetCred -Target '{host.CredentialName.Trim()}' -CredType Generic"}"; StringBuilder stdOutBuffer = new StringBuilder(); StringBuilder stdErrBuffer = new StringBuilder(); var result = Cli.Wrap("powershell") .WithArguments($"-Command \"{psCommand}\"") .WithValidation(CommandResultValidation.None) .WithStandardOutputPipe(PipeTarget.ToStringBuilder(stdOutBuffer)) .WithStandardErrorPipe(PipeTarget.ToStringBuilder(stdErrBuffer)) .ListenAsync(); int exitCode = 0; await foreach (var cmdEvent in result) { if (cmdEvent is ExitedCommandEvent exited) { exitCode = exited.ExitCode; _logger.LogInformation($"Process exited; Code: {exited.ExitCode}"); } } // Parse the output to retrieve UserName and Password string output = stdOutBuffer.ToString(); string[] lines = output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); _logger.LogInformation($"CredMan Output: {lines}"); foreach (var line in lines) { //" UserName : k-is\\administrator" if (line.Trim().StartsWith("UserName")) { host.UserName = line.Split(':')[1].Trim(); } else if (line.Trim().StartsWith("Password")) { host.Password = line.Split(':')[1].Trim(); } } }
CredMan.ps1中报错的函数代码
#region Reading selected credential if($GetCred) { if(-not $Target) { Write-Host "You must supply a target URI." return } # may be [PsUtils.CredMan+Credential] or [Management.Automation.ErrorRecord] [Object] $Cred = Read-Creds $Target $CredType if($null -eq $Cred) { Write-Host "Credential for '$Target' as '$CredType' type was not found." return } if($Cred -is [Management.Automation.ErrorRecord]) { return $Cred } [String] $CredStr = @" Found credentials as: UserName : $($Cred.UserName) Password : $($Cred.CredentialBlob) Target : $($Cred.TargetName.Substring($Cred.TargetName.IndexOf("=")+1)) Updated : $([String]::Format("{0:yyyy-MM-dd HH:mm:ss}", $Cred.LastWritten.ToUniversalTime())) UTC Comment : $($Cred.Comment) "@ Write-Host $CredStr } #endregion
手动运行正常的命令
在Windows Server的PowerShell ISE中手动执行以下命令可成功获取凭据:
cd pathofcredman .\CredMan.ps1 -GetCred -Target NameOfTarget -CredType Generic
排查方向
- 凭据存储范围差异:Windows凭据分为「当前用户」和「本地计算机」两个存储范围。后台服务以管理员身份运行时,若使用Local System等系统账户,访问的是系统凭据存储,而非手动测试时的用户凭据存储。检查目标凭据是否存储在「本地计算机」范围。
- 目标名称匹配问题:确保后台服务传递的
Target名称与凭据管理器中的完全一致,注意大小写、特殊字符(如反斜杠、空格)的转义或处理是否正确。添加日志打印实际传递的Target值,与手动运行时的参数对比。 - PowerShell执行上下文差异:手动运行PowerShell是交互用户上下文,后台服务调用时是系统服务上下文。检查脚本路径是否正确,服务是否有权限访问脚本文件;尝试使用
powershell.exe -NoProfile -Command ...避免加载用户配置文件导致的环境差异。 - CredRead参数与结构验证:确认
CredentialEnum.CRED_TYPE.GENERIC的枚举值是否为1(符合Windows API定义),以及CredentialEnum.CREDENTIAL结构的字段定义是否与API要求完全匹配,特别是字符串字段的编码要对应CharSet.Unicode。 - 权限与运行账户问题:系统账户(如Local System)对用户私有的凭据存储无访问权限。可尝试将服务运行账户改为手动测试时的用户账户,或确保凭据存储在系统范围内。
- 补充错误日志:在
CredRead调用失败后,调用Marshal.GetLastWin32Error()获取更详细的错误信息;PowerShell调用时完整捕获stdErrBuffer的内容,查看是否有隐藏错误输出。
内容的提问来源于stack exchange,提问作者samsoft
相关产品推荐
相关产品推荐

