电子证书在现实世界的使用,以及CXF如何支持SSL.


类型:Java,创建时间:Dec. 30, 2011, 6:14 p.m.

标题无“转载”即原创文章,版权所有。转载请注明来源:http://hgoldfish.com/blogs/article/3/。

本文作者:goldfish@besteam.im,欢迎来我的博客:http://besteam.im/blogs/。版权所有,转载请注明来源。

  1. SSL概述

    安全套接层(SSL)及其新继任者传输层安全(TLS)是在互联网上提供保密安全通道的加密协议,为诸如网站、电子邮件、网上传真等等数据传输进行保密。TLS利用密钥算法在互联网上提供端点身份认证与通讯保密。在典型例子中,只有服务器被可靠身份认证(即其验证被确保),客户端踪迹不一定经可靠认证;相互间的身份认证需要公钥基础设施(PKI)设置于客户端中。协议的设计在某种程度上能够使客户端/服务器应用程序通讯本身预防窃听、干扰(Tampering)、和消息伪造。

    TLS包含三个基本阶段:

    1. 对等协商密钥算法支持
    2. 基于公钥密码的密钥交换和基于证书的身份认证
    3. 基于对称密钥的数据传输保密

    在第一阶段,客户端与服务器协商所用密码算法。 当前广泛实现的算法选择如下:

    • 公钥密码系统:RSA、Diffie-Hellman、DSA及Fortezza;
    • 对称密钥系统:RC2、RC4、IDEA、DES、Triple DES及AES;
    • 单向散列函数: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)中。

  2. 公开密钥加密、证书与CA的概念

    公开密钥加密也称为非对称密钥加密,该加密算法使用两个不同的密钥:公开密钥和私有密钥。这两个密钥是数学相关的,用某用户私钥加密后所得的信息只能用该用户的公钥才能解密,反之亦然。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签名的证书来验证对方的身份。

  3. 实践数字证书生成

    Java 5集成了对SSL的支持,而且提供了一个名为keytool.exe的命令行工具来管理证书与CA签名。keytool.exe位于JRE的bin文件夹下。本文使用%JAVA_HOME%\bin\keytool来指代它。Java 5内置了一些信任的CA证书,它们位于%JAVA_HOME%\lib\security\cacerts(如果安装的是JDK,则对应%JAVA_HOME%\jre\lib\security\cacerts)文件内,可以使用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(支持的格式实际上取决于JDK或者当前已安装的支持软件包)。该文件受密码保护。
      -storepass <storepass> keystore的密码。参见-keypass
      -storetype <storetype> 存储文件的格式,可以是jks或者pkcs12。参见-keystore

    • -certreq

      生成CA签名请求(以下是它的参数)

      参数 参数说明
      -file <csr_file> 签名请求存储文件名
      -alias <alias> 参见-genkey
      -sigalg <sigalg>
      -keypass <keypass>
      -keystore <keystore>
      -storepass <storepass>
      -storetype <storetype>

    • -delete

      从存储文件中删除一个记录(以下是它的参数)

      参数 参数说明
      -alias <alias> 参见-genkey
      -keystore <keystore>
      -storepass <storepass>
      -storetype <storetype>

    • -export

      将密钥导出(以下是它的参数)

      参数 参数说明
      -rfc 以RFC1421标准规定的格式导出密钥
      -alias <alias> 参见-genkey
      -file <cert_file>
      -keystore <keystore>
      -storepass <storepass>
      -storetype <storetype>

    • -import

      将密钥导入存储文件(以下是它的参数)

      参数 参数说明
      -noprompt 不提示是否信任证书
      -trustcacerts 导入证书时考虑其它证书,包括前文所述的JDK内置的包含信任CA的证书存储文件
      -alias <alias> 参见-genkey
      -file <cert_file>
      -keypass <keypass>
      -keystore <keystore>
      -storepass <storepass>
      -storetype <storetype>

    • -list

      列出密钥存储文件中的密钥(以下是它的参数)

      参数 参数说明
      -alias <alias> 参见-genkey
      -rfc
      -keystore <keystore>
      -storepass <storepass>
      -storetype <storetype>

    • -printcert

      打印证书文件信息(以下是它的参数)

      参数 参数说明
      -file 参见-delete

    • -selfcert

      自签名(以下是它的参数)

      参数 参数说明
      -alias <alias> 参见-genkey
      -dname <dname>
      -validity <valDays>
      -keypass <keypass>
      -sigalg <sigalg>
      -keystore <keystore>
      -storepass <storepass>
      -storetype <storetype>

    keytool本身虽然可以管理密钥和证书,也能够完全自签名,可以满足普通的应用,但是如果需要根据PKI规范的要求建立证书链,就需要用到另一个工具OpenSSL。OpenSSL的命令行比较复杂。首先从OpenSSL的官方网站下载OpenSSL的编译版本(大多数Linux一般已经预安装OpenSSL,试着运行输入openssl命令看看)。下载安装后可以看到一个名为openssl.exe的可执行文件,它不需要其它动态链接库,可以将其拷贝到%SystemRoot%,即(C:\Windows\system32\)下。以下是它常用的几条子命令:

    • genrsa

      使用RSA算法生成密钥(以下是它的参数)

      参数 参数说明
      -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 包含私钥

    可以发现,keytool和openssl的命令多而繁。本文提供一个简单的脚本工具,实现了创建CA以及创建由CA签名的证书两个功能。

      1
      2
      3
      4
      5
      6
      7
      8
      9
     10
     11
     12
     13
     14
     15
     16
     17
     18
     19
     20
     21
     22
     23
     24
     25
     26
     27
     28
     29
     30
     31
     32
     33
     34
     35
     36
     37
     38
     39
     40
     41
     42
     43
     44
     45
     46
     47
     48
     49
     50
     51
     52
     53
     54
     55
     56
     57
     58
     59
     60
     61
     62
     63
     64
     65
     66
     67
     68
     69
     70
     71
     72
     73
     74
     75
     76
     77
     78
     79
     80
     81
     82
     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    #! /bin/env python
    # -*- encoding:gbk -*-
    
    """
    用于生成WebService使用的CA及并且签署证书
    """
    import sys, shutil, os, subprocess, getpass
    
    configure={"debug":False}
    
    def quiet_run(cmd, argstr=None):
        nf=file(os.devnull,"rw")
        if configure["debug"]:
            p=subprocess.Popen(cmd, stdin=subprocess.PIPE)
        else:
            p=subprocess.Popen(cmd, stdin=subprocess.PIPE, \
                                         stdout=nf, stderr=nf)
        if argstr is not None:
            p.stdin.write(argstr)
            p.stdin.write("\n")
            p.stdin.flush()
        p.wait()
    
    def get_indentity():
        """取得用户身份信息"""
        print "请输入你的身份信息,这些信息将被附加到证书上,以便于客户确认您的身份"
        print "只输入英文"
        identity={}
        identity["C"]="CN"
        identity["ST"]=raw_input("请输入您的省份:")
        identity["L"]=raw_input("请输入您的城市:")
        identity["O"]=raw_input("请输入您的单位名称:")
        identity["OU"]=raw_input("请输入您的部门名称:")
        identity["CN"]=raw_input("请输入您的名字:")
        identity["EMAILADDRESS"]=raw_input("请输入您的电子邮箱地址:")
    
        #连接成OpenSSL要求的X500格式字符串
        subj="".join([
                "/" + "=".join((key, identity[key])) \
                    for key in identity if len(identity[key])>0
            ])
        print "您的身份认证信息是%s"%subj
        print
        return subj
    
    def create_ca():
        #取得用户身份
        subj=get_indentity()
        #要求输入密码和证书文件名
        password=""
        cafile=""
        while password.strip()=="":
            password=raw_input("请输入保护CA证书的密码(明文显示):")
        print "请记录好该密码,如果丢失该密码,将可能面临安全性" \
               "破坏和重新部署客户端的风险"
        while cafile.strip()=="":
            cafile=raw_input("请输入CA证书的文件名:")
    
        try:
            quiet_run("openssl genrsa -out __zt_cakey.pem 1024")
            quiet_run("openssl req -new -out __zt_careq.csr " \
                    "-key __zt_cakey.pem -subj %s"%subj)
            quiet_run("openssl x509 -req -in __zt_careq.csr " \
                    "-out __zt_cacert.pem -signkey __zt_cakey.pem -days %s"% \
                    configure["days"])
            quiet_run("openssl pkcs12 -export -clcerts -in __zt_cacert.pem " \
                    "-inkey __zt_cakey.pem -out %s -passout stdin"% \
                    (cafile, ), password)
        finally:
            try:
                os.unlink("__zt_cakey.pem")
                os.unlink("__zt_careq.csr")
                os.unlink("__zt_cacert.pem")
            except:
                pass
    
    def create_store():
        print "将为服务器/客户端生成并使用CA证书签署的证书文件"
        cafile=""
        while cafile.strip()=="":
            cafile=raw_input("请输入CA证书的文件名:")
        capassword=""
        while capassword.strip()=="":
            capassword=getpass.getpass("请输入CA证书的密码(不回显):")
        storefile=""
        while storefile.strip()=="":
            storefile=raw_input("请输入新证书的文件名:")
        storepassword=""
        while storepassword.strip()=="":
            storepassword=raw_input("请输入保护新证书的密码(明文显示):")
        storetype=""
        cacertfile=""
        while storetype=="":
            print "Java支持两种格式的证书存储格式,一种是Java环境私有的JKS格式," \
                    "另一种是RFC标准的PKCS#12格式。如果在SUN Java环境下," \
                    "优先使用JKS格式,而其它环境则优先使用PKCS#12格式"
            answer=raw_input("请选择,1-JKS格式,2-PKCS#12: ")
            if answer=="1":
                storetype="JKS"
            if answer=="2":
                storetype="PKCS12"
                print "因为PKCS#12格式的存储格式不能同时包含CA的证书," \
                        "生成客户端密钥之后将同时为您导出CA证书。"
                while cacertfile=="":
                    cacertfile=raw_input("请输入导出CA证书的文件名:")
    
        subj=get_indentity()
    
        try:
            #生成未加密的CA公钥
            quiet_run("openssl pkcs12 -in %(cafile)s -clcerts " \
                    "-nodes -nokeys -out __zt_cacert.pem.1 -passin stdin"% \
                    {"cafile":cafile},capassword)
            #去掉公钥文件的前四行,否则不兼容Java JSSE
            fp=file("__zt_cacert.pem.1")
            for i in range(4):
                fp.readline()
            buf=fp.read()
            fp.close()
            fp=file("__zt_cacert.pem","w")
            fp.write(buf)
            fp.close()
            #生成未加密的CA密钥
            quiet_run("openssl pkcs12 -in %(cafile)s -clcerts " \
                    "-nodes -out __zt_cafile.pem -passin stdin"% \
                    {"cafile":cafile}, capassword)
            quiet_run("openssl rsa -in __zt_cafile.pem -out __zt_cakey.pem")
            #生成新证书
            if storetype=="JKS":
                subj=subj.replace("/",",")[1:]
                quiet_run("keytool -genkey -alias mykey -keyalg rsa " \
                        "-keysize 1024 -validity %(days)s -keypass %(storepassword)s " \
                        "-storepass %(storepassword)s -keystore %(storefile)s " \
                        "-storetype %(storetype)s -dname %(dname)s"% \
                        {"days":configure["days"], \
                        "storepassword":storepassword, \
                        "storefile":storefile, \
                        "storetype":storetype, \
                        "dname":subj}
                        )
                quiet_run("keytool -certreq -alias mykey -sigalg MD5withRSA " \
                        "-file __zt_myreq.csr -keystore %(storefile)s " \
                        "-storepass %(storepassword)s"% \
                        {"storepassword":storepassword,"storefile":storefile})
                quiet_run("openssl x509 -req -in __zt_myreq.csr " \
                        "-out __zt_mycert.pem -CA __zt_cacert.pem " \
                        "-CAkey __zt_cakey.pem -days %(days)s " \
                        "-CAcreateserial -sha1 -trustout -CA __zt_cacert.pem " \
                        "-CAkey __zt_cakey.pem -days %(days)s " \
                        "-CAserial ca-cert.srl -sha1 -trustout"% \
                        {"days":configure["days"]})
                quiet_run("keytool -import -alias __zt_caroot -noprompt " \
                        "-keystore %(storefile)s -storepass %(storepassword)s " \
                        "-file __zt_cacert.pem"% \
                        {"storepassword":storepassword,"storefile":storefile})
                quiet_run("keytool -import -alias mykey -trustcacerts " \
                        "-noprompt -keystore %(storefile)s " \
                        "-storepass %(storepassword)s -file __zt_mycert.pem"% \
                        {"storepassword":storepassword,"storefile":storefile})
            elif storetype=="PKCS12":
                quiet_run("openssl genrsa -out __zt_mykey.pem 1024")
                quiet_run("openssl req -new -out __zt_myreq.csr " \
                        "-key __zt_mykey.pem -subj %s"%subj)
                quiet_run("openssl x509 -req -in __zt_myreq.csr " \
                        "-out __zt_mycert.pem -CAkey __zt_cakey.pem " \
                        "-CA __zt_cacert.pem -days %(days)s " \
                        "-CAcreateserial -CAserial ca-cert.srl -sha1 -trustout"% \
                        {"days":configure["days"]})
                quiet_run("openssl pkcs12 -export -clcerts -in __zt_mycert.pem " \
                        "-inkey __zt_mykey.pem -out %(storefile)s -passout stdin"% \
                        {"storefile":storefile},storepassword)
                os.rename("__zt_cacert.pem",cacertfile)
        except:
            try:
                os.unlink(storefile)
            except:
                pass
        finally:
            try:
                os.unlink("__zt_cacert.pem.1")
                os.unlink("__zt_cacert.pem")
                os.unlink("__zt_cafile.pem")
                os.unlink("__zt_cakey.pem")
                if storetype=="JKS":
                    os.unlink("__zt_myreq.csr")
                    os.unlink("__zt_mycert.pem")
                else:
                    os.unlink("__zt_mykey.pem")
                    os.unlink("__zt_myreq.csr")
                    os.unlink("__zt_mycert.pem")
            except:
                pass
    
    def usage():
        print """使用方法错误
            %(cmdname)s newca:
                创建一个CA证书
            %(cmdname)s newstore:
                创建一个证书"""%{"cmdname":sys.argv[0]}
        sys.exit(1)
    
    def main(argv):
        configure["days"]=365*3
        if len(argv)<2:
            usage()
        if argv[1]=="newca":
            create_ca()
        elif argv[1]=="newstore":
            create_store()
    
    if __name__=="__main__":
        main(sys.argv)
    

    接下去我们将使用这个工具生成CA证书以及Web Service客户端与服务器的证书。

    1. 生成CA证书

        D:\Goldfish\workon\temp>keymgr.py newca
        请输入你的身份信息,这些信息将被附加到证书上,以便于客户确认您的身份
        只输入英文
        请输入您的省份:Fujian
        请输入您的城市:Xiamen
        请输入您的单位名称:fish
        请输入您的部门名称:DD
        请输入您的名字:ca
        请输入您的电子邮箱地址:nobody@nowhere.com
        您的身份认证信息是/C=CN/CN=ca/L=Xiamen/O=fish/
        ST=Fujian/EMAILADDRESS=nobody@nowwhere.com/OU=DD
        请输入保护CA证书的密码(明文显示):123456
        请记录好该密码,如果丢失该密码,将可能面临安全性破坏和重新部署客户端的风险
        请输入CA证书的文件名:fish.pfx
    

    1. 生成客户端证书

        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
    

    1. 生成服务器证书,注意,服务器证书填写的名字要与服务器的域名一样,否则可能被某些客户端自动拒绝

        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。

  4. 配置Tomcat的SSL支持

    使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支持有关,它们的作用描述如下:

    属性(attribute) 属性说明
    scheme 协议。有http和https两个取值。这里指定为https
    secure 是否安全链接。指定为true
    clientAuth 是否认证客户端证书,如果为true会要求客户端提交它的证书,本例中是双向认证,所以设为true
    sslProtocol 指定为TLS
    keystoreFile 存储服务器密钥的存储文件
    keystorePass 存储服务器密钥的存储文件密码
    keystoreType 存储服务器密钥的存储文件的文件格式,可以为jks,也可以为pkcs12
    keyAlias 服务器密钥在存储文件中的名字。使用keymgr.py工具生成的密钥存储文件默认是mykey
    truststoreFile 存储信任的证书文件的存储文件
    truststorePass 存储信任的证书文件的存储文件密码
    truststorePass 存储信任的证书文件的存储文件密码
    truststoreType 存储信任的证书文件的存储文件的文件格式。jkspkcs12之一
    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>Protected Context</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>
    
  5. 为Web Service客户端启用SSL支持

    虽然CXF声称可以通过简单的配置支持SSL客户端,但是根据其用户手册操作时却碰到问题。本文利用CXF提供的辅助类,在客户端向服务器发起SSL连接之前,将客户端证书与CA证书的存储文件配置设置到CXF内部。代码如下:

    //UserServiceFactory.java:
    
    import java.io.IOException;
    import java.io.InputStream;
    import java.net.MalformedURLException;
    import java.net.URL;
    import java.security.KeyStore;
    import java.security.KeyStoreException;
    import java.security.NoSuchAlgorithmException;
    import java.security.UnrecoverableKeyException;
    import java.security.cert.CertificateException;
    import javax.net.ssl.KeyManager;
    import javax.net.ssl.KeyManagerFactory;
    import javax.net.ssl.TrustManager;
    import javax.net.ssl.TrustManagerFactory;
    import javax.xml.namespace.QName;
    
    import org.apache.cxf.configuration.jsse.TLSClientParameters;
    import org.apache.cxf.endpoint.Client;
    import org.apache.cxf.frontend.ClientProxy;
    import org.apache.cxf.transport.http.HTTPConduit;
    import org.apache.cxf.transports.http.configuration.HTTPClientPolicy;
    
    import demo.cxf.User;
    import demo.cxf.UserService;
    import demo.cxf.UserServiceService;
    
    public class UserServiceFactory {
        private final static String keyStore = "client.store";
        private final static String trustStore = "client.store";
        private final static String trustStorePass = "123456";
        private final static String keyStorePass = "123456";
        private final static QName SERVICE = new QName("http://cxf.demo/", 
                "UserServiceService");
        private static UserService us;
        /**
        * 取得信任证书管理器
        * @return
        * @throws IOException
        */
        private static TrustManager[] getTrustManagers() throws IOException {
            try{
                String alg = TrustManagerFactory.getDefaultAlgorithm();
                TrustManagerFactory factory = TrustManagerFactory.getInstance(alg);
                InputStream fp = UserServiceFactory.class.getResourceAsStream(trustStore);
                KeyStore ks = KeyStore.getInstance("JKS");
                ks.load(fp, trustStorePass.toCharArray());
                fp.close();
                factory.init(ks);
                TrustManager[] tms = factory.getTrustManagers();
                return tms;
            }catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            }catch (KeyStoreException e) {
                e.printStackTrace();
            }catch (CertificateException e) {
                e.printStackTrace();
            }
            return null;
        }
    
        /**
        * 取得个人证书管理器
        * @return
        * @throws IOException
        */
        private static KeyManager[] getKeyManagers() throws IOException {
            try{
                String alg = KeyManagerFactory.getDefaultAlgorithm();
                KeyManagerFactory factory = KeyManagerFactory.getInstance(alg);
                InputStream fp =UserServiceFactory.class.getResourceAsStream(keyStore);
                KeyStore ks = KeyStore.getInstance("JKS");
                ks.load(fp, keyStorePass.toCharArray());
                fp.close();
                factory.init(ks, keyStorePass.toCharArray());
                KeyManager[] keyms = factory.getKeyManagers();
                return keyms;
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            } catch (KeyStoreException e) {
                e.printStackTrace();
            } catch (CertificateException e) {
                e.printStackTrace();
            } catch (UnrecoverableKeyException e) {
                e.printStackTrace();
            }
            return null;
        }
    
        static{
            UserServiceService service = null;
            try{
                service = new UserServiceService(
                        new URL("file:D:\\ws\\UserServiceService.wsdl"),
                        SERVICE);
            } catch (MalformedURLException e) {
                e.printStackTrace();
            }
            us = service.getUserServicePort();
    
            Client client = ClientProxy.getClient(us);
            HTTPConduit httpConduit = (HTTPConduit) client.getConduit();
            TLSClientParameters tlsParams = 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);
        }
    
        public static UserService getInstance(){
            return us;
        }
    }
    

    这些代码一目了解,不再进行深入的解释。在实际使用中只需酌情修改存储文件的位置、密码、服务名、与WSDL文件地址及由WSDL文档生成的接口或者类名。作为示例,要把client.store放到${CLASSPATH}下,最简单的就是放到与UserServiceFactory相同的位置。

    使用了上述工厂模式之后,可以写出这样的测试代码:

    UserService us=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异常

  6. 吊销客户端证书

    在生产环境中,不可避免地要碰到与客户中止合作,并取消其访问权限的情况。这通常表现为服务器端显式拒绝客户端的连接请求。基本思路是设置一个包含所有已注销客户的黑名单。但J2EE与CXF并没有提供对证书黑名单的支持。这时,我们需要使用CXF的插件功能,为CXF开发过滤插件。

    在在之前,首先了解一下CXF的系统架构。简单地说,CXF使用流水线型(或者说总线型)处理机制,它的核心是一个Bus。一个客户端的请求或者一个对客户端桩代码的调用被组织成为一个Message。同时,所有的CXF功能都组织成Interceptor挂接在Bus上,分阶段依次处理Message。Message本质上是一个Map数据结构,既包含系统公共的也包含Interceptor自定义的数据。

    要提供证书黑名单功能,首先要取得CXFServlet提供的HttpServletRequest对象,从中取得证书包含的身份信息,对其进行验证。代码如下:

    package demo.cxf;
    
    import java.security.cert.X509Certificate;
    import javax.security.auth.x500.X500Principal;
    import javax.servlet.http.HttpServletRequest;
    
    import org.apache.cxf.bus.CXFBusImpl;
    import org.apache.cxf.interceptor.Fault;
    import org.apache.cxf.message.Message;
    import org.apache.cxf.phase.AbstractPhaseInterceptor;
    import org.apache.cxf.phase.Phase;
    import org.apache.cxf.transport.http.AbstractHTTPDestination;
    
    public class SSLFilter extends AbstractPhaseInterceptor<Message> {
        public SSLFilter() {
            super(Phase.RECEIVE);
        }
        private CXFBusImpl bus;
        public CXFBusImpl getBus() {
            return bus;
        }
        public void setBus(CXFBusImpl bus) {
            this.bus = bus;
        }
        public void handleMessage(Message msg) throws Fault {
            HttpServletRequest request = (HttpServletRequest) 
                    msg.get(AbstractHTTPDestination.HTTP_REQUEST);
            if (request == null)
                return;
            String certSubject = null;
            X509Certificate[] certChain = (X509Certificate[]) 
                    request.getAttribute("javax.servlet.request.X509Certificate");
            if (certChain == null) {
                System.out.println("no javax.servlet.request.X509Certificate instance");
            } else {
                int len = certChain.length;
                if (len > 0) {
                    X509Certificate cert = (X509Certificate) certChain[0];
                    X500Principal pSubject = cert.getSubjectX500Principal();
                    certSubject = pSubject.getName();
                    // 判断客户的名字是否出现在吊销列表中,如果是的话,抛出异常
                    if (certSubject.indexOf("client2") != -1)
                        throw new Fault(new RuntimeException("fish is here!"));
                    } 
                System.out.println(certSubject);
            }
        }
    }
    

    写完代码之后,要将这个Interceptor插接到CXF,方法是修改CXF的配置文件WEB-INF/beans.xml,如下(注意红色修改部分):

    <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-extension-soap.xml" />
        <import resource="classpath:META-INF/cxf/cxf-servlet.xml" />
        <bean id="sslfilter" class="demo.cxf.SSLFilter">
            <property name="bus" ref="cxf" />
        </bean>
        <jaxws:endpoint id="UserService" implementor="demo.cxf.UserServiceImpl"
                address="/UserService">
            <jaxws:inInterceptors>
                <ref bean="sslfilter" />
            </jaxws:inInterceptors>
        </jaxws:endpoint>
    </beans>
    

参考文档:

使用openssl和keytool生成证书:http://debian.kanxue.net/2007/03/19/tomcat-ssl/

此文基于我在公司的部分工作,有版权(大部分在家里写出来的,大方一点,我和公司各一半),但是因为抄了维基百科上的东西,按GPL,本文也是GPL的

附带的Python脚本用于生成CA证书和CA签名后的个人证书,在使用之前要先安装openssl,如果要使用java的jks格式,还要安装jre。要求openssl.exekeytool.exe在$PATH下

标题无“转载”即原创文章,版权所有。转载请注明来源:http://hgoldfish.com/blogs/article/3/。


fxhnkf(Jan. 2, 2012, 8:21 a.m.)

not bad


何不来发表一下您对本文的看法(使用Markdown语法,分段空两行):