简介: 随着 Web Service 应用越来越广泛, 如何保证服务能够安全的访问和传输,也逐渐引起开发人员和用户的关注。Web Service 的安全可以从两方面考虑:访问安全和传输安全 .。前者主要指只有授权用户可以访问应用, 而后者侧重于在消息传输过程中如何保证消息的私密性和完整性。本文介绍了如何在 Apache Geronimo 开发和部署安全的 Web Service 应用,以确保信息交互的安全,将侧重介绍如何基于 HTTP/HTTPS 协议保护 Web Service 应用访问和传输的安全。
在 Java EE 平台上,先后有了 JAX-RPC 和 JAX-WS 两个 Web Service 应用规范,但是两者均未对安全方面做详细规定。那么如何确保 Web Service 应用的安全呢?总所周知,Web Service 应用的服务端和客户端是使用 SOAP 作为交互协议,而 SOAP 作为一个应用层的协议,可以基于多种其他协议传输, 例如 HTTP/HTTPS,FTP 等。 而在实际的应用环境之中, HTTP/HTTPS 协议是应用最为广泛的 .。事实上, 在 SOAP 和 Web Service 的相关规范中, 主要对 SOAP 基于 HTTP/HTTPS 上进行传输作了说明。显然,当我们考虑 Web Service 安全时, 在传输协议进行安全控制便是一个自然而然的选择方案之一。
Apache Geronimo 中的 Web Service Provider
Apache Geronimo 共集成了三个比较流行的 Web Service 引擎, 分别是 Apache Axis, Apache Axis2 和 Apache CXF。 对于 Axis, 通过集成其实现对 JAX-RPC 规范的支持, 后续两个项目, 侧重提供对 JAX-WS 规范的支持。 在 Geronimo 的两个发行版本 Geronimo-Tomcat 和 Geronimo-Jetty 中, 分别默认启用了 Axis2 和 CXF。对于 Geronimo 的两个发行版本,用户可以通过安装对应插件并进行简单配置以切换至另外一种 Web Service 引擎, 详情请参照 Geronimo 的相关文档,,本文不再作累述。 默认情况下, 本文示例均是运行于 Geronimo-Tomcat 发行版本上,并使用 Axis2 作为 Web Service 引擎,并遵循 JAX-WS 规范编写。
本文中, 我们以一个在线书店的应用为例, 其提供通过书名检阅图书的功能, 并以 Web Service 的形式对外提供服务。同时还有一个 Web 客户端应用,来访问在线书店提供的服务。如 清单 1 所示,服务端是一个简单的 POJO 类,并添加了 WebService 标识。其提供 queryByName
的方法,以传入的 name
为参数检索是否有符合条件的书籍,最终返回一个 Book
对象数组。
@WebService(name = "BookStore", targetNamespace = "http://geronimo.apache.org/bookstore") public class BookStoreImpl { private List<Book> books = new LinkedList<Book>(); @PostConstruct protected void initialize() { books.add(new Book("1", "Thinking In Java", "Bruce")); books.add(new Book("2", "WAS CE Bible", "WAS CE Team")); } public Book[] queryByName(String name) { if (name == null || name.length() == 0) { return new Book[0]; } List<Book> foundBookList = new ArrayList<Book>(); for (Book book : books) { if (book.getName().indexOf(name) != -1) { foundBookList.add(book); } } return foundBookList.toArray(new Book[0]); } @PreDestroy protected void destory() { books.clear(); } } |
同时,在部署文件 web.xml 中,我们将 清单 1 中所示 POJO 类以 Servlet 形式对外发布,具体配置请参照 清单 2 。
<servlet> <servlet-name>BookStore</servlet-name> <servlet-class> org.apache.geronimo.samples.securityWebService.BookStoreImpl </servlet-class> </servlet> <servlet-mapping> <servlet-name>BookStore</servlet-name> <url-pattern>/bookstore</url-pattern> </servlet-mapping> |
将以上所示的应用部署至 Apache Geronimo 中后,在浏览器输入 http://localhost:8080/SecurityWebServices/bookstore 后,如若 图片 1 所示,即表示该在线书店的应用已经成功部署了。
图片 1. BookStoreImplService 访问页面
客户端同样是一个 Web 应用程序,首先通过工具根据 Geronimo 生成的 WSDL 文件生成本地的相关类文件, 通过在 BookStoreClient 中使用 WebServiceRef
标识注入的方式获取 BookStore 服务的引用, 请参照 清单 3 :
BookStoreClient.java @WebServiceRef(name = "services/BookStore") private BookStoreImplService service; protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String queryName = request.getParameter("name"); if (queryName != null && queryName.length() > 0) { request.setAttribute("books", service.getPort(BookStore.class).queryByName(queryName)); } request.getRequestDispatcher("index.jsp").forward(request, response); } |
而在客户端应用的 web.xml 和 geronimo-web.xml 文件中, 需要配置该服务的一些参数, 请参照 清单 4 。设置参数主要包括服务端 Web Service 的发布地址等信息,对应于 清单 2 中的配置。
清单 4. web.xml/geronimo-web.xml 配置片段
web.xml <service-ref> <service-ref-name>services/BookStore</service-ref-name> <service-interface> org.apache.geronimo.samples.securityWebService.BookStoreImplService </service-interface> </service-ref> geronimo-web.xml <name:service-ref> <name:service-ref-name>services/BookStore</name:service-ref-name> <name:port> <name:port-name>BookStorePort</name:port-name> <name:protocol>http</name:protocol> <name:host>localhost</name:host> <name:port>8080</name:port> <name:uri>/SecurityWebServices/bookstore</name:uri> </name:port> </name:service-ref> |
至此, 我们完成了一个简单的在线书库的例子。通过在浏览器输入 http://localhost:8080/SecurityWebServiceClient/,并以 Java 作为关键字检索书籍,将如 图片 2 所示。
目前为止,示例中的 Web Service 应用没有任何安全设置,任何客户端均可以直接访问。 在后续的章节中,我们将基于该示例展示在 Apache Geronimo 中如何使用各种安全策略来确保 Web Service 应用的安全,包括授权访问和传输安全。
HTTP BASIC 的认证方式非常简单,当客户端向服务端受保护资源发起请求时,服务端会返回的消息中包括这样一个消息头 WWW-Authenticate: Basic realm="example.com",此时如果客户端是浏览器,则一个输入框将弹出,提示用户输入用户名和密码。 在用户输入用户名和密码之后,客户端使用 Base64 编码方式对用户名和密码进行加密并返回至服务端。由于我们的 Web Service 应用实质上是通过一个 Servlet 的形式对外发布, 显而易见,我们可以通过在 web.xml 和 geronimo-web.xml 中为 Servlet 对应的访问路径进行安全设置, 由此可达到对该 Web Service 应用授权访问。 请参照 清单 5 和 清单 6 。
<security-constraint> <web-resource-collection> <web-resource-name>basicResources</web-resource-name> <url-pattern>/bookstore/*</url-pattern> <http-method>POST</http-method> <http-method>GET</http-method> </web-resource-collection> <auth-constraint> <role-name>admin</role-name> </auth-constraint> <user-data-constraint> <transport-guarantee>NONE</transport-guarantee> </user-data-constraint> </security-constraint> <login-config> <auth-method>BASIC</auth-method> <realm-name>geronimo-admin</realm-name> </login-config> <security-role> <role-name>admin</role-name> </security-role> |
<web:security-realm-name>geronimo-admin</web:security-realm-name> <sec:security> <sec:role-mappings> <sec:role role-name="admin"> <sec:principal name="admin" class="org.apache.geronimo.security.realm.providers.GeronimoGroupPrincipal" /> </sec:role> </sec:role-mappings> </sec:security> |
在 清单 5 中设置了对访问路径 /bookstore 的任何 GET 和 POST 请求都需要使用 HTTP BASIC 认证方式,允许访问的角色为 admin。而在 清单 6 中, 指定了执行认证的安全域,出于示例方便。我们直接使用了 Geronimo 自带的 geronimo-admin 作为指定安全域,,用户完全可以定义和部署自己的安全域,同时将角色 admin 映射为安全域中的 admin 组,即当登录用户隶属于 admin 组时,其将具备对受保护资源的访问权限。 当重新部署之后, 此时再通过浏览器访问 http://localhost:8080/SecurityWebServices/bookstore?WSDL, 会提示输入用户名和密码。如 图片 3 所示。
图片 3. BookStoreImplService 访问页面
接下来,我们对 Web 客户端进行修改,使其可以访问受 HTTP BASIC 认证方式保护的 Web Service 应用。 在 JAX-WS 的 API 中, 可以通过 BindingProvider
接口来设置认证所需的用户名和密码。 请参照 清单 7 ,出于示例方便, 用户名和密码采取硬编码的方式, 实际使用时可以采取更灵活的方式。
String queryName = request.getParameter("name"); if (queryName != null && queryName.length() > 0) { BookStore bookStore = service.getPort(BookStore.class); BindingProvider bindingProvider = (BindingProvider) bookStore; bindingProvider.getRequestContext().put(BindingProvider.USERNAME_PROPERTY, "system"); bindingProvider.getRequestContext().put(BindingProvider.PASSWORD_PROPERTY,"manager"); request.setAttribute("books", bookStore.queryByName(queryName)); } request.getRequestDispatcher("index.jsp").forward(request, response); |
通过在客户端代码中添加认证信息,我们的客户端又可以访问在线书店的 Web Service 应用了。从 清单 7 可 以注意到, 用户需要修改客户端代码以添加登录所需信息。 事实上, Apache Geronimo 在应用服务器平台级别提供了相关支持, 用户可以通过在部署文件中定义 Credential Store 和指定登录信息的方式, 由应用服务器来处理登录所需事宜。下面示例中, 我们仍然使用应用服务器自带的 geronimo-admin 安全域, 在实际开发和生成环境, 用户必需定义自己的安全域以及验证模块。 详细步骤如下 :
BookStoreClient
类中用于登录认证的相关代码。 <module name="org.apache.geronimo.framework/server-security-config/2.2/car"> <gbean name="org.apache.geronimo.framework/server-security-config/2.2/car? ServiceModule=org.apache.geronimo.framework/server-security-config/2.2/car, j2eeType=LoginModule,name=simple-crddentials-login" gbeanInfo="org.apache.geronimo.security.jaas.LoginModuleGBean"> <attribute name="loginModuleClass"> org.apache.geronimo.security.realm.providers. GeronimoPropertiesFileMappedPasswordCredentialLoginModule </attribute> <attribute name="options"> credentialsURI=var/security/simple_credentials.properties </attribute> <attribute name="loginDomainName">simple-crddentials</attribute> </gbean> <gbean name="org.apache.geronimo.framework/server-security-config/2.2/car? ServiceModule=org.apache.geronimo.framework/server-security-config/2.2/car, j2eeType=LoginModuleUse,name=simple-crddentials-login-use" gbeanInfo="org.apache.geronimo.security.jaas.JaasLoginModuleUse"> <attribute name="controlFlag">REQUIRED</attribute> <reference name="LoginModule"> <pattern> <name>simple-crddentials-login</name> </pattern> </reference> </gbean> <gbean name="org.apache.geronimo.framework/server-security-config/2.2/car? ServiceModule=org.apache.geronimo.framework/server-security-config/2.2/car, j2eeType=LoginModuleUse,name=properties-login"> <reference name="Next"> <pattern> <name>simple-crddentials-login-use</name> </pattern> </reference> </gbean> </module> |
在 清单 8 中,我们为当前服务器默认使用的安全域的登录链添加一个新的 LoginModule GeronimoPropertiesFileMappedPasswordCredentialLoginModule
, 此模块的作用是当用户使用其所在的安全域进行认证时,会将在 simple-credentials.properties 配置文件中对应的用户名和密码信息对添加到 Subject
中 的 Private Credentials 中去。以属性文件中第一行的内容为例,system=system:system=manager,第一个 system 对应于当前登录所使用的用户名,第二个 system 为登录信息的名称,最后的 system 和 manager 则为真正使用的用户名密码信息。后续会介绍 Geronimo 如何使用保存在 Subject 中的 Private Credentials 信息为 Web Service 的访问提供平台级别支持。
<name:service-ref> <name:service-ref-name>services/BookStore</name:service-ref-name> <name:port> <name:port-name>BookStorePort</name:port-name> <name:protocol>http</name:protocol> <name:host>localhost</name:host> <name:port>8080</name:port> <name:uri>/SecurityWebServices/bookstore</name:uri> <name:credentials-name>system</name:credentials-name> </name:port> </name:service-ref> <web:security-realm-name>geronimo-admin</web:security-realm-name> <app:security xsi:type="sec:securityType" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <sec:credential-store-ref> <dep:name>SimpleCredentialStore</dep:name> </sec:credential-store-ref> <sec:default-subject> <sec:realm>geronimo-admin</sec:realm> <sec:id>system</sec:id> </sec:default-subject> </app:security> <dep:gbean name="SimpleCredentialStore" class="org.apache.geronimo.security.credentialstore.SimpleCredentialStoreImpl" xsi:type="dep:gbeanType"> <dep:xml-attribute name="credentialStore"> <credential-store xmlns="http://geronimo.apache.org/xml/ns/credentialstore-1.0"> <realm name="geronimo-admin"> <subject> <id>system</id> <credential> <type> o.a.g.security.credentialstore.NameCallbackHandler </type> <value>system</value> </credential> <credential> <type> o.a.g.security.credentialstore.PasswordCallbackHandler </type> <value>manager</value> </credential> </subject> </realm> </credential-store> </dep:xml-attribute> </dep:gbean> |
清单 9 为 更新后的 Web Service 客户端的部署文件,相比于没有安全设置的配置,可以看出, service-ref 元素增加了一个子元素 credentials-name, 同时在安全配置中添加了 default-subject 和 credential-store-ref 两个元素。关于各项配置的含义,请参照 表格 1 :
service-ref-name/port/credentials-name | system | 该名称对应于用户名密码对的名称 |
security/credential-store-ref/name | SimpleCredentialStore | 登录时使用的 Credential Store,主要包含了登录时所使用的 CallbackHandler 信息,当前配置值指向了后续的 SimpleCredentialStore GBean |
security/default-subject/realm | geronimo-admin | 登录时使用的安全域 |
security/default-subject/id | system | 和上一行的 realm 值一起在 Credential Store 中定位到登录时所使用的 CallbackHandler 设置 |
那么 Geronimo 是如何通过以上配置来提供相关功能呢?具体步骤如下所示:
如此,通过 清单 8 中关于登录模块的配置和 清单 9 中关于 Default Subject 的配置,实现了在服务器级别的安全支持,而对于 Web Service 客户端程序而言,无需在其代码中添加安全认证的逻辑。事实上,正如第三步骤所描述,Geronimo 在后台执行的代码与 清单 7 一致,相关代码位于 JAX-WS 插件中 geronimo-jaxws 的 org.apache.geronimo.jaxws.client.PortMethodInterceptor 类中 , 感兴趣的读者可以结合阅读。
另外,如果远端请求的用户名和密码与登录到当前域的用户密码是一致的,如当前示例中,则可以使用 Geronimo 另外一个 LoginModule 的实现 org.apache.geronimo.jaas.NamedUPCredentialLoginModule,并指定 org.apache.geronimo.jaas.NamedUPCredentialLoginModule.Name 选项为 credentials-name 元素的值,也可以达到同样的效果。但相比较于 清单 8 中 所使用的 GeronimoPropertiesFileMappedPasswordCredentialLoginModule 来说,略欠缺灵活性,但是工作原理一致,即将远端 Web Service 登录所需信息以 Private Credential 的形式添加到当前会话的 Subject 中。
使用上述 HTTP BASIC 的认证方式,我们达到了 Web Service 应用授权访问的目的,但是登录成功之后,服务端和客户端之间交互的 SOAP 信息也是使用明文传输, 第三方完全可以通过截取网络传输包获取 SOAP 消息的内容。而通过 SSL, 可以最大程度上弥补这一缺陷,使得 Web Service 交互的安全性得到进一步的提高。
首先, 修改 Web Service 服务端的配置文件, 将 transport-guarantee 元素的值由原来的 NONE 改成 CONFIDENTIAL, 这样将对受保护资源将使用 SSL 连接方式。 如 清单 10 所示 :
<security-constraint> <web-resource-collection> <web-resource-name>basicResources</web-resource-name> <url-pattern>/bookstore/*</url-pattern> <http-method>POST</http-method> <http-method>GET</http-method> </web-resource-collection> <auth-constraint> <role-name>admin</role-name> </auth-constraint> <user-data-constraint> <transport-guarantee>CONFIDENTIAL </transport-guarantee> </user-data-constraint> </security-constraint> |
其次, 客户端访问时, 需要使用 HTTPS 协议和端口进行访问, 将 geronimo-web.xml 文件中的 service-ref 更新如清单 11 所示 :
<name:service-ref> <name:service-ref-name>services/BookStore</name:service-ref-name> <name:port> <name:port-name>BookStorePort</name:port-name> <name:protocol>https</name:protocol> <name:host>localhost</name:host> <name:port>8443</name:port> <name:uri>/SecurityWebServices/bookstore</name:uri> <name:credentials-name>system</name:credentials-name> </name:port> </name:service-ref> |
最后,关闭 Geronimo 服务器,在命令行执行 set GERONIMO_OPTS=-Djavax.net.ssl.trustStore=$YOUR_GERONIMO_HOME/var/security/keystores/geronimo-default -Djavax.net.ssl.trustStorePassword=secret, 并重新启动 Geronimo 服务器。此时 Web Service 客户端与服务端的交互, 将通过 HTTPS 进行传输 . 在设置 Trust Store 时, 可以依据实际环境使用其他方式, 比如将服务端的公钥导入 JRE 的默认 Trust Store。需要强调地是,本文示例中 Web Service 服务端和客户端使用的是同一个 Geronimo 实例,而 Geronimo 默认的 HTTPS Connector 使用的证书是 $YOUR_GERONIMO_HOME/var/security/keystores/geronimo-default, 所以设置 javax.net.ssl.trustStore 也指向同一位置。如果需要访问第三方发布并基于 HTTPS 协议传输的 Web Service 应用,则需要将其证书导入到本地的 Trust Store 中,并为以上两个参数设置合适的值。
在授权访问方面,除了基于用户名和密码的 HTTP BASIC 认证方式之外,CLIENT-CERT 也是一个比较好的选择。通常基于 HTTPS 的访问中,只需要服务端提供证书表明其身份,在之前的例子也介绍到,客户端需要将服务端的公共证书添加到本地受信密钥库中。而当使用 CLIENT-CERT 认证方式时,客户端也必需提供证书来表明自己的身份,换句话说,服务端和客户端均需要提供证书以表明各自身份。所以,CLIENT-CERT 是一种比较特殊的 SSL 使用方式。现实情况中,这种认证方式使用的不是很多,因为大多数用户并没有自己的证书,但在 B2B 场景中会见到其身影。例如两家公司之间可以使用该认证方式来确保信息交互的安全。
为示例方便,我们将使用两个 Geronimo 应用服务器实例,分别部署 Web Service 应用的服务端和客户端。为避免端口冲突,用户可以通过修改其中一台服务器实例的 var/config/config-substitutions.properties 文件中 PortOffset 的值为 10,使得两个服务器实例同时在一台机器上运行。实现步骤如下所示:
1. 使用 KeyTool 或者其他工具为 Web Service 客户端生产密钥库文件 client.jks,并导出客户端证书,再将其导入服务端受信的密钥库文件 trustedclient.jks 中。
2. 在 Web Service 服务端所在的应用服务器,通过控制台中 Web Server 配置页面,添加支持 CLIENT-CERT 方式的 HTTPS Connector。填写配置时,需要注意的是以下配置项:
表 2. CLIENT-CERT HTTPS Connector 关键配置项
keystoreFile | ../security/keystores/geronimo-admin | 服务端 HTTPS Connector 使用的密钥库文件位置 |
port | 8444 | 服务端 HTTPS Connector 监听端口 |
clientAuth | true | 是否支持 CLIENT-CERT 认证方式 |
keyAlias | 密钥库中服务端所使用证书的别名,如果不设置,则为密钥库中读取的第一个密钥 | |
keystorePass | secret | 用于访问密钥库文件的密码 |
truststoreFile | ../security/keystores/trustedclient.jks | 用户验证客户端证书的密钥库文件位置 |
truststorePass | secert | 访问受信任客户端密钥库文件的密码 |
3. 修改 Web Service 服务端部署文件,如 清单 12 所示:
清单 12. 使用 CLIENT-CERT 认证方式的 Web Service 服务端部署文件 geronimo-web.xml 片段
<web:security-realm-name>client-cert-realm</web:security-realm-name> <sec:security> <sec:credential-store-ref> <dep:name>client-cert-credential-store</dep:name> </sec:credential-store-ref> <sec:default-subject> <sec:realm>client-cert-realm</sec:realm> <sec:id>default</sec:id> </sec:default-subject> <sec:role-mappings> ...... </sec:role-mappings> </sec:security> <gbean name="client-cert-realm" class="org.apache.geronimo.security.realm.GenericSecurityRealm" xsi:type="dep:gbeanType"> <attribute name="realmName">client-cert-realm</attribute> <attribute name="global">true</attribute> <reference name="ServerInfo"> <name>ServerInfo</name> </reference> <xml-reference name="LoginModuleConfiguration"> <log:login-config xmlns:log="......"> <log:login-module control-flag="REQUIRED" wrap-principals="false"> <log:login-domain-name>client-cert-realm</log:login-domain-name> <log:login-module-class> o.a.g.security.realm.providers.PropertiesFileNoPasswordLoginModule </log:login-module-class> <log:option name="groupsURI"> var/security/groups.properties</log:option > </log:login-module> </log:login-config> </xml-reference> </gbean> <gbean name="client-cert-credential-store" class="org.apache.geronimo.security.credentialstore.SimpleCredentialStoreImpl"> <xml-attribute name="credentialStore"> <credential-store xmlns="......"> <realm name="client-cert-realm"> <subject> <id>default</id> <credential> <type> o.a.g.security.credentialstore.NameCallbackHandler </type> <value>system</value> </credential> </subject> </realm> </credential-store> </xml-attribute> </gbean> |
当使用 CLIENT-CERT 认证方式时,服务端和客户端之间的证书验证在 SSL 握手阶段已经完成,即一旦握手成功,表明用户身份得到确认,故而该请求具备了受保护资源的访问权限。如 清单 12 所 示,使用了与 HTTP BASIC 认证时 Web Service 客户端类似的部署配置,使用 default-subject 配置项,并结合 Security Realm 和 Credential Store 设置,为用户会话添加默认的安全设置,使之可以访问受保护的资源。
4. 修改 Web Service 客户端部署文件中的端口值,改为 8444,即服务端新建支持 CLIENT-CERT 认证方式的 HTTPS Connector 监听端口。
5. 启动 Web Service 客户端所在 Geronimo 应用服务器之前,在命令行使用 GERONIMO_OPTS 设置以下系统属性:
javax.net.ssl.trustStore | $YOUR_GERONIMO_HOME/gt/var/security/keystores/geronimo-default | 客户端受信密钥库,包含从服务端导入的公钥信息 |
javax.net.ssl.trustStorePassword | secret | 客户端受信密钥库访问密码 |
javax.net.ssl.keyStore | $YOUR_GERONIMO_HOME/gt/var/security/keystores/client.jks | 客户端密钥库,包含客户端自身的私钥信息 |
javax.net.ssl.keyStorePassword | secret | 客户端密钥库访问密码 |
从表格 3 的设置可以看出,Web Service 客户端和服务端设置是完全对等的,只是客户端是通过系统属性设置,而服务端则通过 HTTPS Connector 设置。也说明了 CLIENT-CERT 本质上是双向证书认证方式。
本文以一个简单的在线书店 Web Service 应用为例,介绍了如何基于 HTTP/HTTPS 保护 SOAP 信息交互安全。概而述之,通过 HTTP BASIC/CLIENT-CERT 实现对 Web Service 应用的授权访问,而通过 SSL 确保消息传输的完整性和私密性。实际环境中,可以基于需求选择合适的安全策略。
原文:http://www.ibm.com/developerworks/cn/opensource/os-cn-ag-secwebs/index.html?ca=drs-