首页 > 代码库 > 在JEE项目中实施SSL双向认证

在JEE项目中实施SSL双向认证

一、为什么要实施双向认证(Why)

双向认证一般使用在B2B系统或企业内部系统中,目的就是阻止无关人员访问系统,哪怕就一个登录页面也不行。只有系统管理员给你发放了证书,你才能访问到该系统。

二、准备工作(Getting ready)

1. 你系统中要有JDK

2. 你要有一个Servlet容器,这里使用tomcat7

3. 下载BouncyCastle安全工具包,如果你使用Maven管理依赖,请加入下面的依赖项

1
2
3
4
5
6
7
8
9
10
<dependency>                                 
    <groupId>org.bouncycastle</groupId>      
    <artifactId>bcprov-jdk15on</artifactId>  
    <version>1.50</version>
</dependency>                                
<dependency>                                 
    <groupId>org.bouncycastle</groupId>      
    <artifactId>bcpkix-jdk15on</artifactId>  
    <version>1.50</version>
</dependency>

三、实施(How to do it)

1. 生成一个自签名的CA证书

使用JDK自带的keytool工具生成CA证书

1
2
3
keytool -genkeypair -alias "Daojoo.com Co.Ltd. Root CA" -keyalg RSA -keysize 2048 -sigalg SHA1withRSA -dname "CN=Daojoo.com Co.Ltd. Root CA,OU=CA Center,O=Daojoo.com Co.Ltd.,C=CN" -keypass www.daojoo.com -validity 36135 -storetype PKCS12 -keystore "E:\Daojoo.com Co.Ltd. Root CA.pfx" -storepass www.daojoo.com
 
keytool -exportcert -alias "Daojoo.com Co.Ltd. Root CA" -file "E:\Daojoo.com Co.Ltd. Root CA.cer" -storetype PKCS12 -keystore "E:\Daojoo.com Co.Ltd. Root CA.pfx" -storepass www.daojoo.com -rfc

第一条命令生成pfx格式的keystore,包含私钥;第二条命令导出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
// CA 别名
String caAlias = "Daojoo.com Co.Ltd. Root CA";
// CA 证书保护密码
char[] password = "www.daojoo.com".toCharArray();
 
X500NameBuilder x500NameBuilder = new X500NameBuilder(BCStrictStyle.INSTANCE);
x500NameBuilder.addRDN(BCStyle.C, "CN");
x500NameBuilder.addRDN(BCStyle.O, "Daojoo.com Co.Ltd.");
x500NameBuilder.addRDN(BCStyle.OU, "CA Center");
x500NameBuilder.addRDN(BCStyle.CN, "Daojoo.com Co.Ltd. Root CA");
 
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
 
// 证书签发者
X500Name issuer = x500NameBuilder.build();
// 证书序列号
BigInteger serial = BigInteger.valueOf(0);
// 证书开始有效期
Date notBefore = new Date();
// 证书结束有效期,99年
Date notAfter = new Date(notBefore.getTime() + 1000L * 3600 * 24 * 365 * 99);
// 证书使用者
X500Name subject = issuer;
// CA证书公钥
PublicKey caPublicKey = keyPair.getPublic();
// CA证书私钥
PrivateKey caPrivateKey = keyPair.getPrivate();
 
JcaX509v3CertificateBuilder x509v3CertificateBuilder = new JcaX509v3CertificateBuilder(
        issuer,
        serial,
        notBefore,
        notAfter,
        subject,
        caPublicKey
);
 
JcaX509ExtensionUtils x509ExtensionUtils = new JcaX509ExtensionUtils();
// 证书扩展 基本约束 关键 可签发证书
x509v3CertificateBuilder.addExtension(Extension.basicConstraints, true
        , new BasicConstraints(true));
// 证书扩展 签发者密钥标识 非关键
x509v3CertificateBuilder.addExtension(Extension.authorityKeyIdentifier, false
        , x509ExtensionUtils.createAuthorityKeyIdentifier(caPublicKey));
// 证书扩展 使用者密钥标识 非关键
x509v3CertificateBuilder.addExtension(Extension.subjectKeyIdentifier, false
        , x509ExtensionUtils.createSubjectKeyIdentifier(caPublicKey));
// 证书扩展 密钥用法 关键 (用于吊销列表签名、证书签名)
x509v3CertificateBuilder.addExtension(Extension.keyUsage, true, new KeyUsage(
        KeyUsage.cRLSign | KeyUsage.keyCertSign));
 
ContentSigner signer = new JcaContentSignerBuilder("SHA1withRSA").build(caPrivateKey);
X509CertificateHolder x509CertificateHolder = x509v3CertificateBuilder.build(signer);
JcaX509CertificateConverter certificateConverter = new JcaX509CertificateConverter();
Certificate caCertificate = certificateConverter.getCertificate(x509CertificateHolder);
 
// 导出根证书
OutputStream caOs = new FileOutputStream(caAlias + ".cer");
caOs.write(caCertificate.getEncoded());
caOs.close();
 
// 导出为PKCS12
// 将证书、私钥及信任链保存到PKCS12格式的文件中
OutputStream os = new FileOutputStream(caAlias + ".pfx");
KeyStore eeKeyStore = KeyStore.getInstance("PKCS12", "BC");
eeKeyStore.load(null, null);
eeKeyStore.setCertificateEntry(caAlias, caCertificate);
eeKeyStore.setKeyEntry(caAlias, caPrivateKey, password, new Certificate[]{caCertificate});
eeKeyStore.store(os, password);


2. 生成服务器证书

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
// CA 别名
String caAlias = "Daojoo.com Co.Ltd. Root CA";
// CA 证书保护密码
char[] caPassword = "www.daojoo.com".toCharArray();
// 使用者别名
String eeAlias = "*.daojoo.com";
// 使用者密码
char[] eePassword = "daojoo.com".toCharArray();
 
KeyStore caKeyStore = KeyStore.getInstance("PKCS12", "BC");
caKeyStore.load(new FileInputStream(caAlias + ".pfx"), caPassword);
Enumeration<String> aliases = caKeyStore.aliases();
// CA 证书
X509Certificate caCertificate = null;
// CA 私钥
PrivateKey caPrivateKey = null;
while (aliases.hasMoreElements()) {
    String alias = aliases.nextElement();
    if (caKeyStore.isKeyEntry(alias)) {
        caCertificate = (X509Certificate) caKeyStore.getCertificate(alias);
        caPrivateKey = (PrivateKey) caKeyStore.getKey(alias, caPassword);
    }
}
 
X500NameBuilder x500NameBuilder = new X500NameBuilder();
x500NameBuilder.addRDN(BCStyle.C, "CN");
x500NameBuilder.addRDN(BCStyle.O, "Daojoo.com Co.Ltd.");
x500NameBuilder.addRDN(BCStyle.OU, "Web Servers");
x500NameBuilder.addRDN(BCStyle.CN, eeAlias);
 
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
 
// 证书使用者
X500Name subject = x500NameBuilder.build();
// 证书签发者
X500Name issuer = X500Name.getInstance(caCertificate.getIssuerX500Principal().getEncoded());
// 证书开始有效期
Date notBefore = new Date();
// 证书结束有效期  以有效期为1年计算
Date notAfter = new Date(notBefore.getTime() + 1000L * 3600 * 24 * 365);
// 证书序列号
BigInteger serial = BigInteger.valueOf(1);
// 证书公钥
PublicKey eePublicKey = keyPair.getPublic();
// 证书私钥
PrivateKey eePrivateKey = keyPair.getPrivate();
 
X509v3CertificateBuilder x509v3CertificateBuilder = new JcaX509v3CertificateBuilder(
        caCertificate,
        serial,
        notBefore,
        notAfter,
        subject,
        eePublicKey
);
 
JcaX509ExtensionUtils x509ExtensionUtils = new JcaX509ExtensionUtils();
// 证书扩展 基本约束 关键 不签发证书
x509v3CertificateBuilder.addExtension(Extension.basicConstraints, true
        , new BasicConstraints(false));
// 证书扩展 签发者密钥标识 非关键
x509v3CertificateBuilder.addExtension(Extension.authorityKeyIdentifier, false
        , x509ExtensionUtils.createAuthorityKeyIdentifier(caCertificate));
// 证书扩展 使用者密钥标识 非关键
x509v3CertificateBuilder.addExtension(Extension.subjectKeyIdentifier, false
        , x509ExtensionUtils.createSubjectKeyIdentifier(keyPair.getPublic()));
// 证书扩展 密钥用法 关键 (数字签名、数据加密、密钥加密)
x509v3CertificateBuilder.addExtension(Extension.keyUsage, true, new KeyUsage(
        KeyUsage.digitalSignature | KeyUsage.dataEncipherment | KeyUsage.keyEncipherment));
// 证书扩展 增强密钥用法 关键 (客户端认证、服务器认证)
x509v3CertificateBuilder.addExtension(Extension.extendedKeyUsage, true
        , new ExtendedKeyUsage(new KeyPurposeId[]{KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth}));
 
// 为证书签名
ContentSigner signer = new JcaContentSignerBuilder("SHA1withRSA").build(caPrivateKey);
X509CertificateHolder x509CertificateHolder = x509v3CertificateBuilder.build(signer);
JcaX509CertificateConverter x509CertificateConverter = new JcaX509CertificateConverter();
// 得到使用者证书
Certificate endEntityCertificate = x509CertificateConverter.getCertificate(x509CertificateHolder);
 
// 导出根证书
KeyStore endEntityKeyStore = KeyStore.getInstance("JKS");
endEntityKeyStore.load(null, null);
endEntityKeyStore.setCertificateEntry(caAlias, caCertificate);
// 证书链,最终证书在前、CA证书在后
Certificate[] chain = new Certificate[]{endEntityCertificate, caCertificate};
endEntityKeyStore.setKeyEntry(eeAlias, keyPair.getPrivate(), eePassword, chain);
endEntityKeyStore.store(new FileOutputStream(eeAlias.substring(2, eeAlias.length()) + ".jks"), eePassword);


3. 配置tomcat

将生成的服务器证书daojoo.com.jks拷贝到tomcat的conf目录下

在server.xml中添加一个Connector

1
2
3
4
5
6
7
8
9
10
11
12
13
<Connector port="443" protocol="org.apache.coyote.http11.Http11Protocol"
               connectionTimeout="20000"
   SSLEnabled="true"
   scheme="https"
   secure="true"
   clientAuth="true"
   sslProtocol="TLS"
   keystoreFile="conf/daojoo.com.jks"
   keystorePass="daojoo.com"
   truststoreFile="conf/daojoo.com.jks"
   truststorePass="daojoo.com"
       URIEncoding="UTF-8" />

4. 生成客户端证书

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
// CA 别名
String caAlias = "Daojoo.com Co.Ltd. Root CA";
// CA 证书保护密码
char[] caPassword = "www.daojoo.com".toCharArray();
// 使用者别名
String eeAlias = "daojoo";
// 使用者密码
char[] eePassword = "daojoo".toCharArray();
 
KeyStore caKeyStore = KeyStore.getInstance("PKCS12", "BC");
caKeyStore.load(new FileInputStream(caAlias + ".pfx"), caPassword);
Enumeration<String> aliases = caKeyStore.aliases();
// CA 证书
X509Certificate caCertificate = null;
// CA 私钥
PrivateKey caPrivateKey = null;
while (aliases.hasMoreElements()) {
    String alias = aliases.nextElement();
    if (caKeyStore.isKeyEntry(alias)) {
        caCertificate = (X509Certificate) caKeyStore.getCertificate(alias);
        caPrivateKey = (PrivateKey) caKeyStore.getKey(alias, caPassword);
    }
}
 
X500NameBuilder x500NameBuilder = new X500NameBuilder();
x500NameBuilder.addRDN(BCStyle.C, "CN");
x500NameBuilder.addRDN(BCStyle.ST, "Beijing");
x500NameBuilder.addRDN(BCStyle.L, "Beijing");
x500NameBuilder.addRDN(BCStyle.E, "daojoo@daojoo.com");
x500NameBuilder.addRDN(BCStyle.O, "Daojoo.com Co.Ltd.");
x500NameBuilder.addRDN(BCStyle.OU, "Tech");
x500NameBuilder.addRDN(BCStyle.CN, "daojoo");
 
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
 
// 证书使用者
X500Name subject = x500NameBuilder.build();
// 证书签发者
X500Name issuer = X500Name.getInstance(caCertificate.getIssuerX500Principal().getEncoded());
// 证书开始有效期
Date notBefore = new Date();
// 证书结束有效期  以有效期为1年计算
Date notAfter = new Date(notBefore.getTime() + 1000L * 3600 * 24 * 365);
// 证书序列号
BigInteger serial = BigInteger.valueOf(2);
// 证书公钥
PublicKey eePublicKey = keyPair.getPublic();
// 证书私钥
PrivateKey eePrivateKey = keyPair.getPrivate();
 
X509v3CertificateBuilder x509v3CertificateBuilder = new JcaX509v3CertificateBuilder(
        caCertificate,
        serial,
        notBefore,
        notAfter,
        subject,
        eePublicKey
);
 
JcaX509ExtensionUtils x509ExtensionUtils = new JcaX509ExtensionUtils();
// 证书扩展 基本约束 关键 不签发证书
x509v3CertificateBuilder.addExtension(Extension.basicConstraints, true
        , new BasicConstraints(false));
// 证书扩展 签发者密钥标识 非关键
x509v3CertificateBuilder.addExtension(Extension.authorityKeyIdentifier, false
        , x509ExtensionUtils.createAuthorityKeyIdentifier(caCertificate));
// 证书扩展 使用者密钥标识 非关键
x509v3CertificateBuilder.addExtension(Extension.subjectKeyIdentifier, false
        , x509ExtensionUtils.createSubjectKeyIdentifier(keyPair.getPublic()));
// 证书扩展 密钥用法 关键 (数字签名、数据加密、密钥加密)
x509v3CertificateBuilder.addExtension(Extension.keyUsage, true, new KeyUsage(
        KeyUsage.digitalSignature | KeyUsage.dataEncipherment | KeyUsage.keyEncipherment));
// 证书扩展 增强密钥用法 关键 (客户端认证、服务器认证)
x509v3CertificateBuilder.addExtension(Extension.extendedKeyUsage, true
        , new ExtendedKeyUsage(new KeyPurposeId[]{KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth}));
 
// 为证书签名
ContentSigner signer = new JcaContentSignerBuilder("SHA1withRSA").build(caPrivateKey);
X509CertificateHolder x509CertificateHolder = x509v3CertificateBuilder.build(signer);
JcaX509CertificateConverter x509CertificateConverter = new JcaX509CertificateConverter();
// 得到使用者证书
Certificate endEntityCertificate = x509CertificateConverter.getCertificate(x509CertificateHolder);
 
// 导出根证书
KeyStore endEntityKeyStore = KeyStore.getInstance("PKCS12", "BC");
endEntityKeyStore.load(null, null);
endEntityKeyStore.setCertificateEntry(caAlias, caCertificate);
// 证书链,最终证书在前、CA证书在后
Certificate[] chain = new Certificate[]{endEntityCertificate, caCertificate};
endEntityKeyStore.setKeyEntry(eeAlias, keyPair.getPrivate(), eePassword, chain);
endEntityKeyStore.store(new FileOutputStream(eeAlias + ".pfx"), eePassword);

四、如何工作(How it works)

1. 添加BCProvider

1
2
3
static {
    Security.addProvider(new BouncyCastleProvider());
}

2. 解除BCProvider密钥长度限制

下载对应版本jdk的jce不限密钥长度策略文件

Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files 7

Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files 6 

解压后替换掉以下目录中的同名文件

$JRE_HOME/lib/security

$JAVA_HOME/jre/lib/security

3. 同一CA颁发的证书序列号一定不能相同。如果在项目中,证书序列号没有业务含义,则可使用uuid,如:

1
2
// 证书序列号
BigInteger serial = new BigInteger(UUID.randomUUID().toString().replaceAll("-", ""), 16);

4. 当使用X500NameBuilder构建DN时,如果想控制生成的证书中的DN字段顺序,注意最终生成的DN与构建顺序相反

构建时DN字段顺序如下:

1
2
3
4
5
6
7
8
X500NameBuilder x500NameBuilder = new X500NameBuilder();
x500NameBuilder.addRDN(BCStyle.C, "CN");
x500NameBuilder.addRDN(BCStyle.ST, "Beijing");
x500NameBuilder.addRDN(BCStyle.L, "Beijing");
x500NameBuilder.addRDN(BCStyle.E, "daojoo@daojoo.com");
x500NameBuilder.addRDN(BCStyle.O, "Daojoo.com Co.Ltd.");
x500NameBuilder.addRDN(BCStyle.OU, "Tech");
x500NameBuilder.addRDN(BCStyle.CN, "daojoo");

证书中展现的DN字段顺序如下:

1
2
3
4
5
6
7
CN = daojoo
OU = Tech
O = Daojoo.com Co.Ltd.
E = daojoo@daojoo.com
L = Beijing
S = Beijing
C = CN

5. 使用keytool生成证书时,我没找到控制证书扩展项的参数。

五、参考资料(Reference)

?1. RFC2459

2. 公钥基础设施(PKI)实现和管理电子安全 清华大学出版社

3. The Cryptoworkshop Guide to Java Cryptography and the Bouncy Castle APIs 



来自为知笔记(Wiz)