C# 从登录和注销获得通知

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/16282231/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-10 00:37:43  来源:igfitidea点击:

Get notified from logon and logoff

c#eventsauthenticationlogoff

提问by Andre Hofmeister

I have to develop a program which runs on a local pc as a service an deliver couple of user status to a server. At the beginning I have to detect the user logonand logoff.

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

My idea was to use the ManagementEventWatcherclass and to query the Win32_LogonSessionto be notified if something changed.

我的想法是使用ManagementEventWatcher该类并Win32_LogonSession在发生变化时查询要通知的内容。

My first test works well, here is the code part (This would executed as a thread from a service):

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

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);
    }
}

But I have some understanding problems and I'm not sure if this is the common way to solve that task.

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

  1. If I query Win32_LogonSessionI get several records which are associated to the same user. For example I get this IDs 7580798 and 7580829 and if I query

    ASSOCIATORS OF {Win32_LogonSession.LogonId=X} WHERE ResultClass=Win32_UserAccount

    I get the same record for different IDs. (Win32_UserAccount.Domain="PC-Name",Name="User1")

    Why are there several logon session with the same user? What is the common way to get the current signed in user? Or better how to get notified correctly by the login of a user?

  2. I thought I could use the same way with __InstanceDeletionEventto determine if a user is log off. But I guess if the event is raised, I cant query Win32_UserAccountfor the username after that. I'm right?

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

    {Win32_LogonSession.LogonId=X} 的关联者,其中 ResultClass=Win32_UserAccount

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

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

  2. 我想我可以使用相同的方式__InstanceDeletionEvent来确定用户是否已注销。但我想如果事件被引发,我不能Win32_UserAccount在那之后查询用户名。我是对的?

I'm at the right direction or are there better ways? It would be awesome if you could help me!

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

EditIs the WTSRegisterSessionNotification class the correct way? I don't know if it's possible, because in a service I haven't a window handler.

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

采纳答案by Andre Hofmeister

I use ServiceBase.OnSessionChangeto catch the different user events and load the necessary information afterwards.

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

protected override void OnSessionChange(SessionChangeDescription desc)
{
    var user = Session.Get(desc.SessionId);
}

To load the session information I use the WTS_INFO_CLASS. See my example below:

要加载会话信息,我使用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);
        }
    }
}

The following code use the static AvailabilityChangedevent from User, which gets fired as soon as the session state changes. The arg econtains the specific user.

以下代码使用AvailabilityChanged来自的静态事件User,一旦会话状态发生变化,它就会被触发。arge包含特定用户。

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);
}

回答by Simon Mourier

You could use the System Event Notification Servicetechnology which is part of Windows. It has the ISensLogon2 interfacethat provides logon/logoff events (and other events such as remote session connections).

您可以使用系统事件通知服务技术,它是 Windows 的一部分。它具有提供登录/注销事件(以及其他事件,例如远程会话连接)的ISensLogon2 接口

Here is a piece of code (a sample Console Application) that demonstrates how to do it. You can test it using a remote desktop session from another computer for example, this will trigger the SessionDisconnect, SessionReconnect events for example.

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

This code should support all versions of Windows from XP to Windows 8.

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

Add reference to the COM component named, COM+ 1.0 Admin Type Libraryaka COMAdmin.

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

NoteBe sure to set the Embed Interop Types to 'False', otherwise you will get the following error: "Interop type 'COMAdminCatalogClass' cannot be embedded. Use the applicable interface instead."

注意请务必将嵌入互操作类型设置为 'False',否则您将收到以下错误:“无法嵌入互操作类型 'COMAdminCatalogClass'。请改用适用的接口。”

Contrary to other articles you will find on the Internet about using this technology in .NET, it does not references the Sens.dll because ... it does not seem to exist on Windows 8 (I don't know why). However the technology seems supported and the SENS service is indeed installed and runs fine on Windows 8, so you just to need to declare the interfaces and guids manually (like in this sample), or reference an interop assembly created on an earlier version of Windows (it should work fine as the guids and various interfaces have not changed).

与您将在 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
}

Note:Ensure that Visual Studio is running with administrator priviledges by right-clicking your Visual Studio shortcut and clicking run as administrator, otherwise an System.UnauthorizedAccessExceptionwill be thrown when the program is run.

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

回答by Lorenzo Dematté

Since you are on a service, you can get session change events directly.

由于您在服务上,您可以直接获取会话更改事件。

You can register yourself to receive the SERVICE_CONTROL_SESSIONCHANGEevent. In particular, you will want to look for the WTS_SESSION_LOGONand WTS_SESSION_LOGOFFreasons.

您可以自行注册以接收SERVICE_CONTROL_SESSIONCHANGE活动。尤其是,您将要查找WTS_SESSION_LOGONWTS_SESSION_LOGOFF原因。

For details and links to the relevant MSDN docs, check this answer I wrote just yesterday.

有关详细信息和相关 MSDN 文档的链接,请查看我昨天写的这个答案

In C# it is even easier, as ServiceBase already wraps the service control routine and exposes the event as an overridable OnSessionChangemethod for you. See MSDN docs for ServiceBase, and do not forget to set the CanHandleSessionChangeEventproperty to true to enable the execution of this method.

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

What you get back when the framework calls your OnSessionChangeoverride is a SessionChangeDescription Structurewith a reason (logoff, logon, ...) and a session ID you can use to obtain information, for example, on the user logging on/off (see the link to my prev answer for details)

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

EDIT: sample code

编辑:示例代码

 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;
           ...
        }

回答by Soma Mbadiwe

Here's the code (all of them residing inside a class; in my case, the class inheriting ServiceBase). This is especially useful if you also want to get the logged-on user's username.

这是代码(它们都驻留在一个类中;在我的情况下,类继承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;
    }

With the above code in your class, you can simply get the username in the method you're overriding like this:

使用类中的上述代码,您可以简单地在您覆盖的方法中获取用户名,如下所示:

protected override void OnSessionChange(SessionChangeDescription changeDescription)
{
    string username = GetUsername(changeDescription.SessionId);
    //continue with any other thing you wish to do
}

NB: Remember to add CanHandleSessionChangeEvent = true;In the constructor of the class inheriting from ServiceBase

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