使用登录用户长时间运行的 Windows 服务

Posted

技术标签:

【中文标题】使用登录用户长时间运行的 Windows 服务【英文标题】:Long Running Windows Service using LogOn User 【发布时间】:2015-01-26 19:32:07 【问题描述】:

我有一个长期运行的 Windows 服务。在其部署中,我们经常使用自定义域用户登录凭据来为服务提供附加权限。当服务正在运行时,服务正在运行的用户的凭据可能会更改或过期。我正在尝试编写一个功能来通知用户服务凭据已过期并且必须采取手动干预来更新登录凭据。

我的问题是,从在过期上下文下运行的服务中检测其上下文已过期的最佳方法是什么?

谢谢

【问题讨论】:

您目前如何检查context 的用户是否有效..?为什么你不能在该部分添加一些额外的代码来检查域用户的密码/登录是否有效.. 你在使用PrincipalContext along with AD 在某些情况下,我们使用自定义登录来访问网络驱动器。如果上下文已过期,则与网络位置的连接将失败。我希望有一种更通用的方法来测试服务上下文是否已过期。我无权访问用户名并通过,因为这是由 windows 处理的。 我认为这取决于...取决于您的服务是如何设置的,如果它查看您确实有权访问的域用户名\密码,但我不熟悉您当前的结构.. 你是熟悉 `PrincipalContext.. 或者您可以在不暴露您的实际域的情况下显示代码 sn-p 您的服务当前如何检查..? 我仍然找不到解决这个问题的方法。由此...msdn.microsoft.com/en-us/library/cc875826.aspx ..(管理服务帐户密码更改部分)“分配密码后,SCM 不会验证存储在该数据库中的密码,分配给 Active Directory 中用户帐户的密码将继续匹配。”似乎一旦进程运行,AD 就会正常工作,并且无法知道上下文已过期。这很令人沮丧,因为如果系统重新启动,服务将无法启动,并且不会向用户发出警告。 【参考方案1】:

我会采取不同的方法,将服务留给受限用户,例如网络服务。 然后从服务中模拟当前登录的用户,这很棘手,因为可能没有或不止一个用户登录。这个answer 提供sample code 来复制令牌。

最后使用那个重复的令牌来访问网络共享,比如here。

编辑: 以下代码在作为本地系统帐户运行时有效:

class AccessShareAsLoggedOnUser

    public static bool CopyAsLocalUser(String sourceFile, string targetFile)
    
        uint dwSessionId, winlogonPid = 0;
        IntPtr hUserToken = IntPtr.Zero, hUserTokenDup = IntPtr.Zero, hPToken = IntPtr.Zero, hProcess = IntPtr.Zero;

        Debug.Print("CreateProcessInConsoleSession");
        // Log the client on to the local computer.
        dwSessionId = WTSGetActiveConsoleSessionId();

        // Find the winlogon process
        var procEntry = new PROCESSENTRY32();

        uint hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
        if (hSnap == INVALID_HANDLE_VALUE)
        
            return false;
        

        procEntry.dwSize = (uint)Marshal.SizeOf(procEntry); //sizeof(PROCESSENTRY32);

        if (Process32First(hSnap, ref procEntry) == 0)
        
            return false;
        

        String strCmp = "explorer.exe";
        do
        
            if (strCmp.IndexOf(procEntry.szExeFile) == 0)
            
                // We found a winlogon process...make sure it's running in the console session
                uint winlogonSessId = 0;
                if (ProcessIdToSessionId(procEntry.th32ProcessID, ref winlogonSessId) &&
                    winlogonSessId == dwSessionId)
                
                    winlogonPid = procEntry.th32ProcessID;
                    break;
                
            
        
        while (Process32Next(hSnap, ref procEntry) != 0);

        //Get the user token used by DuplicateTokenEx
        WTSQueryUserToken(dwSessionId, ref hUserToken);

        hProcess = OpenProcess(MAXIMUM_ALLOWED, false, winlogonPid);

        if (
            !OpenProcessToken(hProcess,
                TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY | TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY
                | TOKEN_ADJUST_SESSIONID | TOKEN_READ | TOKEN_WRITE, ref hPToken))
        
            Debug.Print(String.Format("CreateProcessInConsoleSession OpenProcessToken error: 0",
                Marshal.GetLastWin32Error()));
        

        var sa = new SECURITY_ATTRIBUTES();
        sa.Length = Marshal.SizeOf(sa);

        if (!DuplicateTokenEx(hPToken, MAXIMUM_ALLOWED, ref sa,
                (int)SECURITY_IMPERSONATION_LEVEL.SecurityIdentification, (int)TOKEN_TYPE.TokenPrimary,
                ref hUserTokenDup))
        
            Debug.Print(
                String.Format(
                    "CreateProcessInConsoleSession DuplicateTokenEx error: 0 Token does not have the privilege.",
                    Marshal.GetLastWin32Error()));
            CloseHandle(hProcess);
            CloseHandle(hUserToken);
            CloseHandle(hPToken);
            return false;
        

        try
        
            using (WindowsImpersonationContext impersonationContext =
                new WindowsIdentity(hUserTokenDup).Impersonate())
            
            // Put your network Code here.
                File.WriteAllText(targetFile // Somewhere with right permissions like @"C:\Users\xxx\output.txt"
                      , File.ReadAllText(sourceFile)); // like @"\\server\share\existingfile.txt"

                impersonationContext.Undo();
            
            return true;
        
        finally
        
            if (hUserTokenDup != IntPtr.Zero)
            
                if (!CloseHandle(hUserTokenDup))
                
                    // Uncomment if you need to know this case.
                    ////throw new Win32Exception();
                
            

            //Close handles task
            CloseHandle(hProcess);
            CloseHandle(hUserToken);
            CloseHandle(hUserTokenDup);
            CloseHandle(hPToken);
        
    

    [DllImport("kernel32.dll")]
    private static extern int Process32First(uint hSnapshot, ref PROCESSENTRY32 lppe);

    [DllImport("kernel32.dll")]
    private static extern int Process32Next(uint hSnapshot, ref PROCESSENTRY32 lppe);

    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern uint CreateToolhelp32Snapshot(uint dwFlags, uint th32ProcessID);

    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern bool CloseHandle(IntPtr hSnapshot);

    [DllImport("kernel32.dll")]
    private static extern uint WTSGetActiveConsoleSessionId();

    [DllImport("Wtsapi32.dll")]
    private static extern uint WTSQueryUserToken(uint SessionId, ref IntPtr phToken);

    [DllImport("kernel32.dll")]
    private static extern bool ProcessIdToSessionId(uint dwProcessId, ref uint pSessionId);

    [DllImport("kernel32.dll")]
    private static extern IntPtr OpenProcess(uint dwDesiredAccess, bool bInheritHandle, uint dwProcessId);

    [DllImport("advapi32", SetLastError = true)]
    [SuppressUnmanagedCodeSecurity]
    private static extern bool OpenProcessToken(IntPtr ProcessHandle, // handle to process
        int DesiredAccess, // desired access to process
        ref IntPtr TokenHandle);

    [DllImport("advapi32.dll", EntryPoint = "DuplicateTokenEx")]
    public static extern bool DuplicateTokenEx(IntPtr ExistingTokenHandle, uint dwDesiredAccess,
        ref SECURITY_ATTRIBUTES lpThreadAttributes, int TokenType,
        int ImpersonationLevel, ref IntPtr DuplicateTokenHandle);


    #region Nested type: PROCESSENTRY32

    [StructLayout(LayoutKind.Sequential)]
    private struct PROCESSENTRY32
    
        public uint dwSize;
        public readonly uint cntUsage;
        public readonly uint th32ProcessID;
        public readonly IntPtr th32DefaultHeapID;
        public readonly uint th32ModuleID;
        public readonly uint cntThreads;
        public readonly uint th32ParentProcessID;
        public readonly int pcPriClassBase;
        public readonly uint dwFlags;

        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
        public readonly string szExeFile;
    

    #endregion


    #region Nested type: SECURITY_ATTRIBUTES

    [StructLayout(LayoutKind.Sequential)]
    public struct SECURITY_ATTRIBUTES
    
        public int Length;
        public IntPtr lpSecurityDescriptor;
        public bool bInheritHandle;
    

    #endregion

    #region Nested type: SECURITY_IMPERSONATION_LEVEL

    private enum SECURITY_IMPERSONATION_LEVEL
    
        SecurityAnonymous = 0,
        SecurityIdentification = 1,
        SecurityImpersonation = 2,
        SecurityDelegation = 3,
    

    #endregion


    #region Nested type: TOKEN_TYPE

    private enum TOKEN_TYPE
    
        TokenPrimary = 1,
        TokenImpersonation = 2
    

    #endregion

    public const int READ_CONTROL = 0x00020000;

    public const int STANDARD_RIGHTS_REQUIRED = 0x000F0000;

    public const int STANDARD_RIGHTS_READ = READ_CONTROL;
    public const int STANDARD_RIGHTS_WRITE = READ_CONTROL;
    public const int STANDARD_RIGHTS_EXECUTE = READ_CONTROL;

    public const int STANDARD_RIGHTS_ALL = 0x001F0000;

    public const int SPECIFIC_RIGHTS_ALL = 0x0000FFFF;

    public const int TOKEN_ASSIGN_PRIMARY = 0x0001;
    public const int TOKEN_DUPLICATE = 0x0002;
    public const int TOKEN_IMPERSONATE = 0x0004;
    public const int TOKEN_QUERY = 0x0008;
    public const int TOKEN_QUERY_SOURCE = 0x0010;
    public const int TOKEN_ADJUST_PRIVILEGES = 0x0020;
    public const int TOKEN_ADJUST_GROUPS = 0x0040;
    public const int TOKEN_ADJUST_DEFAULT = 0x0080;
    public const int TOKEN_ADJUST_SESSIONID = 0x0100;

    public const int TOKEN_READ = STANDARD_RIGHTS_READ | TOKEN_QUERY;

    public const int TOKEN_WRITE = STANDARD_RIGHTS_WRITE |
                                   TOKEN_ADJUST_PRIVILEGES |
                                   TOKEN_ADJUST_GROUPS |
                                   TOKEN_ADJUST_DEFAULT;

    public const uint MAXIMUM_ALLOWED = 0x2000000;

    private const uint TH32CS_SNAPPROCESS = 0x00000002;

    public static int INVALID_HANDLE_VALUE = -1;

    // handle to open access token

【讨论】:

我不太喜欢这个,因为它要求服务能够访问密码才能进行提升。这个密码需要存储在网络服务可以访问的地方,因此我觉得这会引入一个安全漏洞。如果您通过 kerberos 之类的方式进行提升,则需要将该控制委托给网络服务,这又会导致一些安全问题 不,它没有。第一个链接在不知道密码的情况下复制登录用户的令牌。第二个链接使用该令牌访问网络共享。无需密码。 此链接有一种更简洁的方式来获取登录用户的令牌。它使用 2 个 API 调用,您可以轻松地将它们从 c++ 转换为 c#。 ***.com/a/19796825/97471 不幸的是,这种方法对我不起作用。我的 Windows 服务经常在服务器上运行,并且没有用户登录。这就是我们使用 Windows 服务 - Windows 提供的运行方式功能的原因。

以上是关于使用登录用户长时间运行的 Windows 服务的主要内容,如果未能解决你的问题,请参考以下文章

C#开发Windows服务

C#创建一个Window服务

在aws中预定长时间运行的c#作业

带有进度通知的长时间运行的后台任务

windows service编程

Windows服务简单一例,捕获关机信号做些处理