VPD學習筆記

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. 獲取連接 getConnection

2. 構造Statement對象

createStatement()   //最容用的方式,缺點容易被注入攻擊  
prepareStatement(String sql) //防止SQL注入  
prepareCall(String sql)  //存儲過程調用
3. 執行Execute
4. 獲取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 還有其他功能,有興趣的可以參考文檔





發佈了96 篇原創文章 · 獲贊 29 · 訪問量 12萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章