You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 09:52:07