登录触发器必须建在ALL SERVER作用域,使用AFTER LOGON和EVENTDATA()提取XML数据,如LoginName、ClientHost、PostTime;CLIENT_HOST可能为IP或,真实客户端IP应取CONNECTIONPROPERTY('client_net_address');审计日志应写入数据库表并用TRY...CATCH容错。

直接用 AFTER LOGON 登录触发器 + EVENTDATA(),别碰 HOST_NAME() 或试图在 DML 触发器里查登录信息——它根本不在那个上下文里。
登录触发器必须建在 ALL SERVER 作用域
SQL Server 的登录事件只在服务器级别暴露,ON DATABASE 触发器收不到 LOGON 事件。建错位置会导致触发器完全不执行。
- 正确写法:
CREATE TRIGGER tr_logon_audit ON ALL SERVER FOR LOGON AS ... - 错误写法:
CREATE TRIGGER ... ON [master] FOR LOGON(语法报错)或ON DATABASE(事件不触发) - 触发器权限由
CREATE SERVER AUDIT权限控制,不是数据库级权限;普通用户需被显式授予ALTER ANY SERVER AUDIT才能创建
EVENTDATA() 返回 XML,必须用 .value() 提取字段
EVENTDATA() 不返回字符串或表,它返回 XML 类型值,直接 SELECT 会看到一长串 XML 文本,没法直接插入审计表。
- 提取登录名:
EVENTDATA().value('(/EVENT_INSTANCE/LoginName)[1]', 'sysname') - 提取客户端 IP:
EVENTDATA().value('(/EVENT_INSTANCE/ClientHost)[1]', 'nvarchar(128)')(注意:这个值在直连时是 IP,在代理后可能是主机名或) - 提取时间:
EVENTDATA().value('(/EVENT_INSTANCE/PostTime)[1]', 'datetime2'),比GETDATE()更准,它是连接建立瞬间的时间戳 - 别用
CAST(EVENTDATA() AS NVARCHAR(MAX))后再字符串解析——XML 结构可能变,且性能差
真实客户端 IP 要用 CONNECTIONPROPERTY('client_net_address')
EVENTDATA().value(.../ClientHost) 和 CONNECTIONPROPERTY('client_net_address') 是两回事:/ClientHost 来自登录握手包,client_net_address 来自 TCP 连接层,后者更可靠。
- 必须显式转换:
CAST(CONNECTIONPROPERTY('client_net_address') AS NVARCHAR(46)),IPv6 最长 46 字符 - 在登录触发器中调用安全,但不能在 DDL 或 DML 触发器中依赖它——那些触发器运行时连接可能已复用或变更
- 如果应用走 pgbouncer、HAProxy 或 Azure SQL 的网关,这个值也会变成中间节点地址,不是最终用户 IP
- 命名管道或共享内存连接下返回
NULL或127.0.0.1,这是网络栈限制,不是代码问题
避免用 xp_cmdshell 写日志文件
把登录信息用 xp_cmdshell + ECHO 追加到文本文件,看似简单,实则脆弱:权限失败、路径不存在、并发写入损坏、杀毒软件拦截都会让整个登录流程卡死甚至失败。
- 推荐写入数据库表:
INSERT INTO dbo.LoginAudit (login_name, client_ip, login_time, spid) VALUES (@login, @ip, @time, @@SPID) - 表结构至少包含:
login_name sysname、client_ip NVARCHAR(46)、login_time datetime2、spid smallint - 不要在触发器里做复杂逻辑或远程调用;登录触发器执行慢会拖慢所有新连接,严重时导致连接池耗尽
- 如果审计表写入失败,建议用
TRY...CATCH吞掉异常,否则登录会被拒绝——这点极容易被忽略
最常被跳过的细节是:登录触发器里 CONNECTIONPROPERTY 和 EVENTDATA() 都有效,但它们来源不同、精度不同、失效场景也不同。混用却不加注释,半年后自己都看不懂哪条记录对应哪个 IP。










