引言
在一些對安全審計有較高要求的系統中,管理員需要查看每個應用程序的登錄用戶執行了哪些數據庫操作,而通常我們應用程序在訪問數據庫時,都是公用同一個數據庫的認證用戶去獲取數據庫連接的,這樣我們的登錄用戶的標識無法傳遞到數據庫端。而很多客戶是需要在數據庫端能審計登錄用戶的操作。當然,在應用服務器端的程序裡寫日志,記錄下每個登錄用戶執行了哪些操作也能達到審計的需求,但這種方式往往會有性能的開銷。經過實踐,本文介紹的解決途徑是將用戶標識通過數據庫連接傳遞到數據庫端,從而完成在數據庫端的審計,這是一種較輕量級的方式。
解決方案簡述
在 JDBC 4.0 之前,JDBC 規范沒有提供傳遞用戶標識的 API,我們只能通過數據庫廠商提供的 API 去實現。考慮到這一需求的實用性,JDBC 4.0 為我們增加了相應的 API。目前,Oracle 11g Release 1 (11.1) 和 DB2 9.5 的 JDBC driver 都支持 JDBC 4.0 規范,但在此之前的版本中,我們只能借助於廠商提供的 API。本文介紹了使用這些 API 的一些實踐,同時說明了如何在數據庫端查看傳遞過來的用戶標識。
傳遞用戶標識的基本模式為:
數據庫會話(session)
JDBC framework(或 O/R mapping 框架)如 Hibernate 和 iBatis 都提供了 session 的概念,session 是對數據庫連接和事務使用的封裝。一個數據庫會話期間通常使用一個連接,對應一個事務。
對於純 JDBC 來說,獲得一個數據庫連接就相當於開啟了一個會話。
打開一個數據庫會話。
設置用戶標識信息。
執行一些數據庫操作。
清除連接上的用戶標識。
關閉數據庫會話。
其中,清除連接上的標識非常重要,因為我們通常使用的數據庫連接都是邏輯連接,關閉邏輯連接後其對應的物理連接 (TCP/IP 連接 ) 並未關閉,所以清除連接上的標識信息可以確保不影響別的數據庫邏輯連接。
JDBC 4.0 提供的支持
Java 6 支持 JDBC 4.0 規范,在 JDBC 4.0 中提供了在數據庫連接 java.sql.Connection 上傳遞用戶信息的支持。在該接口中提供了兩個方法:
void setClientInfo(String name, String value) throws SQLClientInfoException;
void setClientInfo(Properties properties) throws SQLClientInfoException;
第一個方法允許我們在 Connection 上傳遞三個屬性:
ApplicationName:訪問數據庫的應用程序名稱。
ClientUser: 訪問數據庫的用戶標識,這個用戶和建立數據庫連接的用戶是不同的。建立數據庫連接的用戶是被能數據庫認證和被授權過的用戶。
ClientHostname:訪問數據庫客戶端的主機名。
第二個方法和第一個方法功能類似,只是將參數放到了一個 Properties 對象中。我們通常 setClientInfo(“ClientUser” , userId) 將用戶標識附加在數據庫連接上。使用該方法的常見模式是:
清單 1. 使用 JDBC 4.0 API 傳遞用戶標識
Connection conn = getConnection();
conn.setClientInfo("ClientUser" , currentUserId);
//do something on the connection
conn.setClientInfo("ClientUser" , null);
conn.close();
注意,清除連接上的用戶標識的方式是將標識置為空。下面我們針對兩種的常用數據庫類型介紹標識傳遞的方法。
針對 DB2 的標識傳遞
DB2 提供了 com.ibm.db2.jcc.DB2Connection,該類有下列方法,支持用戶信息傳遞:
public void setDB2ClientUser(String s) throws SQLException;
public void setDB2ClientWorkstation(String s) throws SQLException;
public void setDB2ClientApplicationInformation(String s) throws SQLException;
public void setDB2ClientAccountingInformation(String s) throws SQLException;
在獲得連接後,通過上面的方法在連接上設置用戶信息,在使用完畢後通過置空來清除連接上的用戶信息。示例代碼如下:
清單 2. 使用 DB2Connection 傳遞用戶標識
DB2Connection conn ;
DriverManager.registerDriver(new com.ibm.db2.jcc.DB2Driver());
String connString = "jdbc:db2://hostname:50000/dbname" ;
conn = (DB2Connection)DriverManager.getConnection(connString, "connUser", "connPasswd");
// 上面的連接也可以從 DataSource 上獲取
conn.setDB2ClientUser(“loginUser”) ;
//do something on the connection
conn.setDB2ClientUser(null) ;
conn.close() ;
在開放式平台上,通過下面的 DB2 命令來查看傳遞過來的用戶信息:db2 get snapshot for applications on databasealias,輸出結果示例:
TP Monitor client user ID = DB2UserID
TP Monitor client workstation name = yourApplication
TP Monitor client application name = clientWorkstation
TP Monitor client accounting string = yourAccountingInfo
在主機(z/OS)上,通過 DB2 命令: -DISPLAY THREAD(*) DETAIL 來查看,輸出結果示例:
DSNV401I -DB8G DISPLAY THREAD REPORT FOLLOWS - DSNV402I -DB8G ACTIVE THREADS
-NAME ST A REQ ID AUTHID PLAN ASID TOKEN SERVER RA * 4 V2.27.1302 DB2USER DISTSERV
0042 17 V437-WORKSTATION=clientWorkstation, USERID=DB2UserID,
APPLICATION NAME=yourApplication
針對 Oracle 的標識傳遞
在 Oracle 11g Release 1 之前的版本中,Oracle JDBC driver 提供了接口 oracle.jdbc.driver.OracleConnection,通過 OracleConnection 上的兩個方法 setClientIdentifier() 和 clearClientIdentifier() 可以完成標識傳遞。OracleConnection 只能傳遞一個屬性 clientIdentifier,但通常這已經足夠。
示例如下:
清單 3. 使用 OracleConnection 傳遞用戶標識
OracleDataSource dataSource = new OracleDataSource();
dataSource.setURL("jdbc:oracle:thin:@hostname:1521:orcl");
dataSource.setUser("username");
dataSource.setPassword("passwd");
conn = (OracleConnection) dataSource.getConnection();
conn.setClientIdentifier(clientId) ;
// do something on the connection
conn.clearClientIdentifier(clientId) ;
conn.close() ;
dataSource.close() ;
這個 client_id 傳到 oracle 後,可以通過下面 sql 語句來查看每個 session 上的用戶標識。
select client_identifier from v$session
那如何看到每個 client_id 執行的 sql 呢?需打開 oracle 的審計開關。例如可以打開對查詢語句的審計:
audit select table by session;
然後執行:
select sql_text,CLIENT_ID from dba_audit_trail where username='connectionUser'
order by EXTENDED_TIMESTAMP desc
可以列出每個用戶執行的 sql 語句。
數據源在 WebSphere 應用服務器上的情形
如果是采用 WebSphere 應用服務器上配置的數據源,則無法將數據源上獲得的連接轉化為 OracleConnection 或 DB2Connection,須采用 WAS 提供的 connection wrapper 類 com.ibm.websphere.rsadapter.WSConnection。編程模型如下:
清單 4. 使用 WSConnection 傳遞用戶標識
import com.ibm.websphere.rsadapter.WSConnection;
…
InitialContext ctx = new InitialContext();
DataSource ds = (javax.sql.DataSource) ctx.lookup("jbdc/mydatasource") ;
conn = ds.getConnection();
WSConnection wsconn = (WSConnection) conn ;
Properties props = new Properties();
props.setProperty(WSConnection.CLIENT_ID, clientId);
wsconn.setClientInformation(props);
//do something on the wsconn
wsconn.setClientInformation(null); // 清除連接上的用戶信息
WSConnection 支持下列屬性的傳遞:
WSConnection.CLIENT_ACCOUNTING_INFO
WSConnection.CLIENT_LOCATION
WSConnection.CLIENT_ID
WSConnection.CLIENT_APPLICATION_NAME
WSConnection.CLIENT_OTHER_INFO
WSConnection.OTHER_CLIENT_TYPE
和開源項目的結合
在實際大型項目中,直接通過 JDBC API 訪問數據庫比較少見,大多通過 O/R mapping 框架如 iBatis 或 Hibernate 去操縱數據庫。這些框架往往對數據庫連接進行了封裝,同時客戶的框架又經常進行了二次封裝,這使得在連接上傳遞屬性變得不太容易。下面針對 iBatis 和 Hibernate 提出了自己的一些實踐解法。
下面都是針對 JDBC 4.0 之前的 JDBC driver 的編程實踐。
在 iBatis 中傳遞連接屬性
iBatis 提供了一個接口 com.ibatis.sqlmap.client.SqlMapClient,這個接口包含了數據庫增刪改查的常用方法。很多客戶都是基於該接口的一個 wrapper 類去完成數據庫操作。但 SqlMapClient 默認的方法封裝掉了對連接的使用,即開發者無須獲得連接和釋放連接即可使用。
客戶常用的 SqlMapClient 包裝類的形式:
清單 5. 一個典型的 SqlMapClient 封裝類
public class SqlMapClientUtil {
private SqlMapClient sqlMap ;
public SqlMapClientUtil(SqlMapClient sqlMap) {
this.sqlMap = sqlMap ;
}
public SqlMapClient getSqlMap() {
return sqlMap ;
}
…
}
客戶使用這種包裝類的好處是減輕調用方對 SqlMapClient 的初始化工作,同時也可以對 SqlMapClient 做一些增強。但如果需要在連接上傳遞屬性,需要進行一些改造。改造辦法是寫一個自己的 SqlMapClient 實現,逐一實現 SqlMapClient 裡的方法。
清單 6. 一個自定制的 SqlMapClient 實現
public class MySqlMapClient implements SqlMapClient{
SqlMapClient sqlMap ;
public MySqlMapClient(SqlMapClient sqlMap) {
this.sqlMap = sqlMap ;
}
public Object insert(String id, Object parameterObject) throws SQLException {
Object retObj = null ;
OracleDataSource dataSource = null ;
OracleConnection conn = null ;
try {
conn = (OracleConnection)dataSource.getConnection();
SqlMapSession session = sqlMap.openSession(conn);
conn.setClientIdentifier("") ;
sqlMap.setUserConnection(conn) ;
retObj = session.insert(id, parameterObject) ;
conn.clearClientIdentifier("") ;
conn.commit() ;
} catch (Exception e) {
// TODO: handle exception
} finally {
try {
conn.close() ;
} catch (Exception e2) {
// TODO: handle exception
}
}
return retObj ;
}
//other methods … .
}
於是將上面的 SqlMapClientUtil 重構成:
清單 7. 重構過的 SqlMapClient 封裝類
public class SqlMapClientUtil {
private SqlMapClient sqlMap ;
public SqlMapClient getSqlMapClient() {
return new MySqlMapClient(sqlMap) ;
}
…
}
在 Hibernate 中傳遞連接屬性
典型的使用 Hibernate 操作數據庫的編程模型如下:
清單 8. Hibernate 的典型編程模型
Session sess = factory.openSession();
Transaction tx;
try {
tx = sess.beginTransaction();
//do some work
...
tx.commit();
} catch (Exception e) {
if (tx!=null) tx.rollback();
throw e;
} finally {
sess.close();
}
為了能在會話內傳遞用戶標識,將上述編程模型改造成下面方式即可:
清單 9. 加進傳遞用戶標識後的 Hibernate 編程模型
Session sess = factory.openSession();
Transaction tx;
Connection conn ;
try {
tx = sess.beginTransaction() ;
conn = sess.connection() ;
OracleConnection oraconn = (OracleConnection)conn ;
// 上面連接或轉換成 DB2Connection,視數據庫而定
oraconn.setClientIdentifier("") ;
//do some work
oraconn.clearClientIdentifier("") ;
tx.commit();
} catch (Exception e) {
if (tx!=null) tx.rollback();
throw e;
} finally {
sess.close();
}
結束語
本文源於客戶的真實場景,很多客戶在實際 Java EE 項目中都有“在數據庫端審計前端登錄用戶”的需求。本文針對幾種典型場景給出了如何傳遞用戶標識的編程實踐,並介紹了如何在數據庫端進行審計查看。希望能給相關開發者提供一些借鑒。