當一個資料庫只有你一個人用的時候,權限根本不是問題——反正所有資料都是你的。
但當團隊開始共用同一個資料庫,問題就來了。
HR 不應該看到客戶訂單的資料表,業務部門不應該改到員工個資的資料表,而同一張員工資料表裡,經理能看到薪水欄位,但一般員工不行。
這時候你就需要「權限控管」——決定誰能看什麼、誰能改什麼。
但資料庫的權限控管其實比大多數人想的複雜得多。
它不是一個開關,而是五個層級,從「你能不能連進這個資料庫」一直細到「這個格子的值要不要讓你看到」。
很多人只設了最外面的一兩層就覺得搞定了,結果裡面三層全是漏洞。
這篇文章會從最外層到最內層,把五個安全性層級完整拆解。
看完之後,你會知道每一層各負責什麼、解決什麼問題,以及它們該怎麼搭配使用。
資料庫安全性的五個層級——先看全貌
在開始逐層拆解之前,先用一張全景圖來理解整體架構。
你可以把資料庫的安全性想像成一顆洋蔥,從外到內共有五層:
- Database Level(資料庫層級)——你能不能連進這個資料庫?
- Object / Table Level(物件 / 資料表層級)——你能操作哪些資料表?
- Column-Level Security(CLS)——同一張表裡,你能看到哪些欄位?
- Row-Level Security(RLS)——同一張表裡,你能看到哪些資料列?
- Cell-Level Security——精確到某個「格子」,要不要讓你看到裡面的值?
每一層控管的粒度不同,解決的問題也不同。
接下來會從最外層開始,一層一層往內拆。
Database Level——你連門都進不去
這是最外面的一層,決定的是:這個使用者能不能連進這個資料庫?
你可以把它想像成大樓的門禁卡。
沒有門禁卡,你連大廳都進不去,更別說裡面的辦公室了。
在 SQL Server 裡,控制權限的兩個基本指令是 GRANT(授權)和 DENY(拒絕)。
GRANT 就是「給你這個權限」,DENY 就是「明確禁止你做這件事」。
這一層主要涉及幾種權限:
CONNECT:允許使用者連線到資料庫CREATE TABLE、CREATE VIEW:允許使用者在資料庫裡建立物件ALTER:允許使用者修改資料庫的結構
舉個例子,如果你想讓某個使用者只能連進資料庫、但不能建立任何東西,可以這樣寫:
GRANT CONNECT TO [SomeUser];
DENY CREATE TABLE TO [SomeUser];這一層的重點是:先決定誰可以進來。
但你可能會想:「我平常用資料庫,從來沒打過 GRANT 或 DENY,不是一樣能正常連線嗎?」
這是因為你用的帳號很可能本身就擁有最高權限。
例如 SQL Server 的 sa 帳號,或是你的帳號被加進了 sysadmin、db_owner 這些角色裡。
這些高權限角色預設就擁有所有權限,所以你不需要額外 GRANT 任何東西,自然感覺不到這層的存在。
但在正式的生產環境裡,不可能每個人都用 sa 帳號——這就像把大樓的萬能鑰匙發給每個人一樣危險。
所以才需要透過 GRANT 和 DENY 來精確控制每個使用者的權限。
Object / Table Level——進了門,但哪些房間能進?
通過了 Database Level 的門禁,使用者已經進到了資料庫裡面。
但這不代表他可以碰所有的資料表。
這一層控管的是:使用者對每一張資料表(或 View、Stored Procedure 等物件)有什麼操作權限?
延續大樓的比喻:你有門禁卡進了大樓,但每間辦公室的門鎖不同,你不見得每間都能進。
在 SQL Server 裡,常見的操作是 GRANT 和 DENY:
-- 允許 SomeUser 查詢 Employees 資料表
GRANT SELECT ON dbo.Employees TO [SomeUser];
-- 禁止 SomeUser 刪除 Employees 的資料
DENY DELETE ON dbo.Employees TO [SomeUser];這裡的 dbo 是 SQL Server 預設的 Schema(結構描述),你可以把它想成資料表的「資料夾」。
dbo.Employees 就是「dbo 這個資料夾底下的 Employees 資料表」。
大多數情況下,資料表都放在 dbo 底下,所以你會一直看到這個前綴。
你可以針對每張資料表分別設定 SELECT、INSERT、UPDATE、DELETE 的權限。
如果沒有給 SELECT 權限,使用者連 SELECT * FROM Employees 都會被擋下來,直接報錯。
這一層的重點是:控制使用者能碰哪些資料表,以及能做哪些操作。
很多專案只做到這一層就停了,覺得「我已經限制他只能查某幾張表了」。
但問題是——他雖然只能查這張表,卻可能看到表裡所有的欄位和所有的資料列。
這就是接下來三層要解決的事。
Column-Level Security(CLS)——同一張表,你只能看特定欄位
假設你有一張 Employees 資料表,裡面有這些欄位:
部門經理需要查這張表來看自己部門的人員名單。
但他不應該看到 Salary(薪水)和 SSN(身分證字號)。
這時候就需要 Column-Level Security(CLS),也就是欄位層級的權限控管。
在 SQL Server 裡,CLS 的做法很直覺——直接用 GRANT 指定哪些欄位可以查:
-- 只允許部門經理查 EmployeeID、Name、Department 三個欄位
GRANT SELECT ON dbo.Employees (EmployeeID, Name, Department) TO [DeptManager];設定完之後,如果 DeptManager 執行 SELECT * FROM Employees,資料庫會直接報錯,因為他沒有 Salary 和 SSN 的 SELECT 權限。
他必須明確指定自己有權限的欄位:
SELECT EmployeeID, Name, Department FROM dbo.Employees;你可能會想:「用 View 不是也可以做到一樣的事嗎?」
沒錯,建一個只包含特定欄位的 View 確實也能達到類似效果。
但 CLS 和 View 的差別在於:
- CLS 是在資料表層級直接控管權限,不需要額外建立物件,管理上比較集中。
- View 是建立一個虛擬的「窗口」,透過它來限制可見的欄位。但如果使用者有原始資料表的
SELECT權限,他可以繞過 View 直接查原表。
所以在需要嚴格控管的場景下,CLS 比 View 更可靠。
這一層的重點是:就算使用者能查這張表,也不代表他能看到所有欄位。
CLS 管「欄」,接下來的 RLS 管「列」,它們是最佳搭擋。
Row-Level Security(RLS)——同一張表,你只能看特定資料列
前面的 CLS 管的是「哪些欄位能看」,而 RLS 管的是「哪些資料列能看」。
RLS 解決的問題是:同一張資料表裡,不同的使用者只能看到屬於自己的資料列。
RLS 的核心概念:Security Policy + Predicate Function
RLS 的運作原理其實不複雜,核心只有兩個東西:
- Predicate Function(條件判斷函數):一個你自己寫的函數,資料庫每次查詢時會拿每一列的資料去問它:「這一列要不要讓這個使用者看到?」函數回答「要」就顯示,回答「不要」就過濾掉。這裡的 Predicate 你可以理解成「篩選條件」,就像
WHERE後面接的那個條件一樣。 - Security Policy(安全性原則):光寫好函數還不夠,你需要告訴資料庫「這個函數要套用在哪張資料表上」。Security Policy 就是做這件事的——它把函數和資料表綁在一起,讓篩選自動生效。
簡單來說:Predicate Function 是「規則」,Security Policy 是「把規則貼到資料表上」的動作。
規則寫好但沒貼上去,不會生效;貼上去但沒寫規則,也沒東西可以篩。兩個缺一不可。
實作範例:多部門的員工資料表
假設 Employees 資料表長這樣:
你希望 Engineering 的經理只能看到 Engineering 的資料,Marketing 的經理只能看到 Marketing 的資料。
第一步,建立 Predicate Function:
這段程式碼在做的事情很單純:
建立一個函數,讓資料庫在每次查詢時拿來判斷「這一列要不要給這個使用者看」。
判斷的邏輯是:如果這一列的部門名稱,跟目前登入的使用者名稱相同,就讓他看;不同就過濾掉。
CREATE FUNCTION dbo.fn_SecurityPredicate(@Department NVARCHAR(100))
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN SELECT 1 AS result
WHERE @Department = USER_NAME();其中幾個關鍵字的意思:
@Department NVARCHAR(100):這個函數會接收一個參數,也就是每一列的部門名稱RETURNS TABLE:函數的回傳方式是一張表——有回傳結果代表「可以看」,沒有回傳代表「過濾掉」WITH SCHEMABINDING:把函數跟資料表綁定在一起,防止有人改了資料表結構導致函數壞掉USER_NAME():取得目前登入的使用者名稱WHERE @Department = USER_NAME():核心邏輯——只有當這一列的部門等於目前的使用者時,才回傳結果
這個函數的邏輯很簡單:如果資料列的 Department 欄位值等於目前登入的使用者名稱,就回傳結果(代表可以看);否則不回傳(代表過濾掉)。
第二步,建立 Security Policy,把函數綁到資料表上:
這段程式碼在做的事情是:
把上面建好的篩選函數,貼到 Employees 這張資料表上,並且立即啟用。
啟用之後,每當有人查詢 Employees,資料庫就會自動把每一列的 Department 欄位丟進函數裡判斷,決定要不要讓他看到這一列。
CREATE SECURITY POLICY dbo.EmployeeSecurityPolicy
ADD FILTER PREDICATE dbo.fn_SecurityPredicate(Department)
ON dbo.Employees
WITH (STATE = ON);其中幾個關鍵字的意思:
CREATE SECURITY POLICY:建立一個安全性原則,這裡取名叫EmployeeSecurityPolicyADD FILTER PREDICATE:指定要用哪個函數來做篩選,這裡用的就是剛才建好的fn_SecurityPredicate(Department):告訴函數「拿Department這個欄位的值去做判斷」ON dbo.Employees:這個篩選規則要套用在Employees這張資料表上WITH (STATE = ON):立即啟用;如果設成OFF,規則會建立但不會生效,方便你先測試再上線
設定完之後,當 Engineering 這個使用者執行 SELECT * FROM Employees 時,他只會看到:
Marketing 的資料完全不會出現在結果裡,就像它們不存在一樣。
RLS 的限制與常見誤區
RLS 很強大,但它不是萬能的。
以下是幾個常見的誤區:
- RLS 不管欄位。 就像開頭說的,RLS 只篩選列,不限制欄位。如果你需要隱藏某些欄位,要搭配 CLS。
- RLS 可能影響效能。 因為每次查詢都要經過 Predicate Function 的計算,如果資料量很大、函數邏輯很複雜,查詢速度可能會變慢。建議在篩選欄位上建立索引。
sysadmin和db_owner預設不受 RLS 限制。 如果你用這些高權限帳號測試 RLS,會發現怎麼設都沒效果——因為它們預設會繞過 Security Policy。- RLS 不會防止 Side-Channel 攻擊。 例如,使用者雖然看不到某一列的資料,但可能透過精心設計的查詢(例如利用錯誤訊息或執行時間差異)來推測出隱藏的資料。
Cell-Level Security——最細緻的控管,精確到每個格子
前面四層分別管「能不能連進資料庫」「能不能碰某張表」「能看哪些欄位」「能看哪些列」。
但有時候,你需要更細的控管——精確到某個「儲存格」。
舉個例子:同一張 Employees 資料表裡,你希望 HR 可以看到所有人的身分證字號,但其他人看到的是遮蔽後的版本(例如 A123******)。
注意,這裡不是把整個身分證字號(SSN)欄位隱藏(那是 CLS 的事),而是同一個欄位,不同人看到不同的值。
要做到這件事,在 SQL Server 裡有不同的做法,安全強度由低到高分成三個層級。
做法一:Dynamic Data Masking(DDM)——最輕量,但防君子不防小人
DDM(動態資料遮罩)是最容易設定的做法。
你可以想像成在紙上貼了一張便利貼把內容蓋住——資料本身還是完整地存在資料庫裡,只是查詢的時候「顯示」被蓋住了。
設定方式很簡單:
ALTER TABLE dbo.Employees
ALTER COLUMN SSN ADD MASKED WITH (FUNCTION = 'partial(1, "***-***-", 4)');設定之後,沒有 UNMASK 權限的使用者查詢時會看到:
而有 UNMASK 權限的使用者則看到完整的值,就像把便利貼撕掉一樣。
但 DDM 有一個重要的限制:它擋不住刻意的攻擊。
例如,使用者雖然看到的是 A***-***-3456,但他可以用 WHERE SSN = 'A123-456-3456' 去試,如果查得到結果,就知道這個值是對的。
所以 DDM 適合的場景是:防止一般使用者「不小心看到」敏感資料,例如客服人員打開畫面時不會直接看到客戶的完整身分證字號。
做法二:Always Encrypted——連資料庫自己都看不到原始值
如果你需要更強的保護,Always Encrypted 是把資料鎖進保險箱的做法。
資料在寫入資料庫之前就先加密,資料庫本身也看不到原始值。
只有持有加密金鑰的應用程式才能解密。
這代表就算有人直接存取資料庫檔案,或者就算是資料庫管理員(DBA),拿到的也是加密後的亂碼。
適合用在身分證字號、信用卡號這類高度敏感的資料。
做法三:Transparent Data Encryption(TDE)——防硬碟被偷
TDE 的做法不太一樣——它不是針對單一欄位或格子加密,而是把整個資料庫檔案加密。
這樣即使有人把伺服器的硬碟偷走,也無法直接讀取裡面的資料。
但跟 Always Encrypted 不同的是,只要你能正常登入資料庫,查詢時看到的還是明文。
所以 TDE 防的是「實體層面的資料外洩」,而不是「登入後的越權存取」。
三種做法的比較
這一層的重點是:當「欄位層級」的控管還不夠細,你需要精確控制每個格子的可見性時,就是 Cell-Level Security 上場的時候。 至於要用哪種做法,取決於你要防的是「不小心看到」還是「刻意竊取」。
五層安全性怎麼搭配?實務上的組合策略
了解了五層之後,最重要的問題是:在實務上,這五層該怎麼組合使用?
先用一張表整理每層的控管對象和適用情境:
實務情境:一個企業 ERP 系統
假設你在維護一個企業 ERP 系統,裡面有員工資料、訂單資料、財務資料等多張表。
以下是每一層各自負責的事情:
- Database Level:外包廠商的帳號只能連進測試資料庫,不能碰正式環境。
- Object / Table Level:業務部門只能查
Orders和Customers表,不能碰Employees和Financials表。 - Column-Level Security:HR 可以看到
Employees表的所有欄位,但部門經理看不到Salary和SSN。 - Row-Level Security:北區經理只能看到北區的訂單資料,南區經理只能看到南區的。
- Cell-Level Security:所有人查
Customers表時,信用卡號欄位都會被遮罩,只有財務部門有UNMASK權限。
這五層各管各的,彼此不衝突,也不能互相取代。
缺少任何一層,都會在安全性上留下缺口。
常見問題整理
RLS 會影響效能嗎?
會,但通常影響不大。
RLS 的 Predicate Function 會在每次查詢時被執行,相當於在每個查詢後面自動加了一個 WHERE 條件。
如果你的篩選欄位上有索引,效能影響通常可以忽略。
但如果 Predicate Function 的邏輯很複雜(例如要去查另一張表),或是資料量非常大,就需要注意效能調校。
建議做法:在 Predicate Function 使用的欄位上建立索引,並用執行計畫(Execution Plan)確認查詢效能。
CLS 和 View 有什麼不同?
兩者都能限制使用者看到的欄位,但控管的嚴格程度不同。
View 是建立一個虛擬的窗口,只暴露你想讓使用者看到的欄位。
但如果使用者同時擁有原始資料表的 SELECT 權限,他可以直接查原表,繞過 View 的限制。
CLS 則是在資料表層級直接控管欄位的存取權限,就算使用者知道欄位名稱,沒有權限就是查不到。
在需要嚴格控管的場景下,CLS 比 View 更可靠。
只用 RLS 夠不夠?
不夠。
RLS 只管「哪些列可以看到」,但不管「哪些欄位可以看到」「哪些表可以碰」「能不能連進資料庫」。
如果你只設了 RLS,使用者雖然只能看到自己部門的資料列,但可能看到所有的欄位(包括薪水、個資等敏感資訊)。
安全性是一個整體。
五層防線各有各的職責,少了任何一層都會有缺口。
根據你的需求,至少把前四層(Database、Table、Column、Row)都設定好,Cell-Level 則視資料的敏感程度決定是否啟用。