WindowsIdentity.RunImpersonated中调用ServiceController遇FileNotFoundException求助
我是C#新手,正在Visual Studio 2022中开发一款应用,其中部分功能需要访问远程电脑查询运行中的服务。单独运行获取本地服务的代码、身份模拟代码(均来自微软官方示例)都正常;在同一个类中先完成身份模拟再取消模拟后调用服务查询代码,也能正常运行。但将ServiceController查询远程服务的代码放入WindowsIdentity.RunImpersonated的执行委托中时,以普通用户身份运行会出现FileNotFoundException,提示无法加载System.ServiceProcess.ServiceController程序集。
错误信息
System.IO.FileNotFoundException HResult=0x80070002 Message=Could not load file or assembly 'System.ServiceProcess.ServiceController, Version=7.0.0.1, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'. The system cannot find the file specified. Source=Identity StackTrace: at ImpersonationDemo.<>c.<Main>b__1_0() in C:\Users\me\source\repos\TestProjects\Identity\Program.cs:line 75 at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state) --- End of stack trace from previous location --- at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state) at System.Security.Principal.WindowsIdentity.RunImpersonatedInternal(SafeAccessTokenHandle token, Action action) at ImpersonationDemo.Main() in C:\Users\me\source\repos\TestProjects\Identity\Program.cs:line 54
完整代码
using Microsoft.Win32.SafeHandles; using System.Runtime.InteropServices; using System.Security.Principal; using System.ServiceProcess; public class Test2 { [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] public static extern bool LogonUser(String lpszUsername, String lpszDomain, String lpszPassword, int dwLogonType, int dwLogonProvider, out SafeAccessTokenHandle phToken); public static void Main() { // Get the user token for the specified user, domain, and password using the // unmanaged LogonUser method. // The local machine name can be used for the domain name to impersonate a user on this machine. Console.Write("Enter the name of the domain on which to log on: "); string domainName = Console.ReadLine(); Console.Write("Enter the login of a user on {0} that you wish to impersonate: ", domainName); string userName = Console.ReadLine(); Console.Write("Enter the password for {0}: ", userName); const int LOGON32_PROVIDER_DEFAULT = 0; //This parameter causes LogonUser to create a primary token. const int LOGON32_LOGON_INTERACTIVE = 2; // Call LogonUser to obtain a handle to an access token. SafeAccessTokenHandle safeAccessTokenHandle; bool returnValue = LogonUser(userName, domainName, Console.ReadLine(), LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, out safeAccessTokenHandle); if (false == returnValue) { int ret = Marshal.GetLastWin32Error(); Console.WriteLine("LogonUser failed with error code : {0}", ret); throw new System.ComponentModel.Win32Exception(ret); } Console.WriteLine("Did LogonUser Succeed? " + (returnValue ? "Yes" : "No")); // Check the identity. Console.WriteLine("Before impersonation: " + WindowsIdentity.GetCurrent().Name); WindowsIdentity.RunImpersonated( safeAccessTokenHandle, // User action () => { // Check the identity. Console.WriteLine("During impersonation: " + WindowsIdentity.GetCurrent().Name); // **** Combined the following into RunImpersonated: **** string computerName = "db-test-2022"; ServiceController[] scServices; scServices = ServiceController.GetServices(computerName); Console.WriteLine(String.Format("Services running on [{0}]:", computerName)); foreach (ServiceController scTemp in scServices) { if (scTemp.Status == ServiceControllerStatus.Running) { Console.WriteLine("Service name: {0}\tDisplay name: {1}", scTemp.ServiceName, scTemp.DisplayName); } } }); // Check the identity again. Console.WriteLine("After impersonation: " + WindowsIdentity.GetCurrent().Name); } }
问题总结
- 普通用户账户单独运行两个微软示例代码均无问题
- 普通用户账户先完成身份模拟再取消模拟后调用服务查询代码,可正常运行
- 仅当服务查询代码放入
WindowsIdentity.RunImpersonated的执行范围时,才会出现找不到ServiceController程序集的错误
不想通过以管理员身份运行Visual Studio规避问题,测试发现普通和管理员账户创建的项目都需要安装NuGet包System.ServiceProcess.ServiceController,应用部署后将使用具备相应权限的服务账户运行,目前处于测试代码阶段。
问题根源
这是由于程序集加载上下文的权限差异导致的:
当在WindowsIdentity.RunImpersonated委托内执行代码时,CLR会使用模拟用户的上下文加载所需程序集。如果模拟的用户没有访问本地NuGet缓存目录或项目输出目录的权限,就会加载失败。而在模拟上下文外调用代码时,是用当前普通用户的上下文加载程序集,该用户拥有本地文件的访问权限,因此能正常加载。
解决办法
1. 提前加载程序集
在进入身份模拟逻辑之前,先触发ServiceController相关类型的加载,让CLR在当前普通用户上下文下把程序集加载到内存中。修改代码,在WindowsIdentity.RunImpersonated调用前添加一行:
// 提前加载ServiceController程序集,避免模拟时权限不足无法加载 _ = typeof(ServiceController);
这样程序集会提前加载到内存,模拟执行时直接使用已加载的程序集,不会再触发加载操作,也就避开了模拟用户的权限限制。
2. 配置模拟用户的文件访问权限(可选)
如果必须在模拟上下文加载程序集,需要确保模拟的用户账户对以下目录有读取权限:
- 项目的输出目录(比如
bin\Debug) - NuGet包缓存目录(通常为
%USERPROFILE%\.nuget\packages)
不过这种方法配置繁琐,不如提前加载高效。
无需管理员身份测试高权限代码的方法
方法一:用runas命令运行编译后的程序
编译项目后,打开命令提示符,用目标服务账户身份运行程序:
runas /user:domain\serviceaccount "C:\path\to\your\project\bin\Debug\YourApp.exe"
方法二:配置调试时的启动身份
右键项目→属性→调试→勾选“使用其他用户身份运行”,输入服务账户的凭据。这样调试时会直接以该账户启动程序,无需编写模拟代码,更贴近最终部署场景。
内容的提问来源于stack exchange,提问作者Dave Harding

