从登录和注销中获取通知
Posted
技术标签:
【中文标题】从登录和注销中获取通知【英文标题】:Get notified from logon and logoff 【发布时间】:2013-04-23 07:56:26 【问题描述】:我必须开发一个在本地 pc 上运行的程序作为服务向服务器提供几个用户状态。一开始我必须检测用户logon和logoff。
我的想法是使用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_LOGON
和WTS_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 多个虚拟机。因此,手动查找登录这些机器的人员非常耗时。所以我正在寻找解决方案以上是关于从登录和注销中获取通知的主要内容,如果未能解决你的问题,请参考以下文章