上一篇讲了如何使用UsernameToken的方式来安全访问CXF,这篇将讲解使用证书的签名和加密技术来达到安全访问的目的。
1.证书的签名和加密的原理
在CXF官网关于WS-SECURITY的章节中首先介绍了,签名和加密的原理,图和文字很形象,就不再多说了。
下面附上本文中使用的生成证书的代码:
1. 生成别名和密码为 "serverkey"/"myPassword"的服务端证书,别名都使用小写(在keystore中存储的别名都是小写字符), 并保存在server-keystore.jks中(改证书用来服务端解密) keytool -genkey -alias serverkey -validity 365 -keypass myPassword -keystore server-keystore.jks -storepass myPassword -dname "cn=serverkey" -keyalg RSA 2. 自签名我们的生成的证书(正式环境应该由正式的公司来做这个步骤,比如Verisign) keytool -selfcert -alias serverkey -validity 365 -keystore server-keystore.jks -storepass myPassword -keypass myPassword 3. 从服务端keystore中导出公钥并且命名为 key.cer keytool -export -alias serverkey -file serverkey.cer -keystore server-keystore.jks -storepass myPassword 4. 将步骤3导出的证书导入到客户端的client-truststore.jks(用来做客户端加密) keytool -import -alias serverkey -file serverkey.cer -keystore client-truststore.jks -storepass myPassword 5. 生成别名和密码为 "clientkey"/"myPassword"的客户端证书, 并保存在client-keystore.jks中(改证书用来服务端解密) keytool -genkey -alias clientkey -validity 365 -keypass myPassword -keystore client-keystore.jks -storepass myPassword -dname "cn=clientkey" -keyalg RSA 6. 自签名我们的生成的证书(正式环境应该由正式的公司来做这个步骤,比如Verisign) keytool -selfcert -alias clientkey -validity 365 -keystore client-keystore.jks -storepass myPassword -keypass myPassword 7. 从客户端keystore中导出公钥并且命名为 key.cer keytool -export -alias clientkey -file clientkey.cer -keystore client-keystore.jks -storepass myPassword 8. 将步骤3导出的证书导入到服务端的server-truststore.jks(用来做客户端加密) keytool -import -alias clientkey -file clientkey.cer -keystore server-truststore.jks -storepass myPassword
执行完,你可以在%JDK_HOME%/bin目录得到4个jks文件(数字证书库),这就是我们即将用来加密和签名的证书文件了。
2.添加四个证书配置文件
- Client_Encrypt.properties
- Client_Sign.properties
- Server_Decrypt.properties
- Server_SignVerf.properties
四个文件格式都一样,里面配置的keystore的类型、地址、密码以及做相应操作的证书别名。
内容如下:
org.apache.ws.security.crypto.provider=org.apache.ws.security.components.crypto.Merlin org.apache.ws.security.crypto.merlin.keystore.type=jks org.apache.ws.security.crypto.merlin.keystore.password=myPassword org.apache.ws.security.crypto.merlin.keystore.alias=clientKey org.apache.ws.security.crypto.merlin.keystore.file=resource/keystore/server-truststore.jks
3.修改客户端和服务端spring配置文件
各个配置文件中的内容相应做了注释,请看下面的详细文件
服务端配置:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jaxws="http://cxf.apache.org/jaxws" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd"> <import resource="classpath:META-INF/cxf/cxf.xml" /> <import resource="classpath:META-INF/cxf/cxf-servlet.xml" /> <jaxws:endpoint id="helloWorld" implementor="com.demo.cxf.helloword.impl.HelloWordImpl" address="/HelloWorld"> <jaxws:inInterceptors> <ref bean="serverWSS4JInInterceptor" /> <bean class="com.demo.cxf.helloword.ClientIpInInterceptor" /> <bean class="org.apache.cxf.interceptor.LoggingInInterceptor" /> </jaxws:inInterceptors> <jaxws:outInterceptors> <ref bean="serverWSS4JOutInterceptor" /> <bean class="org.apache.cxf.interceptor.LoggingOutInterceptor" /> </jaxws:outInterceptors> </jaxws:endpoint> <bean id="passwordCallback" class="com.demo.cxf.callbacks.PasswordCallback"></bean> <bean id="serverWSS4JInInterceptor" class="org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor"> <property name="properties"> <map> <entry key="action" value="Timestamp Encrypt Signature" /> <!-- 服务器会自动在SOAP中拿到解码(私钥)的用户名,并在 PasswordCallback中取到密码。 公钥不需要密码。 --> <entry key="passwordCallbackRef"> <ref bean="passwordCallback" /> </entry> <entry key="decryptionPropFile" value="resource/properties/Server_Decrypt.properties" /> <entry key="signaturePropFile" value="resource/properties/Server_SignVerf.properties" /> </map> </property> </bean> <bean id="serverWSS4JOutInterceptor" class="org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor"> <property name="properties"> <map> <!-- 此处配置需注意,当指定Signature时就必须像UsernameToken那样指定user和passwordCallbackRef。 因为假如我们没指定signatureUser或者encryptionUser,CXF将会使用user来替代之,而signatureUser的 密码必须通过passwordCallbackRef赋值。所以哪怕定义了signatureUser也必须同时定义user, 且不能为空。 公钥不需要密码。 --> <entry key="action" value="Timestamp Encrypt Signature" /> <!-- MD5加密明文密码 --> <entry key="passwordType" value="PasswordDigest" /> <!-- 该用户名只能在激活了UsernameToken时才能拿到并使用 --> <entry key="user" value="admin" /> <entry key="passwordCallbackRef"> <ref bean="passwordCallback" /> </entry> <entry key="encryptionPropFile" value="resource/properties/Server_SignVerf.properties" /> <entry key="encryptionUser" value="clientkey" /> <entry key="signaturePropFile" value="resource/properties/Server_Decrypt.properties" /> <entry key="signatureUser" value="serverkey" /> </map> </property> </bean> </beans>
客户端配置:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:jaxws="http://cxf.apache.org/jaxws" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd"> <jaxws:client id="helloClient" serviceClass="com.demo.cxf.helloword.HelloWord" address="http://10.248.157.51:8080/web_service/services/HelloWorld"> <jaxws:inInterceptors> <ref bean="clientWSS4JInInterceptor"/> </jaxws:inInterceptors> <jaxws:outInterceptors> <ref bean="clientWSS4JOutInterceptor" /> </jaxws:outInterceptors> </jaxws:client> <bean id="passwordCallback" class="com.demo.cxf.callbacks.PasswordCallback"></bean> <bean id="clientWSS4JInInterceptor" class="org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor"> <property name="properties"> <map> <entry key="action" value="Timestamp Encrypt Signature" /> <entry key="passwordCallbackRef"> <ref bean="passwordCallback" /> </entry> <entry key="decryptionPropFile" value="resource/properties/Client_Sign.properties" /> <entry key="signaturePropFile" value="resource/properties/Client_Encrypt.properties" /> </map> </property> </bean> <bean id="clientWSS4JOutInterceptor" class="org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor"> <property name="properties"> <map> <entry key="action" value="Timestamp Encrypt Signature" /> <entry key="passwordType" value="PasswordDigest" /> <entry key="user" value="admin" /> <entry key="passwordCallbackRef"> <ref bean="passwordCallback" /> </entry> <entry key="encryptionPropFile" value="resource/properties/Client_Encrypt.properties" /> <entry key="encryptionUser" value="serverkey" /> <entry key="signaturePropFile" value="resource/properties/Client_Sign.properties" /> <entry key="signatureUser" value="clientkey" /> </map> </property> </bean> </beans>
- 客户端发送数据前:使用服务端的公钥进行加密,同时使用客户端的私钥进行签名
- 服务端收到请求:使用服务端的私钥解密,并使用客户端的公钥进行签名验证
- 服务端响应前:使用客户端的公钥进行加密,同时使用服务端的私钥进行签名
- 客户端收到响应:使用客户端的私钥解密,并使用服务端的公钥进行签名验证
这两个配置文件的大致内容如上。
4.添加PasswordCallback
UsernameToken中就使用过,不多做解释,只是需要注意下面代码中的证书密码部分,证书密码在客户端和服务端分别只需要保存己方的私钥密码,公钥是不需要密码的。
package com.demo.cxf.callbacks; import java.io.IOException; import java.util.HashMap; import java.util.Map; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.callback.UnsupportedCallbackException; import org.apache.ws.security.WSPasswordCallback; public class PasswordCallback implements CallbackHandler { Map<String, String> user = new HashMap<String, String>(); { // 用户名和密码 user.put("admin", "123"); user.put("su", "123"); // 证书的密码 user.put("serverkey", "myPassword"); user.put("clientkey", "myPassword"); } @Override public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { WSPasswordCallback wpc = (WSPasswordCallback) callbacks[0]; System.out.println(wpc.getIdentifier()); if (!user.containsKey(wpc.getIdentifier())) { throw new SecurityException("权限不足!"); } /* * 此处特别注意:: WSPasswordCallback 的passwordType属性和password 属性都为null, * 你只能获得用户名(identifier), 一般这里的逻辑是使用这个用户名到数据库中查询其密码, 然后再设置到password * 属性,WSS4J 会自动比较客户端传来的值和你设置的这个值。 你可能会问为什么这里CXF * 不把客户端提交的密码传入让我们在ServerPasswordCallbackHandler 中比较呢? * 这是因为客户端提交过来的密码在SOAP 消息中已经被加密为MD5 的字符串, * 如果我们要在回调方法中作比较,那么第一步要做的就是把服务端准备好的密码加密为MD5 字符串, 由于MD5 * 算法参数不同结果也会有差别,另外,这样的工作CXF 替我们完成不是更简单吗? */ // 如果包含用户名,就设置该用户名正确密码,由CXF验证密码 wpc.setPassword(user.get(wpc.getIdentifier())); } }
5.其他的SEI和IMPL请参考上一篇中的代码,完全一样,附上代码。