上一篇讲了如何使用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请参考上一篇中的代码,完全一样,附上代码。