SQL Server 限制IP登陸(登陸觸發器運用)

一.本文所涉及的內容(Contents)

  1. 本文所涉及的內容(Contents)
  2. 背景(Contexts)
  3. 實現代碼(SQL Codes)
  4. 補充說明(Addon)
  5. 疑問(Questions)
  6. 參考文獻(References)

二.背景(Contexts)

  在MySQL的mysql.User表保存了登陸用戶的權限信息,Host和User字段則是關於登陸IP的限制。但是在SQL Server沒有這樣一個表,那SQL Server有什麼辦法可以實現類似的安全控制的功能呢?

  SQL Server 包括三種常規類型的觸發器:DML觸發器、DDL觸發器和登錄觸發器。DML觸發器是比較常使用的,它通常在表或視圖中修改數據(INSERT、UPDATE和DELETE 等)爲了保證業務數據的完整性和一致性,可以對事務進行回滾等操作;如果你對DDL觸發器感興趣,可以參考:SQL Server DDL觸發器運用,裏面涉及到DDL觸發器的知識;登陸觸發器將在本文運用到關於IP登陸限制的解決方案中。

  登陸觸發器能爲我們解決什麼問題呢?本文將爲你講述5種運用登陸觸發器的場景:

1) 限制某登錄名(比如sa)只能在本機或者指定的IP中登陸;

2) 限制服務器角色(比如sysadmin)只能在本機或者指定的IP中登陸;

3) 限制某登錄名(比如sa)只能某時間段內登陸;

4) 限制登錄名與IP的對應關係,支持多對多關係;

5) 限制某登錄名可以在某IP段登錄(比如192.168.1.*),如下圖;

image

三.實現代碼(SQL Codes)

(一) 我機器的IP是:192.168.1.48,首先我在數據庫創建一個test帳號,設置密碼爲123,接着創建登陸觸發器:tr_connection_limit,它會在用戶登陸的時候觸發,通過EVENTDATA()函數返回的客戶端的IP,使用ORIGINAL_LOGIN()函數返回的登陸名,對IP和登錄名進行判斷。

  當登錄名是test的時候,如果登陸的IP地址本地<local machine>或者是192.168.1.50,192.168.1.120就允許登陸,在這之外其它情況的登陸將回滾。登陸失敗的如Figure1所示。

複製代碼
--Script1:創建test登錄賬號
CREATE LOGIN test WITH PASSWORD = '123'
GO

-- =============================================
-- Author:        <聽風吹雨>
-- Create date:    <2013.05.21>
-- Description:    <限制test用戶只能在本機和指定的IP中登陸>
-- Blog:        <http://www.cnblogs.com/gaizai/>
-- =============================================
CREATE TRIGGER [tr_connection_limit]
ON ALL SERVER WITH EXECUTE AS 'sa'
FOR LOGON
AS
BEGIN

--限制test這個帳號的連接
IF ORIGINAL_LOGIN()= 'test'
--允許test在本機和下面的IP登錄
AND
(SELECT EVENTDATA().value('(/EVENT_INSTANCE/ClientHost)[1]', 'NVARCHAR(15)'))
NOT IN('<local machine>','192.168.1.50','192.168.1.120')
     ROLLBACK;
END;
複製代碼

clip_image002

(Figure1:test用戶登陸錯誤信息)

  我在一臺IP爲192.168.1.115的機器上使用test登陸名登陸我的SQL Server數據庫,因爲這個IP不在允許的IP列表中,所以出現了Figure1的錯誤信息。我再使用一臺IP爲192.168.1.120的機器登陸我的SQL Server數據庫,成功登陸了,使用Script2返回登陸的信息;如Figure2,請看session_id爲58的記錄:登陸名爲test,登陸的IP爲192.168.1.120。

複製代碼
--Script2:返回登錄信息
SELECT 
a.[session_id],a.[login_time],a.[host_name],
a.[original_login_name],b.[client_net_address]
FROM MASTER.sys.dm_exec_sessions a 
INNER JOIN MASTER.sys.dm_exec_connections b 
ON a.session_id=b.session_id
複製代碼

clip_image004

(Figure2:用戶登陸信息)

  關於Script1腳本中EXECUTE AS的用法可以參考:EXECUTE AS (Transact-SQL),ORIGINAL_LOGIN()函數可以參考:ORIGINAL_LOGIN (Transact-SQL),EVENTDATA()函數用法可以參考:EVENTDATA (Transact-SQL)

 

(二) 有些時候,你數據庫可能有許多個登陸帳號,而你更希望的是限制IP,而登陸名跟IP並沒有直接的關聯,那這應該怎麼實現呢?

  首先創建一個Logon_DB數據庫,再創建一個ValidIP表,在表中插入<local machine>和192.168.1.195,表示允許本地和IP爲192.168.1.195進行登陸,登陸的帳號屬於服務器角色:sysadmin。

複製代碼
--Script3:
--創建測試數據庫
USE MASTER
GO
CREATE DATABASE Logon_DB

--創建IP過濾表
USE Logon_DB
GO
CREATE TABLE dbo.ValidIP ( 
    IP NVARCHAR(15), 
    CONSTRAINT PK_ValidIP PRIMARY KEY CLUSTERED(IP) 
); 
GO

--插入過濾IP
USE Logon_DB
GO
INSERT INTO dbo.ValidIP(IP) VALUES('<local machine>');
INSERT INTO dbo.ValidIP(IP) VALUES('192.168.1.195');

--創建登錄觸發器
-- =============================================
-- Author:        <聽風吹雨>
-- Create date:    <2013.05.21>
-- Description:    <限制本機和指定的IP登陸>
-- Blog:        <http://www.cnblogs.com/gaizai/>
-- =============================================
CREATE TRIGGER [tr_logon_CheckIP] 
ON ALL SERVER 
FOR LOGON 
AS 
BEGIN 
    IF IS_SRVROLEMEMBER('sysadmin') = 1 
    BEGIN 
        DECLARE @IP NVARCHAR(15); 
        SET @IP = (SELECT EVENTDATA().value('(/EVENT_INSTANCE/ClientHost)[1]', 'NVARCHAR(15)')); 
        IF NOT EXISTS(SELECT IP FROM [Logon_DB].[dbo].[ValidIP] WHERE IP = @IP) 
        ROLLBACK;
    END;
END;
複製代碼

  這次我們在IP爲:192.168.1.120的機器上進行測試,這個IP之前是允許使用test帳號登陸的(tr_connection_limit),這次使用sa這個帳號登陸,返回了Figure3的錯誤信息,這是因爲它違反了登陸觸發器tr_logon_CheckIP的規則。

clip_image006

(Figure3:sa用戶登陸錯誤信息)

  在IP爲192.168.1.195的機器上使用sa這個帳號成功登陸,再次執行Script2腳本,返回的列表如Figure4所示。

clip_image008

(Figure4:用戶登陸信息)

 

(三) 還有一種場景,我們需要限制某些用戶只能在指定的時間內登陸數據庫,比如業務上某些運用只能在晚上跑的,通過這個登陸觸發器,可以防止運用修改執行時間在白天中運行。

  首先我們創建一個名爲nightworker的登陸名,再創建一個LogonBlockedLog的登陸攔截日誌表,接着創建登陸觸發器:tr_logon_CheckTime,在早上7:00之後至晚上18:00之前(BETWEEN 7 AND 17)是不允許nightworker帳號登陸數據庫的。

複製代碼
--Script4:
--創建nightworker登錄賬號
CREATE LOGIN nightworker WITH PASSWORD = '123'
GO

--創建登錄攔截日誌表
USE Logon_DB
GO
CREATE TABLE dbo.LogonBlockedLog ( 
    [Id] INT IDENTITY(1,1),
    [session_id] SMALLINT,
    [login_time] DATETIME,
    [host_name] NVARCHAR(128),
    [original_login_name] NVARCHAR(128),
    [client_net_address] VARCHAR(48),
    CONSTRAINT PK_LogonLog PRIMARY KEY CLUSTERED(Id) 
); 

--創建登錄觸發器
-- =============================================
-- Author:        <聽風吹雨>
-- Create date:    <2013.05.21>
-- Description:    <限制登陸時間>
-- Blog:        <http://www.cnblogs.com/gaizai/>
-- =============================================
CREATE TRIGGER [tr_logon_CheckTime]
ON ALL SERVER WITH EXECUTE AS 'sa'
FOR LOGON
AS
BEGIN
    IF ORIGINAL_LOGIN()='nightworker' AND
    DATEPART(hh,GETDATE()) BETWEEN 7 AND 17
    BEGIN
        ROLLBACK;
        INSERT INTO [Logon_DB].[dbo].[LogonBlockedLog]
            ([session_id]
            ,[login_time]
            ,[host_name]
            ,[original_login_name]
            ,[client_net_address])
        SELECT 
            a.[session_id],a.[login_time],a.[host_name],
            a.[original_login_name],b.[client_net_address]
            FROM MASTER.sys.dm_exec_sessions a 
            INNER JOIN MASTER.sys.dm_exec_connections b 
            ON a.session_id=b.session_id
            WHERE a.session_id = @@SPID
    END;
END;
複製代碼

  現在時間是17:20左右,我使用nightworker登陸數據庫,這違反了登陸觸發器:tr_logon_CheckTime,所以提示Figure5的錯誤信息,並且在LogonBlockedLog攔截日誌表中出現了一條記錄,這個表可以幫助我們更好的瞭解登陸賬號的登陸信息。

clip_image010

(Figure5:nightworker用戶登陸錯誤信息)

clip_image011

(Figure6:攔截日誌表)

 

(四) 我們進一步模仿MySQL的mysql.User表的用法,用表保存用戶與IP的對應關係,這樣就可以對所有登陸用戶進行控制了。

複製代碼
--Script5:
--登陸名與有效IP對應表
USE Logon_DB
GO
CREATE TABLE [dbo].[ValidLogOn](
    [Id] INT IDENTITY(1,1) NOT NULL,
    [LoginName] [sysname] NOT NULL,
    [ValidIP] [nvarchar](15) NOT NULL,
    CONSTRAINT [PK_ValidLogOn] PRIMARY KEY CLUSTERED ([Id])
 )
--創建唯一約束索引
CREATE UNIQUE NONCLUSTERED INDEX [IX_ValidLogOn_LV] ON [dbo].[ValidLogOn] 
(
    [LoginName] ASC,
    [ValidIP] ASC
)
 --插入測試數據
INSERT [dbo].[ValidLogOn] ([LoginName], [ValidIP]) VALUES (N'BARXXX\Administrator', N'<local machine>')
INSERT [dbo].[ValidLogOn] ([LoginName], [ValidIP]) VALUES (N'nightworker', N'<local machine>')
INSERT [dbo].[ValidLogOn] ([LoginName], [ValidIP]) VALUES (N'nightworker', N'192.168.1.48')
INSERT [dbo].[ValidLogOn] ([LoginName], [ValidIP]) VALUES (N'sa', N'<local machine>')
INSERT [dbo].[ValidLogOn] ([LoginName], [ValidIP]) VALUES (N'sa', N'127.0.0.1')
INSERT [dbo].[ValidLogOn] ([LoginName], [ValidIP]) VALUES (N'sa', N'192.168.1.48')
INSERT [dbo].[ValidLogOn] ([LoginName], [ValidIP]) VALUES (N'test', N'<local machine>')
INSERT [dbo].[ValidLogOn] ([LoginName], [ValidIP]) VALUES (N'test', N'192.168.1.120')
INSERT [dbo].[ValidLogOn] ([LoginName], [ValidIP]) VALUES (N'test', N'192.168.1.48')
INSERT [dbo].[ValidLogOn] ([LoginName], [ValidIP]) VALUES (N'test', N'192.168.1.50')
--創建登錄觸發器
-- =============================================
-- Author:        <聽風吹雨>
-- Create date:    <2013.05.21>
-- Description:    <限制登陸名和IP>
-- Blog:        <http://www.cnblogs.com/gaizai/>
-- =============================================
CREATE TRIGGER [tr_logon_CheckLogOn]
ON ALL SERVER WITH EXECUTE AS 'sa'
FOR LOGON
AS
BEGIN
    DECLARE @LoginName sysname
    DECLARE @IP NVARCHAR(15)
    SET @LoginName = ORIGINAL_LOGIN();
    SET @IP = (SELECT EVENTDATA().value('(/EVENT_INSTANCE/ClientHost)[1]', 'NVARCHAR(15)')); 
    --判斷登錄名和IP
    IF NOT EXISTS(SELECT [ValidIP] FROM [Logon_DB].[dbo].[ValidLogOn] WHERE [LoginName] = @LoginName AND [ValidIP] = @IP) 
        ROLLBACK;
END;
複製代碼

clip_image012

(Figure7:登陸名與有效IP對應表)

  用戶登陸名與IP對應關係表[ValidLogOn],有幾點需要注意的,BARXXX\Administrator這個是Windows 身份驗證中操作系統的帳號,你需要根據你的實際情況進行修改;IP當中你則需要注意<local machine>和127.0.0.1這些特殊的地址,我個人還是建議在這個表中加入這些信息的。

 

(五) 對上面的再延伸一點,如果想類似Host like 192.168.1.* 這樣進行範圍的過濾,那這又應該怎麼實現呢?

  可以使用CLR擴展函數對IP進行判斷,後面會講到這種方式。這裏使用SQL就能解決的方法,僅供參考。開放登錄名nightworker在內網所有IP:192.168.1.* 訪問本機的權限。

複製代碼
--Script6:
--插入測試數據
INSERT [dbo].[ValidLogOn] ([LoginName], [ValidIP]) VALUES (N'nightworker', N'192.168.1.*')

--創建登錄觸發器
-- =============================================
-- Author:        <聽風吹雨>
-- Create date:    <2013.05.21>
-- Description:    <登陸名和IP過濾,支持IP範圍規範>
-- Blog:        <http://www.cnblogs.com/gaizai/>
-- =============================================
CREATE TRIGGER [tr_logon_CheckLogOn_RangeIP]
ON ALL SERVER WITH EXECUTE AS 'sa'
FOR LOGON
AS
BEGIN
    DECLARE @LoginName sysname
    DECLARE @IP NVARCHAR(15)
    DECLARE @ValidIP NVARCHAR(15)
    DECLARE @len INT
    DECLARE @data XML
    DECLARE @blocked BIT;
    
    SET @len = 0
    SET @blocked = 0
    SET @LoginName = ORIGINAL_LOGIN();
    SET @data = EVENTDATA();
    SET @IP = @data.value('(/EVENT_INSTANCE/ClientHost)[1]', 'NVARCHAR(15)');
    
    --判斷登錄名和IP
    IF NOT EXISTS(SELECT [ValidIP] FROM [Logon_DB].[dbo].[ValidLogOn] WHERE [LoginName] = @LoginName AND [ValidIP] = @IP) 
    BEGIN
        --是否存在IP範圍匹配
        SET @ValidIP = (SELECT TOP 1 [ValidIP] FROM [Logon_DB].[dbo].[ValidLogOn] 
            WHERE [LoginName] = @LoginName AND [ValidIP] LIKE '%[*]');
        --如果存在就替換Client的IP
        IF (CHARINDEX('*',@ValidIP) > 0 AND @IP <> '<local machine>' AND @IP <> '127.0.0.1')
        BEGIN
            DECLARE @SubValidIP NVARCHAR(15)
            SET @SubValidIP = SUBSTRING(@ValidIP,0,CHARINDEX('*',@ValidIP))
            SET @len = LEN(@SubValidIP) + 1
            IF(SUBSTRING(@IP,0,@len) != @SubValidIP)
            BEGIN
                ROLLBACK;
                SET @blocked = 1
            END
        END
        ELSE
        BEGIN
            ROLLBACK;
            SET @blocked = 1
        END
    END

    --日誌記錄
    INSERT INTO [Logon_DB].[dbo].[LogonLog]
        ([session_id]
        ,[login_time]
        ,[host_name]
        ,[original_login_name]
        ,[client_net_address]
        ,[XmlEvent]
        ,[Blocked])
    SELECT 
        @data.value('(/EVENT_INSTANCE/SPID)[1]', 'smallint'),
        GETDATE(),
        @data.value('(/EVENT_INSTANCE/ServerName)[1]', 'sysname'),
        @data.value('(/EVENT_INSTANCE/LoginName)[1]', 'sysname'),
        @data.value('(/EVENT_INSTANCE/ClientHost)[1]', 'NVARCHAR(15)'),
        @data,@blocked
END;
複製代碼

image

(Figure7_1:添加192.168.1.*後)

clip_image014

(Figure8:nightworker用戶登錄通過)

clip_image016

(Figure9:sa用戶登錄攔截)

  從Figure8和Figure9的對比可以知道,在同一臺機器192.168.1.208使用nightworker和sa有不同的效果,nightworker用戶登錄成功,sa用戶登錄被攔截了。

 

(六) 這裏使用CLR擴展函數對上面類似Host like 192.168.1.* 的實現,關於CLR的一些基礎可以參考:SQL Server擴展函數的基本概念

  新建程序集(引用一個寫好的SQLCLR.dll文件)之後執行下面的SQL腳本創建標量值函數,創建成功後效果如下圖所示:

複製代碼
--Script7:
--CLR實現IP範圍判斷
CREATE FUNCTION [dbo].[RegexIsMatch](@input [nvarchar](max), @pattern [nvarchar](4000))
RETURNS [nvarchar](4000) WITH EXECUTE AS CALLER
AS 
EXTERNAL NAME [SQLCLR].[UserDefinedFunctions].[RegexIsMatch]
GO

EXEC sys.sp_addextendedproperty @name=N'SqlAssemblyFile', @value=N'SQLRegex.cs' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'FUNCTION',@level1name=N'RegexIsMatch'
GO

EXEC sys.sp_addextendedproperty @name=N'SqlAssemblyFileLine', @value=N'20' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'FUNCTION',@level1name=N'RegexIsMatch'
GO
複製代碼

clip_image017

(Figure10:註冊成功後)

複製代碼
--Script8:
--創建登錄觸發器
-- =============================================
-- Author:        <聽風吹雨>
-- Create date:    <2013.05.21>
-- Description:    <登陸名和IP過濾,支持IP範圍規範>
-- Blog:        <http://www.cnblogs.com/gaizai/>
-- =============================================
CREATE TRIGGER [tr_logon_CheckLogOn_RangeIP_ByCRL]
ON ALL SERVER WITH EXECUTE AS 'sa'
FOR LOGON
AS
BEGIN
    DECLARE @LoginName sysname
    DECLARE @IP NVARCHAR(15)
    DECLARE @ValidIP NVARCHAR(15)
    DECLARE @len INT
    DECLARE @data XML
    DECLARE @blocked BIT;
    
    SET @len = 0
    SET @blocked = 0
    SET @LoginName = ORIGINAL_LOGIN();
    SET @data = EVENTDATA();
    SET @IP = @data.value('(/EVENT_INSTANCE/ClientHost)[1]', 'NVARCHAR(15)');
    
    --判斷登錄名和IP
    IF NOT EXISTS(SELECT [LoginName] FROM [Logon_DB].[dbo].[ValidLogOn] WHERE [LoginName] = @LoginName AND [ValidIP] = @IP) 
    BEGIN
        SET @ValidIP = (SELECT TOP 1 [ValidIP] FROM [Logon_DB].[dbo].[ValidLogOn] 
            WHERE [LoginName] = @LoginName AND [ValidIP] LIKE '%[*]');
        --是否存在IP範圍匹配
        IF (CHARINDEX('*',@ValidIP) > 0 AND @IP <> '<local machine>' AND @IP <> '127.0.0.1' AND dbo.RegexIsMatch(@IP,@ValidIP) = 'True')
            SET @blocked = 0
        ELSE
        BEGIN    
            ROLLBACK;
            SET @blocked = 1
        END
    END

    --日誌記錄
    INSERT INTO [Logon_DB].[dbo].[LogonLog]
        ([session_id]
        ,[login_time]
        ,[host_name]
        ,[original_login_name]
        ,[client_net_address]
        ,[XmlEvent]
        ,[Blocked])
    SELECT 
        @data.value('(/EVENT_INSTANCE/SPID)[1]', 'smallint'),
        GETDATE(),
        @data.value('(/EVENT_INSTANCE/ServerName)[1]', 'sysname'),
        @data.value('(/EVENT_INSTANCE/LoginName)[1]', 'sysname'),
        @data.value('(/EVENT_INSTANCE/ClientHost)[1]', 'NVARCHAR(15)'),
        @data,@blocked
END;
複製代碼

四.補充說明(Addon)

(一) 我有一臺服務器A在本地無法使用SSMS登陸(2005),原因是因爲我在A上重新安裝DotnetFramework的時候失敗了,但是不影響程序鏈接A的數據庫,在服務器B也可以使用SSMS鏈接到服務器A,我維護數據庫有時候需要在單用戶下進行表分區的維護(鎖比較多),如果在正常情況下,我只需要在A停止TCP/IP就可以阻止其它用戶登陸,那這種情況有什麼辦法解決呢?對的,讓指定的服務器B的IP能訪問服務器A的數據庫,寫個登陸觸發器,重啓SQL Server服務,維護完之後刪除登陸觸發器,具體的SQL代碼可以參考Script1的登陸觸發器:tr_connection_limit。

clip_image019

(Figure11:A服務器SSMS打開錯誤)

(二) 如果在登陸觸發器中需要讀取表[Logon_DB].[dbo].[ValidLogOn],如果在ON ALL SERVER後面沒有加入WITH EXECUTE AS 'sa',當你使用test或者nightworker登陸就會一直報錯,因爲test和nightworker是沒有權限讀取[Logon_DB].[dbo].[ValidLogOn]表。而tr_logon_CheckIP之所以不用WITH EXECUTE AS 'sa'是因爲這本身就是對服務器角色sysadmin的邏輯處理。

(三) 測試本地登陸的情況的時候需要測試[.]、[local]、[localhost]、[127.0.0.1]、[ipconfig]裏面顯示的內網IP地址這五種情況。([.]、[local]、[localhost]在EVENTDATA()的ClientHost標籤都是顯示<local machine>)

五.疑問(Questions)

(一) 像Figure1、Figure3和Figure5等並沒有清晰顯示登陸錯誤信息。比如:錯誤是什麼原因造成的?是哪個登陸觸發器攔截的?攔截規則是什麼?爲了方便用戶清晰瞭解規則,我們需要自定義這些錯誤內容。

(二) 如果在tr_logon_CheckIP觸發器的ROLLBACK之前加入Print語句會出錯,錯誤信息就如Figure3所示,原來可以登陸的,加了這個Print就不行了?爲什麼?

(三) 創建登陸觸發器要在服務器角色:sysadmin(比如sa)的權限下執行Create腳本,不然會報錯,即使使用了WITH EXECUTE AS 'sa'選項也同樣報錯,具體的官方文檔說明還沒找到。

(四) 爲什麼在服務器名稱使用localhost登陸的時候會有3條記錄插入到[LogonLog]表的呢?

clip_image021

(Figure12:一次登陸3條記錄)

六.參考文獻(References)

Blocking Users by IP

SQL Server 2008中的代碼安全(二):DDL觸發器與登錄觸發器

CREATE TRIGGER (Transact-SQL)

登錄觸發器

Sql server限制IP訪問方法

sys.dm_exec_sessions (Transact-SQL)

EXECUTE AS (Transact-SQL)

ORIGINAL_LOGIN (Transact-SQL)

EVENTDATA (Transact-SQL)

淺談SQL Server 數據庫之觸發器

事件通知

作者:聽風吹雨 
出處: http://www.cnblogs.com/gaizai/ 
郵箱:[email protected] 
版權:本文版權歸作者和博客園共有 
轉載:歡迎轉載,必須保留原文鏈接 
格言:不喜歡是因爲不會 && 因爲會所以喜歡

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章