原文 http://www.newsmth.net/pc/pccon.php?id=10002535&nid=340064
安全套接层(SSL)及其新继任者传输层安全(TLS)是在互联网上提供保密安全通道的加密协议,为诸如网站、电子邮件、网上传真等等数据传输进行保密。TLS利用密钥算法在互联网上提供端点身份认证与通讯保密。在典型例子中,只有服务器被可靠身份认证(即其验证被确保),客户端踪迹不一定经可靠认证;相互间的身份认证需要公钥基础设施(PKI)设置于客户端中。协议的设计在某种程度上能够使客户端/服务器应用程序通讯本身预防窃听、干扰(Tampering)、和消息伪造。
TLS包含三个基本阶段:
1. 对等协商密钥算法支持
2. 基于公钥密码的密钥交换和基于证书的身份认证
3. 基于对称密钥的数据传输保密
在第一阶段,客户端与服务器协商所用密码算法。 当前广泛实现的算法选择如下:
l 公钥密码系统:RSA、Diffie-Hellman、DSA及Fortezza;
l 对称密钥系统:RC2、RC4、IDEA、DES、Triple DES及AES;
l 单向散列函数:MD5及SHA。
TLS的记录层(Record layer)用于封装更高层的HTTP等协议。记录层数据可以被随意压缩、加密,与消息验证码(MAC)打包在一起。每个记录层包都有一个content_type段用以记录更上层用的协议。
当一个连接被发起时,从客户端的角度看,要收发几个握手信号:
1. 发送一个ClientHello消息,说明它支持的密码算法列表、压缩方法及最高协议版本,也发送稍后将被使用的随机字节。
2. 然后收到一个ServerHello消息,包含服务器选择的连接参数,源自客户端初期所提供的ClientHello。
3. 当双方知道了连接参数,客户端与服务器交换证书(依靠被选择的公钥系统)。这些证书通常基于X.509,不过已有草案支持以OpenPGP为基础的证书。
4. 服务器能够请求得到来自客户端的证书,所以连接可以是相互的身份认证。
5. 客户端与服务器通过加密通道协商一个共同的“主密钥”,这通过精心谨慎设计的伪随机数函数实现。结果可能使用Diffie-Hellman交换,或简单的公钥加密,双方各自用私钥解密。所有其他关键数据的加密均使用这个“主密钥”。
TLS/SSL有多样的安全保护措施。所有的记录层数据均被编号,序号用在消息验证码(MAC)中。
公开密钥加密也称为非对称密钥加密,该加密算法使用两个不同的密钥:公开密钥和私有密钥。这两个密钥是数学相关的,用某用户私钥加密后所得的信息只能用该用户的公钥才能解密,反之亦然。RSA算法(由发明者Rivest,Shmir和Adleman姓氏首字母缩写而来)是最著名的公开密钥加密算法。非对称密钥加密的另一用途是身份验证:用私钥加密的信息,只能用公钥对其解密,接收者由此可知这条信息确实来自于拥有私钥的某人。
顾名思义,公开密钥是处于公共域的,它通常被发布出去。公钥发布的形式就是证书,其中除了包含公钥之外,还包含了身份信息,以及CA的签名。证书的传输格式标准一般使用X509证书格式。反之,私钥是被保护存储的,可以使用硬件或者普通的文件来存储私钥。一般使用PKCS#12格式将它和公钥加密存储在普通的文件中。在Java平台,一般也使用JKS格式。
因为证书中包含了身份信息,因为在商务应用中,交易双方会对证书进行验证。方法是查看CA签名。CA是指身份认证机构,是被交易双方都信任的第三方实体。任何人可以将自己的公钥和身份信息提交给CA进行认证,由CA对身份信息进行确实,然后签名发证。CA实际上也是使用非对称加密,同样拥有密钥和公钥。CA的公钥通常已经被内置到IE或者Netscape这样的软件,它使用自己的私钥对其它人的证书签名。理论上任何人都可以成为CA,为其它人的证书进行签名认证。但是,因为要求CA要被交易双方所信任,双方都必须包含有CA的信任记录(即证书),所以在Internet范围内的应用通常会选择Verisign等内置于IE或者Netscape的CA机构,当然,要交一笔不菲的美金。证书与CA的关系可以参照公民与公安部的关系。公民之间通过身份证相互认证,而公安部作为公民都信任的第三方签发携带公民身份信息的身份证。在前文SSL的概述中,服务器与客户端双方就是通过交换被CA签名的证书来验证对方的身份。
Java 5集成了对SSL的支持,而且提供了一个名为keytool.exe的命令行工具来管理证书与CA签名。keytool.exe位于JRE的bin文件夹下。本文使用%JAVA_HOME%\bin\keytool来指代它。Java 5内置了一些信任的CA证书,它们位于%JAVA_HOME%\lib\security\cacerts[1]文件内,可以使用keytool进行管理。下面是keytool的一些命令行参数:
-genkey |
生成新的密钥和公钥并保存(以下是它的参数) |
-alias <alias> |
在keystore中的名字,一个keystore可以存储多个密钥,每个密钥都有不同的名字,可以使用-alias引用 |
-keyalg <keyalg> |
密钥的生成算法,可以是RSA或者DSA |
-keysize <keysize> |
密钥长度,512位长的RSA密钥已经被破解,所以推荐个人使用1024位,CA使用2048位 |
-sigalg <sigalg> |
签名算法,可以使用SHA1或者MD5 |
-dname <dname> |
身份信息,X500格式 |
-validity <valDays> |
有效时间,以天为单位 |
-keypass <keypass> |
当前操作的密钥的密码,以防止未授权访问。注意,这个密码和storepass是不一样的,后者是保护整个存储文件的密码。由于很多客户端认为两个密码是一样的,如果设置了不同的密码可能会发生错误 |
-keystore <keystore> |
密钥的存储文件,如前所述,该存储文件将保存用户的公钥和私钥,也可以保存用户信任的证书(包括CA证书和普通证书)。前者称为keyEntry,后者称为trustedCertEntry。此外,可以使用两种格式存储该文件,分别是JKS和PKCS#12[2]。该文件受密码保护。 |
-storepass <storepass> |
keystore的密码。参见-keypass |
-storetype <storetype> |
存储文件的格式,可以是jks或者pkcs12。参见-keystore |
-certreq |
生成CA签名请求(以下是它的参数) |
-file <csr_file> |
签名请求存储文件名 |
-alias <alias> -sigalg <sigalg> -keypass <keypass> -keystore <keystore> -storepass <storepass> -storetype <storetype> |
参见-genkey |
-delete |
从存储文件中删除一个记录(以下是它的参数) |
-alias <alias> -keystore <keystore> -storepass <storepass> -storetype <storetype> |
参见-genkey |
-export |
将密钥导出(以下是它的参数) |
-rfc |
以RFC1421标准规定的格式导出密钥 |
-alias <alias> -file <cert_file> -keystore <keystore> -storepass <storepass> -storetype <storetype> |
参见-genkey |
-import |
将密钥导入存储文件(以下是它的参数) |
-noprompt |
不提示是否信任证书 |
-trustcacerts |
导入证书时考虑其它证书,包括前文所述的JDK内置的包含信任CA的证书存储文件 |
-alias <alias> -file <cert_file> -keypass <keypass> -keystore <keystore> -storepass <storepass> -storetype <storetype> |
参见-genkey |
-list |
列出密钥存储文件中的密钥(以下是它的参数) |
-rfc -alias <alias> -keystore <keystore> -storepass <storepass> -storetype <storetype> |
参见-genkey |
-printcert |
打印证书文件信息(以下是它的参数) |
-file |
参见-delete |
-selfcert |
自签名(以下是它的参数) |
-alias <alias> -dname <dname> -validity <valDays> -keypass <keypass> -sigalg <sigalg> -keystore <keystore> -storepass <storepass> -storetype <storetype> |
参见-genkey |
[1] 如果安装的是JDK,则对应%JAVA_HOME%\jre\lib\security\cacerts
[2] 支持的格式实际上取决于JDK或者当前已安装的支持软件包
keytool本身虽然可以管理密钥和证书,也能够完全自签名,可以满足普通的应用,但是如果需要根据PKI规范的要求建立证书链,就需要用到另一个工具OpenSSL。OpenSSL的命令行比较复杂。首先从OpenSSL的官方网站下载OpenSSL的编译版本[1]。下载安装后可以看到一个名为openssl.exe的可执行文件,它不需要其它动态链接库,可以将其拷贝到%SystemRoot%,即(C:\Windows\system32\)下。以下是它常用的几条子命令:
genrsa |
使用RSA算法生成密钥(以下是它的参数) |
-in <keyfile> |
密钥输入的文件名 |
-out <keyfile> |
密钥输出的文件名 |
req |
管理CA签名请求(以下是它的参数) |
-new |
生成CA签名请求 |
-out <careqfile> |
证书文件输出文件名 |
-key |
输入的密钥文件 |
x509 |
管理x509证书文件(以下是它的参数) |
-req |
对CA签名请求进行签名 |
-in <careqfile> |
输入CA签名请求文件 |
-out <cacertfile> |
签名后的证书文件 |
-signkey <keyfile> |
自签名使用的密钥文件 TODO 在CA签名中的作用待了解 |
-days |
有效期 |
-CAserial <caserial> |
保存CA签名序列号文件 |
-CAcreateserial |
如果没有,创建CA签名序列号 |
-CAkey <cakey> |
CA私钥文件 |
-CA |
CA证书 |
-sha1 |
使用SHA1散列算法 |
-trustout |
TODO 待了解 |
pkcs12 |
使用PKCS#12格式管理存储文件(以下是它的参数) |
-export |
导出PKCS#12格式的存储文件 |
-in <cacertfile/keystore> |
证书文件或者存储文件 |
-clcerts |
仅导出客户端证书 |
-inkey <keyfile> |
私钥文件 |
-password <storepass> |
保护存储文件的密码 |
-out <keystore/cacertfile/keyfile> |
存储文件或者证书、密钥 |
-nodes |
不加密私钥 |
-nokeys |
不包含私钥 |
[1] 大多数Linux一般已经预安装OpenSSL,试着运行输入openssl命令看看
main(sys.argv)
接下去我们将使用这个工具生成CA证书以及WebService客户端与服务器的证书。
1. 生成CA证书
D:\Goldfish\workon\temp>keymgr.py newca
请输入你的身份信息,这些信息将被附加到证书上,以便于客户确认您的身份
只输入英文
请输入您的省份:Fujian
请输入您的城市:Xiamen
请输入您的单位名称:fish
请输入您的部门名称:DD
请输入您的名字:ca
请输入您的电子邮箱地址:[email protected]
您的身份认证信息是/C=CN/CN=ca/L=Xiamen/O=fish/ST=Fujian/EMAILADDRESS=nobody@now
ere.com/OU=DD
请输入保护CA证书的密码(明文显示):123456
请记录好该密码,如果丢失该密码,将可能面临安全性破坏和重新部署客户端的风险
请输入CA证书的文件名:fish.pfx
2. 生成客户端证书
D:\Goldfish\workon\temp>keymgr.py newstore
将为服务器/客户端生成并使用CA证书签署PKCS12格式的证书文件
请输入CA证书的文件名:fish.pfx
请输入CA证书的密码(不回显):
请输入新证书的文件名:client.store
请输入保护新证书的密码(明文显示):123456
Java支持两种格式的证书存储格式,一种是Java环境私有的JKS格式,另一种是RFC标准的PK
CS#12格式
请选择,1-JKS格式,2-PKCS#12:1
请输入你的身份信息,这些信息将被附加到证书上,以便于客户确认您的身份
只输入英文
请输入您的省份:Fujian
请输入您的城市:Xiamen
请输入您的单位名称:fish
请输入您的部门名称:DD
请输入您的名字:client
请输入您的电子邮箱地址:
您的身份认证信息是/C=CN/CN=client/L=Xiamen/O=fish/ST=Fujian/OU=DD
3. 生成服务器证书,注意,服务器证书填写的名字要与服务器的域名一样,否则可能被某些客户端自动拒绝
D:\Goldfish\workon\temp>keymgr.py newstore
将为服务器/客户端生成并使用CA证书签署PKCS12格式的证书文件
请输入CA证书的文件名:fish.pfx
请输入CA证书的密码(不回显):
请输入新证书的文件名:server.store
请输入保护新证书的密码(明文显示):123456
Java支持两种格式的证书存储格式,一种是Java环境私有的JKS格式,另一种是RFC标准的PK
CS#12格式
请选择,1-JKS格式,2-PKCS#12:1
请输入你的身份信息,这些信息将被附加到证书上,以便于客户确认您的身份
只输入英文
请输入您的省份:Fujian
请输入您的城市:Xiamen
请输入您的单位名称:fish
请输入您的部门名称:DD
请输入您的名字:server
请输入您的电子邮箱地址:
您的身份认证信息是/C=CN/CN=server/L=Xiamen/O=fish/ST=Fujian/OU=DD
现在,当前工作目录下可以找到fish.pfx、client.store和server.store三个文件。其中fish.pfx是PKCS#12格式的CA密钥存储文件,client.store和server.store是经过CA签名的JKS格式的密钥存储文件,密钥在两者中都名为mykey。
使Tomcat是很简单的事,只需要对%CATALINA_HOME%\conf\server.xml进行简单的配置。在<Service/>元素里添加一个子元素<Connector/>,内容如下:
<Connector port="443"maxHttpHeaderSize="8192" maxThreads="150"minSpareThreads="25" maxSpareThreads="75" enableLookups="false"disableUploadTimeout="true" acceptCount="100"scheme="https" secure="true" clientAuth="true"sslProtocol="TLS" keystoreFile="D:\Goldfish\workon\server.store"keystorePass="123456" keystoreType="jks"keyAlias="mykey" truststoreFile="D:\Goldfish\workon\server.store"truststorePass="123456" truststoreType="jks"/>
其中几个属性与SSL支持有关,它们的作用描述如下:
scheme |
指定为https |
secure |
指定为true |
clientAuth |
是否认证客户端证书,如果为true会要求客户端提交它的证书,本例中是双向认证,所以设为true |
sslProtocol |
指定为TLS |
keystoreFile |
存储服务器密钥的存储文件 |
keystorePass |
存储服务器密钥的存储文件密码 |
keystoreType |
存储服务器密钥的存储文件的文件格式,可以为jks,也可以为pkcs12 |
keyAlias |
服务器密钥在存储文件中的名字。使用keymgr.py工具生成的密钥存储文件默认是mykey |
truststoreFile |
存储信任的证书文件的存储文件 |
truststorePass |
存储信任的证书文件的存储文件密码 |
truststoreType |
存储信任的证书文件的存储文件的文件格式。jks和pkcs12之一 |
port |
服务端口,默认的https端口号是443 |
重启Tomcat之后可以Web Service即可生效。可以使用浏览器进行测试。先把clientAuth改为false,在浏览器的地址栏里输入https://server/cxf/UserService?wsdl,如果可以显示WSDL文档,说明服务器的证书可以工作。再将clientAuth改成true。再次打开https://server/cxf/UserService?wsdl,如果浏览器提示选择一个证书以继续浏览,说明SSL支持已经配置成功。
在实际的生产环境中还应去除原有的不安全连接方法。方法是在该Web应用程序的web.xml文件中增加以下配置:
<web-app>
…
<security-constraint>
<web-resource-collection>
<web-resource-name>ProtectedContext</web-resource-name>
<url-pattern>/*</url-pattern>
</web-resource-collection>
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>
…
</web-app>
虽然CXF声称可以通过简单的配置支持SSL客户端,但是根据其用户手册操作时却碰到问题。本文利用CXF提供的辅助类,在客户端向服务器发起SSL连接之前,将客户端证书与CA证书的存储文件配置设置到CXF内部。代码如下:
UserServiceFactory.java:
import java.io.IOException;
import java.io.InputStream;
importjava.net.MalformedURLException;
import java.net.URL;
import java.security.KeyStore;
importjava.security.KeyStoreException;
importjava.security.NoSuchAlgorithmException;
importjava.security.UnrecoverableKeyException;
importjava.security.cert.CertificateException;
import javax.net.ssl.KeyManager;
importjavax.net.ssl.KeyManagerFactory;
importjavax.net.ssl.TrustManager;
importjavax.net.ssl.TrustManagerFactory;
import javax.xml.namespace.QName;
importorg.apache.cxf.configuration.jsse.TLSClientParameters;
importorg.apache.cxf.endpoint.Client;
importorg.apache.cxf.frontend.ClientProxy;
importorg.apache.cxf.transport.http.HTTPConduit;
importorg.apache.cxf.transports.http.configuration.HTTPClientPolicy;
import demo.cxf.User;
import demo.cxf.UserService;
importdemo.cxf.UserServiceService;
public class UserServiceFactory {
privatefinal static String keyStore = "client.store";
privatefinal static String trustStore = "client.store";
privatefinal static String trustStorePass = "123456";
privatefinal static String keyStorePass = "123456";
privatefinal static QName SERVICE = new QName("http://cxf.demo/","UserServiceService");
privatestatic UserService us;
/**
* 取得信任证书管理器
* @return
* @throws IOException
*/
privatestatic TrustManager[] getTrustManagers() throws IOException {
try{
Stringalg = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactoryfactory = TrustManagerFactory.getInstance(alg);
InputStreamfp = UserServiceFactory.class.getResourceAsStream(trustStore);
KeyStoreks = KeyStore.getInstance("JKS");
ks.load(fp,trustStorePass.toCharArray());
fp.close();
factory.init(ks);
TrustManager[]tms = factory.getTrustManagers();
returntms;
}catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}catch (KeyStoreException e) {
e.printStackTrace();
}catch (CertificateException e) {
e.printStackTrace();
}
returnnull;
}
/**
* 取得个人证书管理器
* @return
* @throws IOException
*/
privatestatic KeyManager[] getKeyManagers() throws IOException {
try{
Stringalg = KeyManagerFactory.getDefaultAlgorithm();
KeyManagerFactoryfactory = KeyManagerFactory.getInstance(alg);
InputStreamfp =UserServiceFactory.class.getResourceAsStream(keyStore);
KeyStoreks = KeyStore.getInstance("JKS");
ks.load(fp,keyStorePass.toCharArray());
fp.close();
factory.init(ks,keyStorePass.toCharArray());
KeyManager[]keyms = factory.getKeyManagers();
returnkeyms;
}catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}catch (KeyStoreException e) {
e.printStackTrace();
}catch (CertificateException e) {
e.printStackTrace();
}catch (UnrecoverableKeyException e) {
e.printStackTrace();
}
returnnull;
}
static{
UserServiceServiceservice = null;
try{
service= new UserServiceService(new URL("file:D:\\ws\\UserServiceService.wsdl"),
SERVICE);
}catch (MalformedURLException e) {
e.printStackTrace();
}
us= service.getUserServicePort();
Clientclient = ClientProxy.getClient(us);
HTTPConduithttpConduit = (HTTPConduit) client.getConduit();
TLSClientParameterstlsParams = httpConduit.getTlsClientParameters();
if(tlsParams == null)
tlsParams= new TLSClientParameters();
tlsParams.setSecureSocketProtocol("SSL");
try{
tlsParams.setKeyManagers(getKeyManagers());
tlsParams.setTrustManagers(getTrustManagers());
}catch (IOException e) {
e.printStackTrace();
}
httpConduit.setTlsClientParameters(tlsParams);
}
publicstatic UserService getInstance(){
returnus;
}
}
这些代码一目了解,不再进行深入的解释。在实际使用中只需酌情修改存储文件的位置、密码、服务名、与WSDL文件地址及由WSDL文档生成的接口或者类名。作为示例,要把client.store放到${CLASSPATH}下,最简单的就是放到与UserServiceFactory相同的位置。
使用了上述工厂模式之后,可以写出这样的测试代码:
UserServiceus=UserServiceFactory.getInstance();
User u=new User();
u.setUsername("fish");
us.createUser(u);
try {
us.deleteUser("fish");
} catch(NotFoundException_Exception e) {
e.printStackTrace();
}
上述代码将会在服务器端打印出
createUser
fish
并且在客户端得到一个NotFoundException_Exception异常
在生产环境中,不可避免地要碰到与客户中止合作,并取消其访问权限的情况。这通常表现为服务器端显式拒绝客户端的连接请求。基本思路是设置一个包含所有已注销客户的黑名单。但J2EE与CXF并没有提供对证书黑名单的支持。这时,我们需要使用CXF的插件功能,为CXF开发过滤插件。
在在之前,首先了解一下CXF的系统架构。简单地说,CXF使用流水线型(或者说总线型)处理机制,它的核心是一个Bus。一个客户端的请求或者一个对客户端桩代码的调用被组织成为一个Message。同时,所有的CXF功能都组织成Interceptor挂接在Bus上,分阶段依次处理Message。Message本质上是一个Map数据结构,既包含系统公共的也包含Interceptor自定义的数据。
要提供证书黑名单功能,首先要取得CXFServlet提供的HttpServletRequest对象,从中取得证书包含的身份信息,对其进行验证。代码如下:
package demo.cxf;
importjava.security.cert.X509Certificate;
importjavax.security.auth.x500.X500Principal;
importjavax.servlet.http.HttpServletRequest;
import org.apache.cxf.bus.CXFBusImpl;
importorg.apache.cxf.interceptor.Fault;
importorg.apache.cxf.message.Message;
importorg.apache.cxf.phase.AbstractPhaseInterceptor;
importorg.apache.cxf.phase.Phase;
importorg.apache.cxf.transport.http.AbstractHTTPDestination;
public class SSLFilter extendsAbstractPhaseInterceptor<Message> {
publicSSLFilter() {
super(Phase.RECEIVE);
}
privateCXFBusImpl bus;
publicCXFBusImpl getBus() {
returnbus;
}
publicvoid setBus(CXFBusImpl bus) {
this.bus= bus;
}
publicvoid handleMessage(Message msg) throws Fault {
HttpServletRequestrequest = (HttpServletRequest) msg.get(AbstractHTTPDestination.HTTP_REQUEST);
if(request == null)
return;
StringcertSubject = null;
X509Certificate[]certChain = (X509Certificate[]) request.getAttribute("javax.servlet.request.X509Certificate");
if(certChain == null) {
System.out.println("nojavax.servlet.request.X509Certificate instance");
}else {
intlen = certChain.length;
if(len > 0) {
X509Certificatecert = (X509Certificate) certChain[0];
X500PrincipalpSubject = cert.getSubjectX500Principal();
certSubject= pSubject.getName();
//判断客户的名字是否出现在吊销列表中,如果是的话,抛出异常
if(certSubject.indexOf("client2") != -1)
thrownew Fault(new RuntimeException("fish is here!"));
}
System.out.println(certSubject);
}
}
}
写完代码之后,要将这个Interceptor插接到CXF,方法是修改CXF的配置文件WEB-INF/beans.xml,如下(注意红色修改部分):
<beansxmlns="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/beanshttp://www.springframework.org/schema/beans/spring-beans.xsd
http://cxf.apache.org/jaxwshttp://cxf.apache.org/schemas/jaxws.xsd">
<importresource="classpath:META-INF/cxf/cxf.xml" />
<importresource="classpath:META-INF/cxf/cxf-extension-soap.xml" />
<importresource="classpath:META-INF/cxf/cxf-servlet.xml" />
<beanid="sslfilter" class="demo.cxf.SSLFilter">
<propertyname="bus" ref="cxf" />
</bean>
<jaxws:endpointid="UserService" implementor="demo.cxf.UserServiceImpl"address="/UserService">
<jaxws:inInterceptors>
<refbean="sslfilter" />
</jaxws:inInterceptors>
</jaxws:endpoint>
</beans>
参考文档: