扫一扫
分享文章到微信
扫一扫
关注官方公众号
至顶头条
引言
将 UsernameToken 与 SSL 结合使用对 Web 服务客户端和服务提供者进行身份验证极为常见。指定在服务请求中使用的用户名和密码的缺省方法位于作为 UsernameToken 安全配置一部分的客户端部署描述符中。作为 IBM® WebSphere® Application Server 运行时一部分提供的 NonPromptCallbackHandler 将使用客户端部署描述符中的用户名和密码在 Web 服务请求标头中创建 UsernameToken。
不过,与在部署描述中指定的用户名和密码相比,在许多情况中需要更动态的内容。本文将描述自定义 JAAS 回调处理程序,该程序提供三个选项作为用户名和密码的来源:
这里描述的自定义 JAAS 回调处理程序称为 ClientIdCallbackHandler;该程序较为简短,本文将阐释其工作原理。本文包括演示回调处理程序使用情况的示例应用程序的完整源代码。您将看到如何为 IBM WebSphere Application Server V6.0 运行时配置回调处理程序。这里还使用 IBM Rational® Application Developer 6.0 演示 Web 服务部署描述符的配置。
本文构建在早期 developerWorks 文章(描述如何对 Web服务安全使用 UsernameToken 和 SSL)的基础之上,并假设您大致熟悉 Java 编程和 Web 服务开发。
|
自定义 JAAS 回调处理程序概述
对每个请求调用 ClientIdCallbackHandler 时,该处理程序使您能够从各种来源获取用户名和密码。它们是(按优先级顺序):
线程本地存储中提供的用户名和密码。
这是最高优先级的用户名和密码。线程本地存储中的值是使用静态方法 setThreadIdentity(String, char[]) 设置的。每次使用后,用户名和密码的线程本地存储值都被重新设置为空。如果要对每个请求使用不同的用户名和密码,则需要在每个给定的 Web 服务操作调用之前放置 setThreadIdentity() 调用。
从属性文件读取的和在实例化 ClientIdCallbackHandler时设置的用户名和密码。
这是许多应用程序的用例。实例化 ClientIdCallbackHandler 时,从属性文件读取的用户名和密码分别存储在名为 _appUsername 和 _appPassword 的实例变量中。这些实例变量不能重新设置,如果它们不为空,则用于为每个请求提供用户名和密码。(如果此用户名和密码是为给定的调用设置的,则线程本地用户名和密码将覆盖它们。)
模块部署描述符中作为 UsernameToken 身份验证配置一部分提供的用户名和密码。
这是最低优先级用户名和密码。当实例化 JAAS 回调处理程序时,WebSphere Application Server 运行时传入从部署描述符获取的用户名和密码。通过运行时从部署描述符获得的用户名和密码分别存储在 _wasUsername 和 _wasPassword 中。如果用户名和密码的其他两个来源为空,并且在每次使用后不能重新设置,则只能使用这些实例变量,所以每个请求都可能会使用它们。(使用部署描述符用户名和密码的最可能场景是在初始化开发测试过程中。)
下面的概要步骤将描述 ClientIdCallbackHandler 的操作:
WebSphere Application Server 运行时调用传入用户名和密码以及一些属性的 ClientIdCallbackHandler 构造器。构造器设置输入参数的 _wasUsername 和 _wasPassword 以及 _properties。该构造器还调用一个方法来加载一些配置属性。如果找到配置属性,则调用 initUserIdAndPassword 方法,以便从这些配置属性获取用户名和密码(步骤 2)。
InitUserIdAndPassword 方法读取 Username 和 Password 属性,并为 ClientIdCallbackHandler 设置 _appUsername 和 _appProperty 实例变量。注意,在配置属性文件中使用了 Base64 编码形式的用户名和密码,以避免此敏感信息意外暴露。现在,当客户端的服务请求触发 JAAS 回调处理程序序列时,WebSphere Application Server 将调用处理程序实例(步骤 4)。
如果客户端需要为给定的服务请求设置用户名和密码,则在进行实际服务请求调用之前立即将 setThreadIdentity(String, char[]) 的调用放入代码中,以便为用户名和密码设置线程本地存储。
每次发生服务请求时将会调用 Handle(Callback[]) 方法。处理方法希望以下三种类型的回调:NameCallback、PasswordCallback 和 PropertyCallback。
对于 NameCallback,处理方法使用 NameCallback setName 方法设置用户名:
对于 PasswordCallback,处理方法使用 PasswordCallback setPassword 方法设置密码。类似地,仍将检查线程本地密码,如果不为空,则使用它(并且在使用后重新设置为空)。否则,使用 _appPassword(如果不为空)。最后使用 _wasPassword(如果其他两个值均为空)。
对于 PropertyCallback,当 WebSphere Application Server 运行时实例化 ClientIdCallbackHander 时,处理方法使用 PropertyCallback setProperties 方法返回传递到它的属性。
如果处理方法获得一些其他类型的回调,则引发 UnsupportedCallbackException。
ClientIdCallbackHandler 关闭
现在详细介绍 ClientIdCallbackHandler 源代码的特定段。为便于说明,这里使用简短注释替换了一些跟踪输出语句。
现在将介绍这些代码段:
1. 类声明和实例变量
这里需要注意的关键内容是用于存放用户名和密码的可能来源的线程本地存储声明和实例变量。
清单 1. 类声明和实例变量
public class ClientIdCallbackHandler implements CallbackHandler { private static final String propertiesFileName = "login.properties"; // thread specific username initalized to null; private static ThreadLocal username = new ThreadLocal() { protected synchronized Object initialValue() { return null; } }; // thread specific password, also initialized to null; private static ThreadLocal password = new ThreadLocal() { protected synchronized Object initialValue() { return null; } }; private Map _properties; private String _wasUsername; // value from WAS container private char[] _wasPassword; // value from WAS container private String _appUsername = null; // value from properties file private char[] _appPassword = null; // value from properties file |
2. ClientIdCallbackHandler 构造器
WebSphere Application Server 在第一个 Web 服务请求时调用构造器。注意,这里向构造器提供了静态配置信息:用户名、密码和属性。
清单 2. ClientIdCallbackHandler 构造器
public ClientIdCallbackHandler(String username, char[] password, java.util.Map properties) throws IOException { super(); Properties configProperties = null; this._wasUsername = username; this._wasPassword = password; this._properties = properties; configProperties = loadConfigProperties(propertiesFileName); if (configProperties != null) initUserIdAndPassword(configProperties); } |
3. loadConfigProperties 方法
loadConfigProperties 方法将查看名为 configuration.environment 的 JVM 系统属性,以便为 login.properties 文件名获取前缀;例如,dev、test、qa 和 prod 都是示例应用程序中使用的前缀。(此特定示例中使用的从企业存档读取属性文件的做法尽管常见,但不建议使用。更灵活的方法是使用在文件系统中任何地方都可以放置的访问属性文件的 URL 资源。)
清单 3. loadConfigProperties 方法
private Properties loadConfigProperties(String propertiesFileRootName) { Properties configProperties = null; // Check the JVM property for the environment String myEnv = System.getProperty("deployment.environment"); if (myEnv == null || EMPTY_STRING.equals(myEnv)) { // warning emitted to SystemOut.log indicating defaulting to dev myEnv = "dev"; } String propertiesFile = "/" + myEnv + "_" + propertiesFileName; InputStream in = this.getClass().getResourceAsStream(propertiesFile); if (in == null) { // warning emitted to SystemOut.log regarding no properties file // in classpath } else { try { configProperties = new Properties(); configProperties.load(in); } catch (Exception e) { // exception emitted to SystemOut.log } } return configProperties; } |
4. InitUserIdAndPassword 方法
InitUserIdAndPassword 方法可以读取用户名和密码属性、对值解码并设置类的 _appUsername 和 _appPassword 实例变量。考虑到处理程序方法的工作方式,_appUsername 和 _appPassword 将担任客户端应用程序的缺省用户名和密码的角色。
清单 4. initUserIdAndPassword 方法
private void initUserIdAndPassword(Properties configProperties) { String encodedUsername = null; String encodedPassword = null; if (configProperties != null) { encodedUsername = configProperties.getProperty("Username"); if (encodedUsername == null) { // warning emitted to SystemOut.log } else { this._appUsername = decode(encodedUsername); } encodedPassword = configProperties.getProperty("Password"); if (encodedPassword == null) { // warning emitted to SystemOut.log } else { this._appPassword = decode(encodedPassword).toCharArray(); } } } // decode helper method uses Apache commons Base64 private String decode(String str) { return new String(Base64.decodeBase64(str.getBytes())); } |
5. setThreadIdentity 方法
在调用服务请求(对于该给定请求,需要特定的用户名和密码)之前,在主客户端代码的主体中使用 setThreadIdentity 方法。标识的来源将特定于客户端应用程序;例如,应用程序可以从用户或一些其他来源获取用户名和密码。在本文中,假设在客户端和服务提供者之间没有设置信任关系,所以要让提供者接受请求,在 UsernameToken 中需要用户名和密码。
清单 5. setThreadIdentity 方法
public static void setThreadIdentity(String userId, char[] pw) { username.set(userId); password.set(pw); } |
下面的代码示例(来自示例客户端应用程序)需要用户以允许删除客户的 HTML 形式提供用户名和密码。在调用 deleteCustomer 请求之前,在线程上设置此用户名和密码。(此处显示的代码示例中已删除参数验证代码。)
清单 6. 在 HTML 中设置用户名和密码的代码示例
String customerId = request.getParameter("customerId"); String username = request.getParameter("username"); String password = request.getParameter("password"); ClientIdCallbackHandler. setThreadIdentity(username,password.toCharArray()); crmService.deleteCustomer(customerId); |
6. 处理方法
ClientIdCallbackHandler 最有意义的方法是处理方法,它是由 WebSphere Application Server 运行时对每个服务调用进行调用的,目的是为了设置用户名和密码,容器将该用户名和密码用于为传出服务请求创建的 UsernameToken。
清单 7. 处理方法
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { for (int i = 0; i < callbacks.length; i++) { if (callbacks[i] instanceof NameCallback) { // provide the username, using default from configuration // or thread specific value NameCallback nc = (NameCallback) callbacks[i]; String user = (String) username.get(); if (user == null) if (this._appUsername != null) user = this._appUsername; // from properties file else user = this._wasUsername; // from WAS container nc.setName(user); // after each use of thread local username, // revert to using defaults username.set(null); } else if (callbacks[i] instanceof PasswordCallback) { // provide the password, using default from configuration // or thread specific value PasswordCallback pc = (PasswordCallback) callbacks[i]; char[] pw = (char[]) password.get(); if (pw == null) if (this._appPassword != null) pw = this._appPassword; // from properties file else pw = this._wasPassword; // from WAS container pc.setPassword(pw); // also, after each use revert to using default password.set(null); } else if (callbacks[i] instanceof PropertyCallback) { PropertyCallback pc = (PropertyCallback) callbacks[i]; pc.setProperties(_properties); } else { throw new UnsupportedCallbackException(callbacks[i], callbacks[i] .getClass() + " - Unrecognized Callback"); } } // for } |
|
配置 ClientIdCallbackHandler
ClientIdCallbackHandler 及其支持的类位于自已的 Java 项目中。这样,任何 Web 服务客户端应用程序都可以方便地包括它。这里尽可能通俗地描述该步骤。我们将使用 Rational Application Developer V6.0 的屏幕映像来举例说明该描述。这里描述的所有开发工作都是在 Project Explorer 视图中的 J2EE™ 透视图中执行的。您将在屏幕映像中看到与本文下载文件中提供的演示应用程序相关的特定构件名称。
演示应用程序是公开为 Web 服务的伪客户关系管理(Customer Relationship Manager,CRM)应用程序。调用 CRM 服务提供者的客户端企业应用程序被命名为 CRMWebServiceClientApp。客户端应用程序拥有名为 CRMWebServiceClient 的 Web 模块,它存放 WSDL2Java 从 CRM 服务的 WSDL 定义生成的存根和帮助类。CRMWebServiceClient 还拥有一个 Servlet 和一些 JSP,它们实现使用 CRM 服务的非常简单的 Web 应用程序。CRM 服务支持客户及其开单和发货地址数据库上的 CRUD 操作。
在下面的配置步骤中,先描述步骤的整体目标,接着详细说明完成该目标需要执行的操作。
在父 Web 服务客户端应用程序中作为项目实用工具 JAR 包括 ClientIdCallbackHandler Java 项目:
将 ClientIdCallbackHandler 添加到实现 Web 服务客户端的模块的 Java JAR 依赖项(例如,Web 模块 CRMWebServiceClient):
修改 Web 服务客户端部署描述符 Request Generator Configuration,以使用 UsernameToken 身份验证:
crmclient_username_token
修改客户端部署描述符 WS 绑定以添加回调处理程序,该处理程序为添加到服务请求标头的 UsernameToken 提供用户名和密码:
crmclient_username_token_generator
保存 Web 服务客户端实现模块的部署描述符
客户端配置详细信息
本部分将描述其他客户端配置元素,您需要对这些元素进行设置才能使 ClientIdCallbackHandler 和应用程序正确运行。
每个部署环境的登录属性文件
在示例场景中,您需要能够对四个可能的开发环境中的每个开发环境使用不同的用户名和密码。“properties”文件夹已放置在称为 CRMWebServiceClientApp 的 Web 服务客户端企业应用程序的根目录中,其中包含 dev_login.properties、test_login.properties、qa_login.properties 和 prod_login.properties 文件。
图 9. 添加到企业应用程序的每个开发环境的登录属性文件
将属性文件夹添加到实现模块(例如,CRMWebServiceClient)的清单类路径;这需要使用文本编辑器(而不是通过模块属性 Java JAR 依赖项编辑器)编辑 manifest.mf:
设置 JVM deployment.environment 自定义属性
ClientIdCallbackHandler 将查看名为 deployment.environment 的 JVM 系统属性的值,以确定使用哪一个 login.properties 文件。若要将 JVM 自定义属性 deployment.environment 设置为适当的值,请执行以下操作:
deployment.environment
dev
、test
、qa
或 prod
(此值视为文件名“login.properties”的前缀,这样,只要允许,可以使用您需要的任何值)
提供者端的注册中心条目
已通过服务提供者将用户名添加到正在使用的安全注册中心。在开发环境测试服务器这一简单的示例中,使用了 WebSphere 示例文件注册中心。将一个条目添加到每个开发环境的注册中心,以反映在客户端上的 login.properties 文件中使用的用户名和密码。
清单 8. 提供者端的注册中心条目
#Users file registry entries: devuser:password1:207:406:"Development User" testuser:password1:208:406:"Test User" qauser:password1:209:406:"QA User" produser:password1:210:406:"Production User" |
在示例应用程序中,未定义任何 J2EE 角色,经过身份验证的任何 Web 服务用户都可以使用服务提供者应用程序。更实用的应用程序将定义 J2EE 角色,并将其映射到在安全注册中心中的组。更实用的部署将使用 LDAP 注册中心进行身份验证、用户和组管理。
检查客户端使用的服务端点 URL
在示例应用程序中,CustomerManagerService.wsdl 中的 URL 是 http://localhost:9080/CRMWebServiceRouter/service/CustomerManagerService。要修改主机名和端口号,请使用 WSDL 编辑器打开 WSDL 文件,并查看服务,直到获得 SOAP 地址。
图 16. WSDL 文件中的服务定义的 SOAP 地址绑定属性
URL 中的位置属性位于主 WSDL 编辑器窗格下的 Properties 视图中。
图 17. 位置属性 soap:address 是缺省的服务端点绑定
也可以在运行时使用 PortTypeProxy 上的 setEndpoint() 方法设置服务端点,PortTypeProxy 是为 Web 服务客户端生成的。例如,在 Web 服务客户端 Servlet 的 init() 方法中,您可以为服务代理设置端点。这比使用客户端 WSDL 文件中的内容更灵活。(可以在 WebSphere Service Registry and Repository 中找到管理服务端点绑定的可靠解决方案。)
在更实用的配置中,服务提供者将使用 HTTPS。在先前参考的 developerWorks 文章中描述了用于 Web 服务客户端的 SSL 配置。
提供者端的配置
本文没有介绍服务提供者上 UsernameToken 安全所需的配置,但是在 Irina Singh 的文章 中可以找到如何在服务提供者上配置 UsernameToken 安全的详细描述。
|
结束语
本文描述了自定义 JAAS 回调处理程序,在使用 Web 服务 UsernameToken 身份验证时,该处理程序使 Web 服务客户端程序能够动态设置用户名和密码。您学习了如何在 WebSphere Application Server V6.0 中使用包含完整源代码的完整工作示例为 Web 服务客户端配置回调处理程序。我们希望在您开发自已的 Web 服务项目时为您提供一些有用信息。
如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。
现场直击|2021世界人工智能大会
直击5G创新地带,就在2021MWC上海
5G已至 转型当时——服务提供商如何把握转型的绝佳时机
寻找自己的Flag
华为开发者大会2020(Cloud)- 科技行者