1. 程式人生 > 實用技巧 >【sql server安全】sql server列加密查詢效能問題及解決方案

【sql server安全】sql server列加密查詢效能問題及解決方案

摘要

在SQL Server安全系列專題月報分享中,我們已經分享了:如何使用對稱金鑰實現SQL Server列加密技術使用非對稱金鑰加密方式實現SQL Server列加密使用混合金鑰實現SQL Server列加密技術三篇文章。本期月報我們分享列加密技術帶來的查詢效能問題以及相應的解決方案。

問題引入

根據SQL Server安全系列專題前三篇的月報分享,我們已經可以非常輕鬆的實現SQL Server的列加密,來保護我們關鍵資料列的安全性。但是,如果我們需要使用加密列來做為條件查詢的話,會導致SQL Server No-SARG查詢,進而導致查詢效能低下。比如:在我們場景中,使用電話號碼做為查詢、更新、刪除客戶資訊的條件。

電話號碼條件查詢

在很多種場景中,業務系統需要通過電話號碼來查詢客戶詳細資訊,但是由於我們已經將電話號碼加密儲存,於是查詢語句必須先將電話號碼密文解密後為明文後,再做查詢。比如,我們需要查詢電話號碼為13487759293的客戶詳細資訊,查詢語句會是:

USE [TestDb]
GO

OPEN SYMMETRIC KEY SymKey_TestDb
DECRYPTION BY ASYMMETRIC KEY AsymKey_TestDb WITH PASSWORD = 'Password4@Asy';

SELECT 
	*,
	DescryptedCustomerPhone = CONVERT(CHAR(11), DecryptByKey(EncryptedCustomerPhone))
FROM dbo.CustomerInfo
WHERE CONVERT(CHAR(11), DecryptByKey(EncryptedCustomerPhone)) = '13487759293'

/** 
UPDATE A
SET EncryptedCustomerPhone = EncryptByKey( Key_GUID('SymKey_TestDb'), '13487759293')
FROM dbo.CustomerInfo AS A
WHERE CONVERT(CHAR(11), DecryptByKey(EncryptedCustomerPhone)) = '13487759293'

DELETE A
FROM dbo.CustomerInfo AS A
WHERE CONVERT(CHAR(11), DecryptByKey(EncryptedCustomerPhone)) = '13402872514'
**/

CLOSE SYMMETRIC KEY SymKey_TestDb;
GO

查詢結果為:這個使用電話號碼做為查詢條件的語句,會導致查詢語句存在非常大的效能問題: 在WHERE語句中使用函式運算解密電話號碼密文,即No-SARG查詢 電話號碼密文欄位EncryptedCustomerPhone無法建立索引 類似導致查詢效能問題會同樣出現在客戶資訊更細、客戶資訊刪除等場景中,詳細的原理分析及解決方案,參加下一章節。

原理分析

為什麼說使用電話號碼做為查詢條件,會帶來非常大的效能問題呢,這一章節將從以下兩個方面來進行分析:

No-SARG查詢

寬欄位無法建立索引

No-SARG查詢

由於使用者只知道電話號碼的明文,即查詢條件的輸入端是明文,而在資料庫的表中,儲存的是電話號碼加密過的密文。如果要使用電話號碼做為查詢條件,必須解密電話號碼密文後,再與電話號碼明文匹配。即WHERE語句呈現如下的寫法: WHERE CONVERT(CHAR(11), DecryptByKey(EncryptedCustomerPhone)) = ‘13487759293’ 這種在WHERE語句對錶中正式欄位進行函式運算的查詢是典型的No-SARG查詢,會導致SQL Server Scan表中該欄位的所有值,導致IO,CPU資源的極大消耗,進而導致查詢時間消耗過長,效能低下。 在這裡,可能會有人挑戰說,為什麼不先加密電話號碼為密文後,再與資料庫表中的電話號碼密文進行對比呢?這樣就不會對錶欄位進行函式運算了嗎?這個方法提的非常好,這也是我們平時解決No-SARG查詢的思路。但是,在這個場景中變得行不通了,如下示例我們針對同一個電話號碼明文加密,出來的密文是完全不一致:

DECLARE
	@phone VARCHAR(11) = '13880975623'
;
SELECT 
	encrypted_phone_1 = EncryptByKey( Key_GUID('SymKey_TestDb'), @phone), 
	encrypted_phone_2 = EncryptByKey( Key_GUID('SymKey_TestDb'), @phone)
;

如下結果,加密同一個電話號碼明文13880975623,加密密文encrypted_phone_1和encrypted_phone_2值卻完全不一樣。因此,採用加密電話號碼明文後,再與表中欄位資料匹配的方法不可行。

寬欄位無法建立索引

由於建立索引的欄位寬度,最大不允許超過900 bytes,但是EncryptByKey函式最多可能會返回8000個bytes,加之EncryptedCustomerPhone欄位定義為varbinary(MAX)。因此,電話號碼密文欄位不允許建立索引,嘗試建立索引。

USE [TestDb]
GO
CREATE INDEX ix_EncryptedCustomerPhone 
ON dbo.CustomerInfo(EncryptedCustomerPhone)
;

會報告如下錯誤:

解決方案

我們可以建立一個列,用於存放使用者電話號碼明文Hash值,然後再該Hash列上建立索引。查詢的時候,先將電話號碼明文計算Hash值,再與該Hash列進行匹配查詢到對應的行即可。

解決方法

詳細的解決方法有如下幾個步驟:

新增Hash列

初始化Hash列資料

建立Hash列索引

新增資料行

新增Hash列

我們選擇CHECKSUM函式來計算電話號碼的Hash值,因此,新增一個INT資料型別的CustomerPhone_Hashkey列即可。

USE [TestDb]
GO
ALTER TABLE dbo.CustomerInfo
ADD CustomerPhone_Hashkey INT NULL
;
GO

初始化Hash列資料

初始化Hash列資料時,由於電話號碼已經加密為密文,需要先將其解密出來,然後再計算Hash值。

USE [TestDb]
GO
OPEN SYMMETRIC KEY SymKey_TestDb
DECRYPTION BY ASYMMETRIC KEY AsymKey_TestDb WITH PASSWORD = 'Password4@Asy';


WHILE EXISTS(
			SELECT TOP 1 *
			FROM dbo.CustomerInfo WITH(NOLOCK)
			WHERE CustomerPhone_Hashkey IS NULL
)
BEGIN
	UPDATE TOP(10000) A
	SET CustomerPhone_Hashkey = CHECKSUM(CONVERT(CHAR(11), DecryptByKey(EncryptedCustomerPhone)))
	FROM dbo.CustomerInfo AS A
	WHERE CustomerPhone_Hashkey IS NULL
	
	WAITFOR DELAY '00:00:01'
END
CLOSE SYMMETRIC KEY SymKey_TestDb;
GO

建立Hash列索引

在電話號碼Hash值列上,建立相應的索引。

USE [TestDb]
GO
CREATE INDEX IX_CustomerPhone_Hashkey 
ON dbo.CustomerInfo(CustomerPhone_Hashkey)
WITH(FILLFACTOR=90, ONLINE=ON);
GO

新增資料

添加了電話號碼Hash列後,新增資料時,需要計算電話號碼的Hash值,儲存在CustomerPhone_Hashkey列中。

USE [TestDb]
GO 
OPEN SYMMETRIC KEY SymKey_TestDb
DECRYPTION BY ASYMMETRIC KEY AsymKey_TestDb WITH PASSWORD = 'Password4@Asy';
GO
-- Performs the update of the record
INSERT INTO dbo.CustomerInfo (CustomerName, CustomerPhone_Hashkey, EncryptedCustomerPhone)
VALUES ('CustomerD', CHECKSUM('13880975623'), EncryptByKey( Key_GUID('SymKey_TestDb'), '13880975623'));  

-- Close the symmetric key
CLOSE SYMMETRIC KEY SymKey_TestDb;
GO

效能對比

以下是對優化前後的查詢語句寫法以及效能對比展示。

優化前

優化前,是對電話號碼密文在WHERE語句中解密出來,然後和使用者側輸入的電話號碼明文進行比較,獲取到相應的資料行。

查詢語句

查詢的語句寫法如下,關鍵點請注意WHERE語句:WHERE CONVERT(CHAR(11), DecryptByKey(EncryptedCustomerPhone)) = ‘13487759293’

USE [TestDb]
GO

-- empty buffer cache
DBCC DROPCLEANBUFFERS
GO

OPEN SYMMETRIC KEY SymKey_TestDb
DECRYPTION BY ASYMMETRIC KEY AsymKey_TestDb WITH PASSWORD = 'Password4@Asy';

SET STATISTICS TIME ON
SET STATISTICS IO ON
GO

SELECT 
	*,
	DescryptedCustomerPhone = CONVERT(CHAR(11), DecryptByKey(EncryptedCustomerPhone))
FROM dbo.CustomerInfo
WHERE CONVERT(CHAR(11), DecryptByKey(EncryptedCustomerPhone)) = '13487759293'

SET STATISTICS TIME OFF
SET STATISTICS IO OFF

CLOSE SYMMETRIC KEY SymKey_TestDb;
GO

執行計劃

從查詢語句的執行計劃來看,走的Clustered Index Scan,幾乎等價於表掃描,SQL Server需要掃面這張表的所有資料,才能找到對應的資料行。

效能指標

從效能指標來看,Logical reads: 13957,CPU: 13010 ms,Duration: 41682 ms,效能消耗非常嚴重,查詢效能低下。

優化後

優化後的查詢,我們使用電話號碼明文Hash值列CustomerPhone_Hashkey進行查詢,去找到對應的資料行。

查詢語句

查詢的關鍵點在WHERE CustomerPhone_Hashkey = CHECKSUM(‘13487759293’)。

USE [TestDb]
GO 
-- empty buffer cache
DBCC DROPCLEANBUFFERS
GO

OPEN SYMMETRIC KEY SymKey_TestDb
DECRYPTION BY ASYMMETRIC KEY AsymKey_TestDb WITH PASSWORD = 'Password4@Asy';
GO
SET STATISTICS TIME ON
SET STATISTICS IO ON
GO

SELECT 
		*,
		DescryptedCustomerPhone = CONVERT(CHAR(11), DecryptByKey(EncryptedCustomerPhone))
FROM dbo.CustomerInfo WITH(NOLOCK)
WHERE CustomerPhone_Hashkey = CHECKSUM('13487759293')
	AND CONVERT(CHAR(11), DecryptByKey(EncryptedCustomerPhone)) = '13487759293'

-- Close the symmetric key
CLOSE SYMMETRIC KEY SymKey_TestDb;
GO

執行計劃

從執行計劃我們也可以看出,SQL Server從Clustered Index Scan操作變成了Index Seek操作了,可以直接定位到具體的資料行。

效能指標

優化後的效能指標來看,效能天壤之別,Logical reads: 6,CPU: 0ms,Duration: 2ms。

注意: 為了防止Hash對撞,在WHERE語句中需要新增如下條件語句: AND CONVERT(CHAR(11), DecryptByKey(EncryptedCustomerPhone)) = ‘13487759293’

效能對比圖

總結下優化前後的LogicalReads、CPU和Duration指標,如下表所示:從此表格,我們可以看到查詢效能有質的飛躍,不論是從IO邏輯讀、CPU消耗還是從執行時間,效能都有非常大的提升。

最後總結

本文分享了使用SQL Server列加密技術後,應用端可能面臨的查詢效能問題以及完整的解決方案。通過此方案,我們可以很好兼顧:最大限度保證使用者資料安全性的前提下,還能最大程度的提升我們的查詢效能。