首页 > 代码库 > 在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