全部博文(13)
分类: Oracle
2011-05-23 16:55:06
如何在 DB2 和 Oracle 的数据库连接上传递用户标识的实践
简介: Java EE 应用的安全性越来越受到重视,如何传递用户身份标识并进行操作审计已成为一个课题。本文介绍了如何在数据库连接上传递应用程序用户的标识,针对 DB2 和 Oracle 并结合 iBatis 和 Hibernate 的使用总结了一些开发实践。
在一些对安全审计有较高要求的系统中,管理员需要查看每个应用程序的登录用户执行了哪些数据库操作,而通常我们应用程序在访问数据库时,都是公用同一个数据库的认证用户去获取数据库连接的,这样我们的登录用户的标识无法传递到数据库端。而很多客户是需要在数据库端能审计登录用户的操作。当然,在应用服务器端的程序里写日志,记录下每个登录用户执行了哪些操作也能达到审计的需求,但这种方式往往会有性能的开销。经过实践,本文介绍的解决途径是将用户标识通过数据库连接传递到数据库端,从而完成在数据库端的审计,这是一种较轻量级的方式。
在 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 的一些实践,同时说明了如何在数据库端查看传递过来的用户标识。
传递用户标识的基本模式为:
其中,清除连接上的标识非常重要,因为我们通常使用的数据库连接都是逻辑连接,关闭逻辑连接后其对应的物理连接 (TCP/IP 连接 ) 并未关闭,所以清除连接上的标识信息可以确保不影响别的数据库逻辑连接。
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 上传递三个属性:
第二个方法和第一个方法功能类似,只是将参数放到了一个 Properties 对象中。我们通常 setClientInfo(“ClientUser” , userId) 将用户标识附加在数据库连接上。使用该方法的常见模式是:
Connection conn = getConnection(); conn.setClientInfo("ClientUser" , currentUserId); //do something on the connection conn.setClientInfo("ClientUser" , null); conn.close(); |
注意,清除连接上的用户标识的方式是将标识置为空。下面我们针对两种的常用数据库类型介绍标识传递的方法。
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; |
在获得连接后,通过上面的方法在连接上设置用户信息,在使用完毕后通过置空来清除连接上的用户信息。示例代码如下:
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 11g Release 1 之前的版本中,Oracle JDBC driver 提供了接口 oracle.jdbc.driver.OracleConnection,通过 OracleConnection 上的两个方法 setClientIdentifier() 和 clearClientIdentifier() 可以完成标识传递。OracleConnection 只能传递一个属性 clientIdentifier,但通常这已经足够。
示例如下:
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 应用服务器上配置的数据源,则无法将数据源上获得的连接转化为 OracleConnection 或 DB2Connection,须采用 WAS 提供的 connection wrapper 类 com.ibm.websphere.rsadapter.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 支持下列属性的传递:
在实际大型项目中,直接通过 JDBC API 访问数据库比较少见,大多通过 O/R mapping 框架如 iBatis 或 Hibernate 去操纵数据库。这些框架往往对数据库连接进行了封装,同时客户的框架又经常进行了二次封装,这使得在连接上传递属性变得不太容易。下面针对 iBatis 和 Hibernate 提出了自己的一些实践解法。
下面都是针对 JDBC 4.0 之前的 JDBC driver 的编程实践。
iBatis 提供了一个接口 com.ibatis.sqlmap.client.SqlMapClient,这个接口包含了数据库增删改查的常用方法。很多客户都是基于该接口的一个 wrapper 类去完成数据库操作。但 SqlMapClient 默认的方法封装掉了对连接的使用,即开发者无须获得连接和释放连接即可使用。
客户常用的 SqlMapClient 包装类的形式:
public class SqlMapClientUtil { private SqlMapClient sqlMap ; public SqlMapClientUtil(SqlMapClient sqlMap) { this.sqlMap = sqlMap ; } public SqlMapClient getSqlMap() { return sqlMap ; } … } |
客户使用这种包装类的好处是减轻调用方对 SqlMapClient 的初始化工作,同时也可以对 SqlMapClient 做一些增强。但如果需要在连接上传递属性,需要进行一些改造。改造办法是写一个自己的 SqlMapClient 实现,逐一实现 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 重构成:
public class SqlMapClientUtil { private SqlMapClient sqlMap ; public SqlMapClient getSqlMapClient() { return new MySqlMapClient(sqlMap) ; } … } |
典型的使用 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(); } |
为了能在会话内传递用户标识,将上述编程模型改造成下面方式即可:
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 项目中都有“在数据库端审计前端登录用户”的需求。本文针对几种典型场景给出了如何传递用户标识的编程实践,并介绍了如何在数据库端进行审计查看。希望能给相关开发者提供一些借鉴。
学习
讨论
库俊国,高级软件工程师。目前在 IBM 中国软件开发中心从事 WebSphere 应用服务器系统测试相关工作,有多年的 J2EE 项目开发经验。您可以通过邮件和他联系。
----作者的这篇文章让我们思考如何在应用端与数据库端建立联系,并将应用端信息传入数据库端,是一篇很好的技术文章,特转载之,谢谢。