VPD
最近一直做MC的項目,根據context不同,利用VPD對查詢數據進行過濾。問題:之前我們做權限控制常用的做法是建立視圖,然後多在查詢語句中加where語句來控制。不過這樣當程序改變時,DB改動也比較大。另外,這個只是在應用程序層面上進行過濾。拋開不安全不說,讓應用程序自己控制權限,總有點職責不明。
VPD 即虛擬專用數據庫(Virtual Private Database),簡單來說通過在數據庫裏進行配置,從而讓不同的用戶只能查看某 個表裏的部分數據。這裏說的查看不同的部分數據分兩個級別:行級別和列級別。我們知道Oracle 是關係數據庫,一行一條數據。行級別:在該級別下,可以控制某些用戶只能查看到某些數據行。
列級別:在該級別下,可以控制某些用戶不能檢索某個表的某個列的值。如果沒權限則顯示null。
行級別VPD
當一個表被設置了行級別的VPD時,當SQL執行時,Oracle會自動改寫SQL 語句,在後面添加where條件。例如,對MY_ORDER 設置了行VPD,且限制 customer ='5678'時:
SELECT * FROM orders_tab;
執行時,Oracle會自動轉換成以下:
SELECT * FROM orders_tab cust_no ='5678';
注意,因爲Oracle是改寫where子句,因此VPD 的影響範圍只能作用於where,而不能作用select等子句。列級別VPD
對於一些table 列來說,有可能涉及一些敏感信息。這是爲這些列加上VPD 後,就可以屏幕這些信息,只有有權限的用戶才能訪問到這些列信息。當用戶不具有權限時,Oracle在這列上用null替換。
Policy
Policy 即策略。Oracle提供Policy 來定義和管理上邊說的這些權限控制。比如,上邊提到的 customer ='KAKA'。這樣對於Table和 VPD的管理就分開了。利用Policy 可以很容易的維護我們的權限管理體系。定義一個Policy,首先要定義一個function。這個函數的目的就是根據不同條件返回一個字符串,最後這個字符串會被Oracle執行SQL時拼接到SQL的where 子句中。
這是一個創建 Policy 函數的例子,簡單定義一個返回 cust_no = 5678' 的function。
CREATE OR REPLACE FUNCTION auth_orders(
schema_var IN VARCHAR2,
table_var IN VARCHAR2
)
RETURN VARCHAR2
IS
return_val VARCHAR2 (50);
BEGIN
return_val := 'cust_no = 5678';
RETURN return_val;
END auth_orders;
/
這裏在編寫function時一定要小心,我在用的時候經常報 "ORA-00936: missing expression" 錯,坑了我好多次。
然後我們就可以創建我們需要的 Policy了。Oracle 提供了DBMS_RLS 內置函數包幫助我們對Policy 進行維護。這裏我簡單地找了一些:
ADD_POLICY 將訪問控制策略添加到對象
DROP_POLICY 刪除對象中的策略
REFRESH_POLICY 重新解析與策略關聯的、緩存的所有語句
ENABLE_POLICY 啓用或禁用策略
CREATE_POLICY_GROUP 創建策略組
ADD_GROUPED_POLICY 將策略添加到策略組
ADD_POLICY_CONTEXT 添加當前應用程序的上下文
DELETE_POLICY_GROUP 刪除策略組
DROP_GROUPED_POLICY 從策略組中刪除一個策略
DROP_POLICY_CONTEXT 刪除活動應用程序的上下文
ENABLE_GROUPED_POLICY 啓用或禁用組策略
DISABLE_GROUPED_POLICY 禁用組策略
REFRESH_GROUPED_POLICY 重新解析與策略組關聯的、緩存的所有語句
這裏參考官網給出的例子:BEGIN
DBMS_RLS.ADD_POLICY (
object_schema => 'scott', --數據表(或視圖)所在的Schema名稱
object_name => 'orders_tab', --數據表(或視圖)的名稱
policy_name => 'orders_policy', --POLICY的名稱,主要用於將來對Policy的管理
function_schema => 'sysadmin_vpd', --返回Where子句的函數所在Schema名稱
policy_function => 'auth_orders', --返回Where子句的函數名稱
statement_types => 'select', -- DML類型,如 'Select,Insert,Update,Delete'
Enable =>True --是否啓用,值爲'True'或'False'
);
END;
/
此時,我們再執行以下語句只能得到兩條數據了。SELECT t.* from orders_tab t;
注意,創建Policy時,要用sys用戶將DBMS_RLS包的運行權限 grant 給使用VPD用戶。創建基於列的Policy 也很類似:
CREATE OR REPLACE FUNCTION hide_sal_comm (
v_schema IN VARCHAR2,
v_objname IN VARCHAR2)
RETURN VARCHAR2 AS
con VARCHAR2 (200);
BEGIN
con := 'deptno=30';
RETURN (con);
END hide_sal_comm;
/
同樣創建Policy,不過要指定作用的列。
BEGIN
DBMS_RLS.ADD_POLICY (
object_schema => 'scott',
object_name => 'emp',
policy_name => 'hide_sal_policy',
policy_function => 'hide_sal_comm',
sec_relevant_cols => 'sal,comm'); --作用列
END;
/
此時,當user沒權限時,sal,comm 兩列顯示null。可以通過下面SQL查看所有的Policy:
SELECT * FROM all_policies ;
context
我們通常叫context 爲上下文。在很多地方都看到過。在Oracle中,用戶登錄後相當創建了一個session,這個session的一些信息就是context。默認情況下Oracle提供了一個context,即userenv(user environment),有點類似 key-value的 map。這個context是Oracle在用戶登錄時,數據庫登錄觸發器執行,通過調用dbms_session.set_context爲用戶設置的應用程序上下文。
個人覺得context的最主要的作用是提供了對信息的一種管理上的方便。
例如:
SELECT SYS_CONTEXT('USERENV','LANGUAGE'), SYS_CONTEXT('USERENV','HOST') FROM dual ;
Oracle也允許我們自定義 context,語法如下:
DBMS_SESSION.SET_CONTEXT
( namespace VARCHAR2, attribute VARCHAR2, value VARCHAR2, username VARCHAR2, client_id VARCHAR2 );
這裏,前三個參數是必須的,後兩個非必須,如果不寫的話就默認爲null。後面兩個參數是針對全局的context。
session
我們知道在很多地方,session 都是通信雙方從開始通信到通信結束期間的一個上下文(context)。因此,就有種可能:在通信時,客戶端通過設置context 的信息值,然後Oracle在執行過程中使用該值。這個時候相信你已經將前後聯繫起來了。現在改寫我們的 Policy function,看官網的例子:
CREATE OR REPLACE FUNCTION get_user_orders(
schema_p IN VARCHAR2,
table_p IN VARCHAR2)
RETURN VARCHAR2
AS
orders_pred VARCHAR2 (400);
BEGIN
orders_pred := 'cust_no = SYS_CONTEXT(''orders_ctx'', ''cust_no'')'; -- 使用 context 裏的信息
RETURN orders_pred;
END;
這時的查詢就相當於:
SELECT * FROM scott.orders_tab where cust_no = SYS_CONTEXT('orders_ctx'', 'cust_no');
Package
除了context裏面,我們也可以在創建 PACKAGE,然後在package 定義變量。每次查詢前調用function或存儲過程,更新變量的值。如果我們的Policy function 使用到這個變量,一樣能達到效果。這裏解釋下包。
在一個系統中,可能有很多模塊,而每個模塊又有自己的存儲過程、函數等。默認情況下,這些是放在一起的。利用面向對象設計的思路,對其進行分類就形成包。因此,可以將包看成是一組相關過程、函數、變量、常量、類型和遊標等元素的組合。
包由兩個分開的部分組成:包規範和包體。
包規範:是應用程序的接口,聲明包內數據類型、變量、常量、遊標、子程序和異常錯誤處理等元素,這些元素爲包的公有元素。這點類似Java中的接口。
包主體:是包定義部分的具體實現,其中定義了包規範部分所聲明的遊標和子程序,另外在包主體中還可以聲明包的私有元素。如果在包主體中的遊標或子程序並沒有在包頭中定義,那麼這個遊標或子程序是私有的。
也就是說,包主體實現了包規範的接口,然後還添加了私有的元素。
Java應用
JDBC
如果程序中使用JDBC,可以利用JDBC的API幫我們更新context。
我們知道, JDBC在和Oracle交互的過程,主要經過以下幾個步驟:
1. 獲取連接 getConnection2. 構造Statement對象
createStatement() //最容用的方式,缺點容易被注入攻擊
prepareStatement(String sql) //防止SQL注入
prepareCall(String sql) //存儲過程調用
3. 執行Execute4. 獲取ResultSet,提交事務(Commit)
因此,同樣可以在查詢前,調用存儲過程,更新變量。
Spring
在Spring中,我們通常將dataSource 定義在配置裏。比如:
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="oracle.jdbc.OracleDriver" />
<property name="url" value="jdbc:oracle:thin:@xxx.xxx.com" />
<property name="username" value="user***" />
<property name="password" value="pass***" />
<property name="connectionInitSqls">
<util:list>
<value>call PKG_RLS.SET_CONTEXT ('context', 'your_context', 'SET_VALUE')</value>
</util:list>
</property>
</bean>
這裏利用了,connection的初始化sql的參數。只是這裏有一點,這個屬性是1.3 添加的,如果你的版本比這個低,需要升級。具體可以參考API。
OracleConnection
Oracle提供 VPD,也對OracleConnection提供了更改context的功能。這裏簡單給下例子:
public ResultSet readAPJob() throws SQLException {
Connection connection = getDBConnection();
processContext(connection);
PreparedStatement statement = connection.prepareStatement(readAPJobSql);
return statement.executeQuery();
}
private Connection getDBConnection() throws SQLException {
return DriverManager.getConnection(URL, "username", "password");
}
private void processContext(Connection connection) throws SQLException {
OracleConnection oraCon = connection.unwrap(OracleConnection.class);
oraCon.setApplicationContext("CLIENTCONTEXT", "ORACLE_USER_ID", "");
}
這裏主要利用了OracleConnection 的方法 setApplicationContext()。
eclipselink
如果你用的是eclipselink,通俗的做法利用SessionEventAdapter。把它跟數據庫配置一樣放在persistence.xml 中。
<property name="eclipselink.session-event-listener" value="YourVPDSessionEventAdapter" />
然後讓YourVPDSessionEventAdapter 繼承 SessionEventAdapter ,然後我們就可以根據event 獲取session,然後操作session即可。session event 還有其他功能,有興趣的可以參考文檔。