从登录和注销中获取通知

Posted

技术标签:

【中文标题】从登录和注销中获取通知【英文标题】:Get notified from logon and logoff 【发布时间】:2013-04-23 07:56:26 【问题描述】:

我必须开发一个在本地 pc 上运行的程序作为服务向服务器提供几个用户状态。一开始我必须检测用户logonlogoff

我的想法是使用ManagementEventWatcher 类并查询Win32_LogonSession 以便在发生变化时得到通知。

我的第一个测试运行良好,这是代码部分(这将作为服务中的线程执行)

private readonly static WqlEventQuery qLgi = new WqlEventQuery("__InstanceCreationEvent", new TimeSpan(0, 0, 1), "TargetInstance ISA \"Win32_LogonSession\"");

public EventWatcherUser() 


public void DoWork() 
    ManagementEventWatcher eLgiWatcher = new ManagementEventWatcher(EventWatcherUser.qLgi);
    eLgiWatcher.EventArrived += new EventArrivedEventHandler(HandleEvent);
    eLgiWatcher.Start();


private void HandleEvent(object sender, EventArrivedEventArgs e)

    ManagementBaseObject f = (ManagementBaseObject)e.NewEvent["TargetInstance"];
    using (StreamWriter fs = new StreamWriter("C:\\status.log", true))
    
        fs.WriteLine(f.Properties["LogonId"].Value);
    

但我有一些理解上的问题,我不确定这是否是解决该任务的常用方法。

    如果我查询 Win32_LogonSession 我会得到几条记录 关联到同一个用户。例如,我得到这个 ID 7580798 和 7580829 如果我查询

    Win32_LogonSession.LogonId=X 的关联方 WHERE ResultClass=Win32_UserAccount

    我得到不同 ID 的相同记录。 (Win32_UserAccount.Domain="PC-Name",Name="User1")

    为什么同一用户有多个登录会话?是什么 获取当前登录用户的常用方法?或者更好的如何获得通知 通过用户的登录正确吗?

    我想我可以使用与__InstanceDeletionEvent 相同的方式来 确定用户是否注销。但我想如果事件被提出,我 之后无法查询Win32_UserAccount 的用户名。我是对的?

我的方向是正确的还是有更好的方法?如果你能帮助我,那就太棒了!

编辑 WTSRegisterSessionNotification 类是否正确?我不知道这是否可能,因为在服务中我没有窗口处理程序。

【问题讨论】:

您可以使用 SENS (msdn.microsoft.com/library/cc185680.aspx) 登录事件。看看这个:richardarthur.sys-con.com/node/105651/mobile 这些库在带有 Visual Studio 2012 的 Windows 8 上不可用。缺少 SENS Events Type Library 除了您应该提及您的要求之外,还有哪些库不可用? Windows 8 有 es.dll Visual Studio 2012 和 Windows 8 中缺少 SENS Events Type Library COM 引用。我也无法从 C:\Windows\System32 添加 Sens.dll。它不显示在VS中。问候。 嗯。你是对的,但该服务确实正在运行,我在任何地方都没有提到它不受支持。我稍后会检查。 【参考方案1】:

由于您使用的是服务,因此您可以直接获取会话更改事件。

您可以注册自己以接收SERVICE_CONTROL_SESSIONCHANGE 事件。特别是,您将需要寻找WTS_SESSION_LOGONWTS_SESSION_LOGOFF 的原因。

有关详细信息和相关 MSDN 文档的链接,请查看this answer I wrote just yesterday。

在 C# 中更容易,因为 ServiceBase 已经包装了服务控制例程并将事件公开为可覆盖的 OnSessionChange 方法。请参阅MSDN docs for ServiceBase,不要忘记将CanHandleSessionChangeEvent 属性设置为true 以启用此方法的执行。

当框架调用您的 OnSessionChange 覆盖时,您得到的是带有原因(注销、登录、...)和会话 ID 的 SessionChangeDescription Structure,您可以使用它来获取信息,例如有关用户的信息登录/注销(有关详细信息,请参阅我的上一个答案的链接)

编辑:示例代码

 public class SimpleService : ServiceBase 
    ...
    public SimpleService()
    
        CanPauseAndContinue = true;
        CanHandleSessionChangeEvent = true;
        ServiceName = "SimpleService";
    

    protected override void OnSessionChange(SessionChangeDescription changeDescription)
    
        EventLog.WriteEntry("SimpleService.OnSessionChange", DateTime.Now.ToLongTimeString() +
            " - Session change notice received: " +
            changeDescription.Reason.ToString() + "  Session ID: " + 
            changeDescription.SessionId.ToString());


        switch (changeDescription.Reason)
        
            case SessionChangeReason.SessionLogon:
                EventLog.WriteEntry("SimpleService.OnSessionChange: Logon");
                break;

            case SessionChangeReason.SessionLogoff:       
                EventLog.WriteEntry("SimpleService.OnSessionChange Logoff"); 
                break;
           ...
        

【讨论】:

这是关于 WTSRegisterSessionNotification 的吗?我应该使用哪个窗口处理程序?还是我误解了你的描述? @hofmeister,不,如果您在服务中,则不需要 WTSRegisterSessionNotification:服务已经收到发布到其服务控制例程的会话通知。由于您使用的是 C#,我想您是通过派生 ServiceBase 来实现您的服务的。我说的对吗? 是的,简而言之。当我从ServiceBase 类派生时,我可以重写OnSessionChange 方法吗? 是的,完全正确!不要忘记将 CanHandleSessionChangeEvent 属性设置为 true,否则您的 OnSessionChange 覆盖将不会被调用;那么每次用户登录/注销(但也有其他事件,如锁定/解锁..)时,您的方法将被调用并通知您 我添加了一些示例代码,改编自 MSDN,以使其更清晰【参考方案2】:

您可以使用 Windows 中的System Event Notification Service 技术。它有 ISensLogon2 interface 提供登录/注销事件(以及其他事件,例如远程会话连接)。

这里有一段代码(示例控制台应用程序)演示了如何执行此操作。例如,您可以使用来自另一台计算机的远程桌面会话对其进行测试,这将触发 SessionDisconnect、SessionReconnect 等事件。

此代码应支持从 XP 到 Windows 8 的所有 Windows 版本。

添加对名为 COM+ 1.0 Admin Type Library aka COMAdmin 的 COM 组件的引用。

注意请务必将 Embed Interop Types 设置为“False”,否则会出现以下错误:“Interop type 'COMAdminCatalogClass' cannot be embedded。请改用适用的接口。”

与您在 Internet 上找到的有关在 .NET 中使用此技术的其他文章相反,它没有引用 Sens.dll,因为...它似乎在 Windows 8 上不存在(我不知道为什么) .然而,该技术似乎受支持,并且 SENS 服务确实已安装并在 Windows 8 上运行良好,因此您只需要手动声明接口和 guid(如本示例中所示),或引用在早期版本的 Windows 上创建的互操作程序集(它应该可以正常工作,因为 guid 和各种界面没有改变)。

class Program

    static SensEvents SensEvents  get; set; 

    static void Main(string[] args)
    
        SensEvents = new SensEvents();
        SensEvents.LogonEvent += OnSensLogonEvent;
        Console.WriteLine("Waiting for events. Press [ENTER] to stop.");
        Console.ReadLine();
    

    static void OnSensLogonEvent(object sender, SensLogonEventArgs e)
    
        Console.WriteLine("Type:" + e.Type + ", UserName:" + e.UserName + ", SessionId:" + e.SessionId);
    


public sealed class SensEvents

    private static readonly Guid SENSGUID_EVENTCLASS_LOGON2 = new Guid("d5978650-5b9f-11d1-8dd2-00aa004abd5e");
    private Sink _sink;

    public event EventHandler<SensLogonEventArgs> LogonEvent;

    public SensEvents()
    
        _sink = new Sink(this);
        COMAdminCatalogClass catalog = new COMAdminCatalogClass(); // need a reference to COMAdmin

        // we just need a transient subscription, for the lifetime of our application
        ICatalogCollection subscriptions = (ICatalogCollection)catalog.GetCollection("TransientSubscriptions");

        ICatalogObject subscription = (ICatalogObject)subscriptions.Add();
        subscription.set_Value("EventCLSID", SENSGUID_EVENTCLASS_LOGON2.ToString("B"));
        subscription.set_Value("SubscriberInterface", _sink);
        // NOTE: we don't specify a method name, so all methods may be called
        subscriptions.SaveChanges();
    

    private void OnLogonEvent(SensLogonEventType type, string bstrUserName, uint dwSessionId)
    
        EventHandler<SensLogonEventArgs> handler = LogonEvent;
        if (handler != null)
        
            handler(this, new SensLogonEventArgs(type, bstrUserName, dwSessionId));
        
    

    private class Sink : ISensLogon2
    
        private SensEvents _events;

        public Sink(SensEvents events)
        
            _events = events;
        

        public void Logon(string bstrUserName, uint dwSessionId)
        
            _events.OnLogonEvent(SensLogonEventType.Logon, bstrUserName, dwSessionId);
        

        public void Logoff(string bstrUserName, uint dwSessionId)
        
            _events.OnLogonEvent(SensLogonEventType.Logoff, bstrUserName, dwSessionId);
        

        public void SessionDisconnect(string bstrUserName, uint dwSessionId)
        
            _events.OnLogonEvent(SensLogonEventType.SessionDisconnect, bstrUserName, dwSessionId);
        

        public void SessionReconnect(string bstrUserName, uint dwSessionId)
        
            _events.OnLogonEvent(SensLogonEventType.SessionReconnect, bstrUserName, dwSessionId);
        

        public void PostShell(string bstrUserName, uint dwSessionId)
        
            _events.OnLogonEvent(SensLogonEventType.PostShell, bstrUserName, dwSessionId);
        
    

    [ComImport, Guid("D597BAB4-5B9F-11D1-8DD2-00AA004ABD5E")]
    private interface ISensLogon2
    
        void Logon([MarshalAs(UnmanagedType.BStr)] string bstrUserName, uint dwSessionId);
        void Logoff([In, MarshalAs(UnmanagedType.BStr)] string bstrUserName, uint dwSessionId);
        void SessionDisconnect([In, MarshalAs(UnmanagedType.BStr)] string bstrUserName, uint dwSessionId);
        void SessionReconnect([In, MarshalAs(UnmanagedType.BStr)] string bstrUserName, uint dwSessionId);
        void PostShell([In, MarshalAs(UnmanagedType.BStr)] string bstrUserName, uint dwSessionId);
    


public class SensLogonEventArgs : EventArgs

    public SensLogonEventArgs(SensLogonEventType type, string userName, uint sessionId)
    
        Type = type;
        UserName = userName;
        SessionId = sessionId;
    

    public string UserName  get; private set; 
    public uint SessionId  get; private set; 
    public SensLogonEventType Type  get; private set; 


public enum SensLogonEventType

    Logon,
    Logoff,
    SessionDisconnect,
    SessionReconnect,
    PostShell

注意:通过右键单击您的 Visual Studio 快捷方式并单击 run as administrator,确保 Visual Studio 以管理员权限运行,否则在运行程序时将抛出 System.UnauthorizedAccessException

【讨论】:

谢谢,这很好,效果很好!对于 .Net 4 和 Interop 嵌入式问题,请参阅此link。 我正在开发一个项目来捕获管理员通过 RDP 或控制台登录计算机时的操作。但是我不知道在检测到用户登录时如何启动捕获屏幕的进程?你能给我帮助吗?谢谢! @zhoulinWang - 请提出问题,有人可以提供帮助。 我必须创建一个项目,在检测用户登录时启动进程并在用户注销时停止进程。但现在我不知道如何检测他们何时登录或注销。请给我 C# 代码示例。谢谢! 我在带有visual studio 2010的windows 2008服务中运行上面的代码示例。当管理员用户通过远程桌面从本地PC登录windows 2008时,结果没有显示userName和sessionId,除了'等待事件。按 [ENTER] 停止。'。【参考方案3】:

这是代码(它们都位于一个类中;在我的例子中,类继承了ServiceBase)。如果您还想获取登录用户的用户名,这将特别有用。

    [DllImport("Wtsapi32.dll")]
    private static extern bool WTSQuerySessionInformation(IntPtr hServer, int sessionId, WtsInfoClass wtsInfoClass, out IntPtr ppBuffer, out int pBytesReturned);
    [DllImport("Wtsapi32.dll")]
    private static extern void WTSFreeMemory(IntPtr pointer);

    private enum WtsInfoClass
    
        WTSUserName = 5, 
        WTSDomainName = 7,
    

    private static string GetUsername(int sessionId, bool prependDomain = true)
    
        IntPtr buffer;
        int strLen;
        string username = "SYSTEM";
        if (WTSQuerySessionInformation(IntPtr.Zero, sessionId, WtsInfoClass.WTSUserName, out buffer, out strLen) && strLen > 1)
        
            username = Marshal.PtrToStringAnsi(buffer);
            WTSFreeMemory(buffer);
            if (prependDomain)
            
                if (WTSQuerySessionInformation(IntPtr.Zero, sessionId, WtsInfoClass.WTSDomainName, out buffer, out strLen) && strLen > 1)
                
                    username = Marshal.PtrToStringAnsi(buffer) + "\\" + username;
                    WTSFreeMemory(buffer);
                
            
        
        return username;
    

在你的类中使用上面的代码,你可以像这样在你覆盖的方法中简单地获取用户名:

protected override void OnSessionChange(SessionChangeDescription changeDescription)

    string username = GetUsername(changeDescription.SessionId);
    //continue with any other thing you wish to do

注意:记得在继承自ServiceBase的类的构造函数中添加CanHandleSessionChangeEvent = true;

【讨论】:

【参考方案4】:

我使用ServiceBase.OnSessionChange 来捕捉不同的用户事件并在之后加载必要的信息。

protected override void OnSessionChange(SessionChangeDescription desc)

    var user = Session.Get(desc.SessionId);

要加载会话信息,我使用WTS_INFO_CLASS。请参阅下面的示例:

internal static class NativeMethods

    public enum WTS_INFO_CLASS
    
        WTSInitialProgram,
        WTSApplicationName,
        WTSWorkingDirectory,
        WTSOEMId,
        WTSSessionId,
        WTSUserName,
        WTSWinStationName,
        WTSDomainName,
        WTSConnectState,
        WTSClientBuildNumber,
        WTSClientName,
        WTSClientDirectory,
        WTSClientProductId,
        WTSClientHardwareId,
        WTSClientAddress,
        WTSClientDisplay,
        WTSClientProtocolType,
        WTSIdleTime,
        WTSLogonTime,
        WTSIncomingBytes,
        WTSOutgoingBytes,
        WTSIncomingFrames,
        WTSOutgoingFrames,
        WTSClientInfo,
        WTSSessionInfo
    

    [DllImport("Kernel32.dll")]
    public static extern uint WTSGetActiveConsoleSessionId();

    [DllImport("Wtsapi32.dll")]
    public static extern bool WTSQuerySessionInformation(IntPtr hServer, Int32 sessionId, WTS_INFO_CLASS wtsInfoClass, out IntPtr ppBuffer, out Int32 pBytesReturned);

    [DllImport("Wtsapi32.dll")]
    public static extern void WTSFreeMemory(IntPtr pointer);


public static class Status

    public static Byte Online
    
        get  return 0x0; 
    

    public static Byte Offline
    
        get  return 0x1; 
    

    public static Byte SignedIn
    
        get  return 0x2; 
    

    public static Byte SignedOff
    
        get  return 0x3; 
    


public static class Session

    private static readonly Dictionary<Int32, User> User = new Dictionary<Int32, User>();

    public static bool Add(Int32 sessionId)
    
        IntPtr buffer;
        int length;

        var name = String.Empty;
        var domain = String.Empty;

        if (NativeMethods.WTSQuerySessionInformation(IntPtr.Zero, sessionId, NativeMethods.WTS_INFO_CLASS.WTSUserName, out buffer, out length) && length > 1)
        
            name = Marshal.PtrToStringAnsi(buffer);
            NativeMethods.WTSFreeMemory(buffer);
            if (NativeMethods.WTSQuerySessionInformation(IntPtr.Zero, sessionId, NativeMethods.WTS_INFO_CLASS.WTSDomainName, out buffer, out length) && length > 1)
            
                domain = Marshal.PtrToStringAnsi(buffer);
                NativeMethods.WTSFreeMemory(buffer);
            
        

        if (name == null || name.Length <= 0)
        
            return false;
        

        User.Add(sessionId, new User(name, domain));

        return true;
    

    public static bool Remove(Int32 sessionId)
    
        return User.Remove(sessionId);
    

    public static User Get(Int32 sessionId)
    
        if (User.ContainsKey(sessionId))
        
            return User[sessionId];
        

        return Add(sessionId) ? Get(sessionId) : null;
    

    public static UInt32 GetActiveConsoleSessionId()
    
        return NativeMethods.WTSGetActiveConsoleSessionId();
    


public class AvailabilityChangedEventArgs : EventArgs

    public bool Available  get; set; 

    public AvailabilityChangedEventArgs(bool isAvailable)
    
        Available = isAvailable;
    


public class User

    private readonly String _name;

    private readonly String _domain;

    private readonly bool _isDomainUser;

    private bool _signedIn;

    public static EventHandler<AvailabilityChangedEventArgs> AvailabilityChanged;

    public User(String name, String domain)
    
        _name = name;
        _domain = domain;

        if (domain.Equals("EXAMPLE.COM"))
        
            _isDomainUser = true;
        
        else
        
            _isDomainUser = false;
        
    

    public String Name
    
        get  return _name; 
    

    public String Domain
    
        get  return _domain; 
    

    public bool IsDomainUser
    
        get  return _isDomainUser; 
    

    public bool IsSignedIn
    
        get  return _signedIn; 
        set
        
            if (_signedIn == value) return;

            _signedIn = value;

            OnAvailabilityChanged(this, new AvailabilityChangedEventArgs(IsSignedIn));
        
    

    protected void OnAvailabilityChanged(object sender, AvailabilityChangedEventArgs e)
    
        if (AvailabilityChanged != null)
        
            AvailabilityChanged(this, e);
        
    

以下代码使用来自User 的静态AvailabilityChanged 事件,该事件会在会话状态更改时立即触发。 arg e 包含特定用户。

public Main()

  User.AvailabilityChanged += UserAvailabilityChanged;


private static void UserAvailabilityChanged(object sender, AvailabilityChangedEventArgs e)

  var user = sender as User;

  if (user == null) return;

  System.Diagnostics.Debug.WriteLine(user.IsSignedIn);

【讨论】:

谢谢,我看到登录事件已创建。但登录属性始终返回为“False”。其次,EventID 在事件查看器中总是显示为 0。 监听用户的AvailabilityChanged事件。一旦用户状态发生变化,事件就会被触发。 我知道了,但 EventID 为 0,我们可以设置自定义事件 ID 吗?我认为 70001 是用于登录的事件 ID,而 70002 用于注销。但它没有录制 @codetoshare 对不起 - 事件 ID 是什么意思?哪个来源?对于 Microsoft Windows 安全审核,登录的事件 ID 为 4648。请查看我在答案中的最后更改。我附上了一个例子,你如何捕捉会话变化。 谢谢。得到你。我正在使用 Windows Server 2012 R2。这显示 4624 用于登录和 4634 用于注销。我有 200 多个虚拟机。因此,手动查找登录这些机器的人员非常耗时。所以我正在寻找解决方案

以上是关于从登录和注销中获取通知的主要内容,如果未能解决你的问题,请参考以下文章

有推送通知问题的登录/注销用户

退出信标时获取通知

Apple 推送通知、注销应用和删除应用问题/解决方案

从 Android gcm 服务器接收重复的推送通知 [关闭]

带有登录系统的 GCM

限制 Flutter 中特定用户的 FCM 通知