Kiedy mówisz „bez użycia wyzwalaczy”, masz na myśli jakieś wyzwalacze lub tylko wyzwalacze wiersz po wierszu w tabelach?
Pytam dlatego, że może być w stanie uzyskać to, co chcesz z rozsądnego korzystania z tej CONTEXT_INFO()
funkcji, ale trzeba by upewnić się, że SET CONTEXT_INFO
został wezwany prawidłowo przed swoimi operacje odbywają.
Jednym z takich sposobów może być wyzwalacz logowania na poziomie serwera (tj. Nie wyzwalacz na poziomie bazy danych / obiektu), na przykład:
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
Następnie możesz dodać domyślne ograniczenie do tabeli, aby zapisać kontekst (dla szybkości wstawiania):
ALTER TABLE cdc.schema_table_CT
ADD ContextInfo varbinary(128) NULL DEFAULT(CONTEXT_INFO())
Gdy już to zrobisz, możesz zapytać tę ContextInfo
kolumnę odrobiną kromki:
SELECT *
,spid = CONVERT(INT, SUBSTRING(ContextInfo, 1, 4))
,client = CONVERT(VARCHAR(64), SUBSTRING(ContextInfo, 5, 64))
FROM cdc.schema_table_CT
Technicznie rzecz biorąc, możesz to zrobić SUBSTRING
i CONVERT
takie rzeczy jako część domyślnego ograniczenia i po prostu zapisać tam adres IP klienta, ale może być szybsze przechowywanie tam całego kontekstu (jak ma to miejsce na każdym INSERT
) i wyodrębnianie wartości tylko w SELECT
kiedy ich potrzebujesz.
Mogę być skłonny owinąć wszystkie moje SUBSTRING
i CONVERT
wywołania w jednorzędową wbudowaną funkcję o wartościach tabelarycznych, co zrobię w razie CROSS APPLY
potrzeby. To utrzymuje logikę rozpakowywania w jednym miejscu:
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
Zauważ, że CONTEXT_INFO
jest to tylko 128 bajtów VARBINARY
. Jeśli potrzebujesz więcej danych, niż zmieścisz w 128 bajtach, utworzę tabelę do przechowywania wszystkich tych danych, wstawię jako wiersz dla tej „sesji” do tabeli w wyzwalaczu logowania i CONTEXT_INFO
ustawię wartość klucza zastępczego tej tabeli
Należy również zauważyć, że ponieważ jest to jedynie domyślne ograniczenie, dla odpowiednio uprzywilejowanego użytkownika nadpisanie danych kontekstowych w tabeli spoczynkowej jest banalne. Oczywiście to samo dotyczy również wszystkich innych kolumn w tabelach typu „audyt”.
Byłoby miło, gdyby była to utrwalona kolumna obliczeniowa, a nie domyślna, ale CONTEXT_INFO()
funkcja jest niedeterministyczna, więc nie da się (można użyć pewnych FUNCTION
sztuczek wokół VIEW
, ale nie zrobiłbym tego ).
Jest to również trywialne dla tego użytkownika z wystarczającym dostępem, aby zadzwonić do SET CONTEXT_INFO
siebie i zepsuć Ci dzień (np. Fałszywymi wartościami lub specjalnie spreparowanym przechowywanym zastrzykiem), więc traktuj zawartość z podejrzliwością i ostrożnością, koduj ją przed wyświetleniem i obsługuj wyjątki dobrze.
Jeśli chodzi o nazwę hosta, myślę, że ClientHost
element EVENTDATA()
daje ci adres IP (lub <local machine>
wskaźnik). Podczas technicznie przydałby CLR zrobić wyszukiwań odwrotnego DNS z powrotem do hosta, te wydają się być zbyt powolny, aby zrobić dla każdego INSERT
, więc polecam nie zrobić.
Jeśli musisz mieć nazwę hosta, możesz użyć zadania agenta SQL, aby okresowo wypełniać osobną tabelę bieżącymi dzierżawami z lokalnego serwera DHCP lub pliku strefy DNS, jako proces pozapasmowy i LEFT JOIN
do tego w przyszłe zapytania (lub zawiń skalar, FUNCTION
aby podać wartość domyślnego ograniczenia, na czas).
Znowu należy zauważyć, że jeśli aplikacja zawiera komponent publiczny, adresy IP i nazwy hostów są niewiarygodne (np. Z powodu NAT). Nawet jeśli nie jest publicznie dostępny, istnieje pewien zależny od czasu komponent do większości map adresów IP / nazw hostów, który może wymagać uwzględnienia.
Wreszcie, przed wdrożeniem wyzwalacza logowania, warto włączyć dedykowane połączenie administracyjne na serwerze. Jeśli wyzwalacz logowania ulegnie awarii, może uniemożliwić zalogowanie się wszystkim użytkownikom (w tym kontom 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
Jeśli zostaniesz zablokowany, DAC może zostać użyty do upuszczenia lub wyłączenia wyzwalacza logowania:
C:\> sqlcmd -S localhost -d master -A
1> DISABLE TRIGGER tr_audit_login ON ALL SERVER
2> GO