Ketika Anda mengatakan, "tanpa menggunakan pemicu", maksud Anda setiap pemicu atau hanya baris-demi-baris memicu pada tabel?
Saya bertanya karena Anda mungkin bisa mendapatkan apa yang Anda inginkan dengan penggunaan CONTEXT_INFO()
fungsi yang bijaksana , tetapi Anda perlu memastikan bahwa SET CONTEXT_INFO
itu dipanggil dengan benar sebelum operasi Anda berlangsung.
Satu tempat untuk melakukan itu mungkin pemicu masuk tingkat server (yaitu bukan pemicu tingkat basis data / objek), seperti:
USE master
GO
CREATE TRIGGER tr_audit_login
ON ALL SERVER
WITH EXECUTE AS 'sa'
AFTER LOGON
AS BEGIN
BEGIN TRY
DECLARE @eventdata XML = EVENTDATA();
IF @eventdata IS NOT NULL BEGIN
DECLARE @spid INT;
DECLARE @client_host VARCHAR(64);
SET @client_host = @eventdata.value('(/EVENT_INSTANCE/ClientHost)[1]', 'VARCHAR(64)');
SET @spid = @eventdata.value('(/EVENT_INSTANCE/SPID)[1]', 'INT');
-- pack the required data into the context data binary
-- (spid is just an example of packing multiple data items in a single field: you would probably use @@SPID at the point of use, instead)
DECLARE @context_data VARBINARY(128);
SET @context_data = CONVERT(VARBINARY(4), @spid)
+ CONVERT(VARBINARY(64), @client_host);
-- persist the spid and host into session-level memory
SET CONTEXT_INFO @context_data;
END
END TRY
BEGIN CATCH
/* do better error handling here...
* logon trigger can lock all users out of server, so i am just swallowing everything
*/
DECLARE @msg NVARCHAR(4000) = ERROR_MESSAGE();
RAISERROR('%s', 10, 1, @msg) WITH LOG;
END CATCH
END
Anda kemudian dapat menambahkan batasan default ke tabel Anda, untuk menyimpan konteks (untuk kecepatan memasukkan):
ALTER TABLE cdc.schema_table_CT
ADD ContextInfo varbinary(128) NULL DEFAULT(CONTEXT_INFO())
Setelah memilikinya, Anda dapat menanyakan ContextInfo
kolom itu dengan sedikit irisan:
SELECT *
,spid = CONVERT(INT, SUBSTRING(ContextInfo, 1, 4))
,client = CONVERT(VARCHAR(64), SUBSTRING(ContextInfo, 5, 64))
FROM cdc.schema_table_CT
Secara teknis, Anda bisa melakukan itu SUBSTRING
dan CONVERT
hal - hal sebagai bagian dari batasan default Anda, dan hanya menyimpan IP klien di sana, tetapi mungkin lebih cepat untuk menyimpan seluruh konteks di sana (seperti yang dilakukan pada setiap INSERT
), dan hanya mengekstrak nilai dalam SELECT
ketika Anda membutuhkannya.
Saya mungkin cenderung untuk membungkus semua panggilan saya SUBSTRING
dan CONVERT
dalam fungsi baris-tabel inline bernilai tunggal, yang saya akan CROSS APPLY
bila diperlukan. Itu menjaga logika pembongkaran di satu tempat:
CREATE FUNCTION fn_context (
@context_info VARBINARY(128)
)
RETURNS TABLE
AS RETURN (
SELECT
spid = CONVERT(INT, SUBSTRING(@context_info, 1, 4))
,client = CONVERT(VARCHAR(64), SUBSTRING(@context_info, 5, 64))
)
GO
SELECT *
FROM cdc.schema_table_CT s
CROSS APPLY dbo.fn_context(s.ContextInfo) c
Perhatikan bahwa CONTEXT_INFO
hanya 128 byte VARBINARY
. Jika Anda membutuhkan lebih banyak data daripada yang dapat Anda muat dalam 128 byte, saya akan membuat tabel untuk menampung semua data itu, masukkan sebagai baris untuk 'sesi' itu ke dalam tabel di pemicu masuk dan setel CONTEXT_INFO
ke nilai kunci pengganti dari tabel itu.
Anda juga harus mencatat bahwa, karena ini hanya kendala standar, sepele bagi pengguna yang memiliki hak istimewa untuk menimpa data konteks itu di tabel at-rest. Tentu saja, hal yang sama juga berlaku untuk semua kolom lainnya dalam tabel gaya audit.
Akan lebih baik jika itu bisa menjadi kolom yang dihitung terus-menerus, daripada default, tetapi CONTEXT_INFO()
fungsinya adalah non-deterministik, jadi ini adalah no-go (Anda mungkin dapat menggunakan beberapa FUNCTION
tipu daya di sekitar a VIEW
, tapi saya tidak akan melakukannya. ).
Hal ini juga sepele bagi pengguna dengan akses yang cukup untuk menyebut SET CONTEXT_INFO
diri mereka sendiri dan mengacaukan hari Anda (misalnya dengan nilai-nilai palsu, atau injeksi tersimpan yang dibuat khusus), jadi perlakukan konten dengan kecurigaan dan kehati-hatian, disandikan sebelum ditampilkan, dan tangani pengecualian. baik.
Adapun nama host, saya pikir ClientHost
elemen EVENTDATA()
memberi Anda alamat IP (atau <local machine>
indikator). Meskipun Anda secara teknis dapat menggunakan CLR untuk melakukan pencarian reverse-DNS kembali ke nama host, ini cenderung terlalu lambat untuk dilakukan untuk setiap INSERT
, jadi saya akan merekomendasikan untuk tidak melakukannya.
Jika Anda harus memiliki nama host, Anda mungkin ingin menggunakan pekerjaan SQL Agent untuk secara berkala mengisi tabel terpisah dengan sewa saat ini dari server DHCP lokal atau file zona DNS, sebagai proses out-of-band, dan LEFT JOIN
untuk itu dalam kueri mendatang (atau bungkus skalar FUNCTION
untuk memberikan nilai pada batasan default, untuk point-in-time).
Sekali lagi, Anda harus mencatat bahwa, jika aplikasi memiliki komponen yang menghadap publik, alamat IP dan nama host tidak dapat diandalkan (misalnya karena NAT). Bahkan jika itu tidak menghadap ke publik, ada komponen berbasis waktu tertentu untuk sebagian besar peta IP / hostname, yang mungkin perlu Anda perhitungkan.
Terakhir, sebelum mengimplementasikan pemicu masuk Anda, mungkin perlu menyalakan koneksi admin khusus server Anda. Jika pemicu masuk terputus dengan cara apa pun, itu dapat mencegah semua pengguna masuk (termasuk akun sysadmin):
USE master
GO
-- you may want to do this, so you have a back-out if the login trigger breaks login
EXEC sp_configure 'remote admin connections', 1
GO
RECONFIGURE
GO
Jika Anda terkunci, DAC dapat digunakan untuk menjatuhkan atau menonaktifkan pemicu masuk:
C:\> sqlcmd -S localhost -d master -A
1> DISABLE TRIGGER tr_audit_login ON ALL SERVER
2> GO