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

从系统账户向已登录用户发送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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 03:24:56