从系统账户向已登录用户发送Windows Toast通知的问题
系统账户下发送Toast通知的解决方案
问题根源
系统账户运行的进程默认没有访问当前登录用户交互式会话的权限,Toast通知依赖用户会话的UI环境,因此直接调用Show()无法在用户桌面显示通知。
实现步骤
1. 获取当前活跃用户的会话ID
首先要定位到当前登录且处于活跃状态的用户会话ID,这是后续将通知进程注入到用户会话的关键。可以通过执行系统命令query session并解析输出来实现:
using System.Diagnostics; using System.Linq; public static int GetActiveUserSessionId() { var process = new Process { StartInfo = new ProcessStartInfo { FileName = "query", Arguments = "session", RedirectStandardOutput = true, UseShellExecute = false, CreateNoWindow = true } }; process.Start(); var output = process.StandardOutput.ReadToEnd(); process.WaitForExit(); var lines = output.Split('\n').Where(l => !string.IsNullOrWhiteSpace(l)).Skip(1); foreach (var line in lines) { var parts = line.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); if (parts.Length >= 3 && parts[2].Equals("Active", StringComparison.OrdinalIgnoreCase)) { if (int.TryParse(parts[1], out int sessionId)) { return sessionId; } } } return -1; }
2. 调用Windows API在用户会话中启动通知进程
使用CreateProcessAsUserAPI将发送Toast的程序在目标用户会话中启动,这样通知代码就运行在用户的UI上下文中,能正常显示Toast。
先声明所需的原生API:
using System.Runtime.InteropServices; public static class NativeMethods { [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] public static extern bool CreateProcessAsUser( IntPtr hToken, string lpApplicationName, string lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation); [DllImport("kernel32.dll", SetLastError = true)] public static extern bool CloseHandle(IntPtr hObject); [DllImport("wtsapi32.dll", SetLastError = true)] public static extern bool WTSQueryUserToken(int sessionId, out IntPtr phToken); [StructLayout(LayoutKind.Sequential)] public struct STARTUPINFO { public int cb; public string lpReserved; public string lpDesktop; public string lpTitle; public int dwX; public int dwY; public int dwXSize; public int dwYSize; public int dwXCountChars; public int dwYCountChars; public int dwFillAttribute; public int dwFlags; public short wShowWindow; public short cbReserved2; public IntPtr lpReserved2; public IntPtr hStdInput; public IntPtr hStdOutput; public IntPtr hStdError; } [StructLayout(LayoutKind.Sequential)] public struct PROCESS_INFORMATION { public IntPtr hProcess; public IntPtr hThread; public int dwProcessId; public int dwThreadId; } }
然后实现启动用户会话进程的方法:
public static bool RunProcessInUserSession(int sessionId, string exePath) { if (!NativeMethods.WTSQueryUserToken(sessionId, out IntPtr userToken)) { return false; } var startupInfo = new NativeMethods.STARTUPINFO(); startupInfo.cb = Marshal.SizeOf(startupInfo); startupInfo.lpDesktop = "winsta0\\default"; // 指定用户桌面会话 var processInfo = new NativeMethods.PROCESS_INFORMATION(); bool result = NativeMethods.CreateProcessAsUser( userToken, exePath, null, IntPtr.Zero, IntPtr.Zero, false, 0, IntPtr.Zero, null, ref startupInfo, out processInfo); NativeMethods.CloseHandle(userToken); NativeMethods.CloseHandle(processInfo.hProcess); NativeMethods.CloseHandle(processInfo.hThread); return result; }
3. 拆分程序逻辑
将原有的Toast发送代码单独封装为一个独立的控制台程序(如ToastSender.exe),代码如下:
using Microsoft.Toolkit.Uwp.Notifications; new ToastContentBuilder() .AddArgument("action", "viewConversation") .AddArgument("conversationId", 9813) .AddText("Security Notification") .AddText("A Honeypot file has been modified!") .Show();
在系统服务中,调用以下逻辑触发通知:
int activeSessionId = GetActiveUserSessionId(); if (activeSessionId != -1) { string toastExePath = @"C:\Your\Path\To\ToastSender.exe"; // 替换为实际路径 RunProcessInUserSession(activeSessionId, toastExePath); }
4. 权限配置
- 确保系统账户拥有
SeAssignPrimaryTokenPrivilege和SeIncreaseQuotaPrivilege权限,若默认未开启,需在服务启动时手动启用这些权限。 - 保证
ToastSender.exe的存放路径对系统账户有可读可执行权限,建议放在C:\Program Files或类似系统目录。
验证
设置服务以系统账户运行,触发通知逻辑后,检查目标用户桌面是否收到Toast通知,同时可查看Windows事件日志排查潜在错误。
内容的提问来源于stack exchange,提问作者user1724084
相关产品推荐
相关产品推荐

