在HTTP协议中,信息是明文传输的,因此为了通信安全就有了HTTPS(Hyper Text Transfer Protocol over Secure Socket Layer)协议。HTTPS也是一种超文本传送协议,在HTTP的基础上加入了SSL/TLS协议,SSL/TLS依靠证书来验证服务端的身份,并为浏览器和服务端之间的通信加密。
12.1 什么是SSL/TLS
12.1.1 SSL/TLS协议的版本演进
12.1.2 SSL/TLS协议的分层结构
12.2 加密算法原理与实战
12.2.1 哈希单向加密算法原理与实战
以下代码使用Java提供的MD5、SHA1、SHA256、SHA512等哈希摘要函数生成哈希摘要(哈希加密结果)并进行验证的案例:
package com.crazymakercircle.secure.crypto;//省略importpublic class HashCrypto{/*** 哈希单向加密测试用例*/public static String encrypt(String plain){StringBuffer md5Str = new StringBuffer(32);try{/*** MD5*///MessageDigest md = MessageDigest.getInstance("MD5");/*** SHA-1*///MessageDigest md = MessageDigest.getInstance("SHA-1");/*** SHA-256*///MessageDigest md = MessageDigest.getInstance("SHA-256");/*** SHA-512*/MessageDigest md = MessageDigest.getInstance("SHA-512");String charset = "UTF-8";byte[] array = md.digest(plain.getBytes(charset));for (int i = 0; i < array.length; i++){//转成十六进制字符串String hexString = Integer.toHexString((0x000000FF & array[i]) | 0xFFFFFF00);log.debug("hexString:{}, 第6位之后: {}",hexString, hexString.substring(6));md5Str.append(hexString.substring(6));}} catch (Exception ex){ex.printStackTrace();}return md5Str.toString();}public static void main(String[] args){//原始的明文字符串,也是需要加密的对象String plain = "123456";//使用哈希函数加密String cryptoMessage = HashCrypto.encrypt(plain);log.info("cryptoMessage:{}", cryptoMessage);//验证String cryptoMessage2 = HashCrypto.encrypt(plain);log.info("验证 {},\n是否一致:{}", cryptoMessage2,cryptoMessage.equals(cryptoMessage2));//验证2String plainOther = "654321";String cryptoMessage3 = HashCrypto.encrypt(plainOther);log.info("验证 {},\n是否一致:{}", cryptoMessage3,cryptoMessage.equals(cryptoMessage3));}}
12.2.2 对称加密算法原理与实战
下面是一段使用Java语言编写的进行DES加密的演示代码:
package com.crazymakercircle.secure.crypto;//省略importpublic class DESCrypto{/*** 对称加密*/public static byte[] encrypt(byte[] data, String password) {try{SecureRandom random = new SecureRandom();//使用密码,创建一个密钥描述符DESKeySpec desKey = new DESKeySpec(password.getBytes());
12.2.3 非对称加密算法原理与实战

下面是一段使用Java代码进行RSA加密的演示代码:
package com.crazymakercircle.secure.crypto;//省略import/*** RSA 非对称加密算法*/@Slf4jpublic class RSAEncrypt{/*** 指定加密算法为RSA*/private static final String ALGORITHM = "RSA";/*** 常量,用来初始化密钥长度*/private static final int KEY_SIZE = 1024;/*** 指定公钥存放文件*/private static final String PUBLIC_KEY_FILE =SystemConfig.getKeystoreDir() + "/PublicKey";/*** 指定私钥存放文件*/private static final String PRIVATE_KEY_FILE =SystemConfig.getKeystoreDir() + "/PrivateKey";/*** 生成密钥对*/protected static void generateKeyPair() throws Exception{/*** 为RSA算法创建一个KeyPairGenerator对象*/KeyPairGenerator keyPairGenerator =KeyPairGenerator.getInstance(ALGORITHM);/*** 利用上面的密钥长度初始化这个KeyPairGenerator对象*/keyPairGenerator.initialize(KEY_SIZE);/** 生成密钥对 */KeyPair keyPair = keyPairGenerator.generateKeyPair();/** 得到公钥 */PublicKey publicKey = keyPair.getPublic();/** 得到私钥 */PrivateKey privateKey = keyPair.getPrivate();ObjectOutputStream oos1 = null;ObjectOutputStream oos2 = null;try{log.info("生成公钥和私钥,并且写入对应的文件");File file = new File(PUBLIC_KEY_FILE);if (file.exists()){log.info("公钥和私钥已经生成,不需要重复生成,path:{}", PUBLIC_KEY_FILE);return;}/** 用对象流将生成的密钥写入文件 */log.info("PUBLIC_KEY_FILE 写入:{}", PUBLIC_KEY_FILE);oos1 = new ObjectOutputStream(new FileOutputStream(PUBLIC_KEY_FILE));log.info("PRIVATE_KEY_FILE 写入:{}", PRIVATE_KEY_FILE);oos2 = new ObjectOutputStream(new FileOutputStream(PRIVATE_KEY_FILE));oos1.writeObject(publicKey);oos2.writeObject(privateKey);} catch (Exception e){throw e;} finally{/** 清空缓存,关闭文件输出流 */IOUtil.closeQuietly(oos1);IOUtil.closeQuietly(oos2);}}/*** 加密方法,使用公钥加密* @param plain 明文数据*/public static String encrypt(String plain) throws Exception{//从文件加载公钥Key publicKey = loadPublicKey();/** 得到Cipher对象,来实现对源数据的RSA加密 */Cipher cipher = Cipher.getInstance(ALGORITHM);cipher.init(Cipher.ENCRYPT_MODE, publicKey);byte[] b = plain.getBytes();/** 执行加密操作 */byte[] b1 = cipher.doFinal(b);BASE64Encoder encoder = new BASE64Encoder();return encoder.encode(b1);}/*** 从文件加载公钥*/public static PublicKey loadPublicKey() throws Exception{PublicKey publicKey=null;ObjectInputStream ois = null;try{log.info("PUBLIC_KEY_FILE 读取:{}", PUBLIC_KEY_FILE);/** 读出文件中的公钥 */ois = new ObjectInputStream(new FileInputStream(PUBLIC_KEY_FILE));publicKey = (PublicKey) ois.readObject();} catch (Exception e){throw e;} finally{IOUtil.closeQuietly(ois);}return publicKey;}//方法:对密文解密,使用私钥解密public static String decrypt(String crypto) throws Exception{PrivateKey privateKey = loadPrivateKey();/** 得到Cipher对象,对已用公钥加密的数据进行RSA解密 */Cipher cipher = Cipher.getInstance(ALGORITHM);cipher.init(Cipher.DECRYPT_MODE, privateKey);BASE64Decoder decoder = new BASE64Decoder();byte[] b1 = decoder.decodeBuffer(crypto);/** 执行解密操作 */byte[] b = cipher.doFinal(b1);return new String(b);}/*** 从文件加载私钥* @throws Exception*/public static PrivateKey loadPrivateKey() throws Exception{PrivateKey privateKey;ObjectInputStream ois = null;try{log.info("PRIVATE_KEY_FILE 读取:{}", PRIVATE_KEY_FILE);/** 读出文件中的私钥 */ois = new ObjectInputStream(new FileInputStream(PRIVATE_KEY_FILE));privateKey = (PrivateKey) ois.readObject();} catch (Exception e){e.printStackTrace();throw e;} finally{IOUtil.closeQuietly(ois);}return privateKey;}public static void main(String[] args) throws Exception{//生成密钥对generateKeyPair();//待加密内容String plain = "疯狂创客圈 Java 高并发研习社群";//公钥加密String dest = encrypt(plain);log.info("{} 使用公钥加密后:\n{}", plain, dest);//私钥解密String decrypted = decrypt(dest);log.info(" 使用私钥解密后:\n{}", decrypted);}}
12.2.4 数字签名原理与实战
下面是一段使用JSHA512withRSA算法实现数字签名的Java演示代码:
package com.crazymakercircle.secure.crypto;//省略import/*** RSA签名演示*/@Slf4jpublic class RSASignDemo{/*** RSA签名** @param data 待签名的字符串* @param priKey RSA私钥字符串* @return 签名结果* @throws Exception 签名失败则抛出异常*/public byte[] rsaSign(byte[] data, PrivateKey priKey)throws SignatureException{try{Signature signature = Signature.getInstance("SHA512withRSA");signature.initSign(priKey);signature.update(data);byte[] signed = signature.sign();return signed;} catch (Exception e){throw new SignatureException("RSAcontent = " + data+ "; charset = ", e);}}/*** RSA验签* @param data 被签名的内容* @param sign 签名后的结果* @param pubKey RSA公钥* @return 验签结果*/public boolean verify(byte[] data, byte[] sign, PublicKey pubKey)throws SignatureException{try{Signature signature = Signature.getInstance("SHA512withRSA");signature.initVerify(pubKey);signature.update(data);return signature.verify(sign);} catch (Exception e){e.printStackTrace();throw new SignatureException("RSA验证签名[content = " + data+"; charset = " + "; signature = " + sign + "]发生异常!", e);}}/*** 私钥*/private PrivateKey privateKey;/*** 公钥*/private PublicKey publicKey;/*** 加密过程* @param publicKey 公钥* @param plainTextData 明文数据* @throws Exception 加密过程中的异常信息*/public byte[] encrypt(PublicKey publicKey, byte[] plainTextData)throws Exception{if (publicKey == null){throw new Exception("加密公钥为空, 请设置");}Cipher cipher = null;try{cipher = Cipher.getInstance("RSA");cipher.init(Cipher.ENCRYPT_MODE, publicKey);byte[] output = cipher.doFinal(plainTextData);return output;} catch (NoSuchAlgorithmException e){throw new Exception("无此加密算法");}…}/*** 解密过程* @param privateKey 私钥* @param cipherData 密文数据* @return 明文* @throws Exception 解密过程中的异常信息*/public byte[] decrypt(PrivateKey privateKey, byte[] cipherData)…{if (privateKey == null){throw new Exception("解密私钥为空, 请设置");}Cipher cipher = null;try{cipher = Cipher.getInstance("RSA");cipher.init(Cipher.DECRYPT_MODE, privateKey);byte[] output = cipher.doFinal(cipherData);return output;} catch (NoSuchAlgorithmException e){throw new Exception("无此解密算法");}…}/*** Main 测试方法* @param args*/public static void main(String[] args) throws Exception{RSASignDemo RSASignDemo = new RSASignDemo();//加载公钥RSASignDemo.publicKey = RSAEncrypt.loadPublicKey();//加载私钥RSASignDemo.privateKey = RSAEncrypt.loadPrivateKey();//测试字符串String sourceText = "疯狂创客圈 Java 高并发社群";try{log.info("加密前的字符串为:{}", sourceText);//公钥加密byte[] cipher = RSASignDemo.encrypt(RSASignDemo.publicKey, sourceText.getBytes());//私钥解密byte[] decryptText = RSASignDemo.decrypt(RSASignDemo.privateKey, cipher);log.info("私钥解密的结果是:{}", new String(decryptText));//字符串生成签名byte[] rsaSign = RSASignDemo.rsaSign(sourceText.getBytes(), RSASignDemo.privateKey);//签名验证Boolean succeed = RSASignDemo.verify(sourceText.getBytes(),rsaSign, RSASignDemo.publicKey);log.info("字符串签名为:\n{}", byteToHex(rsaSign));log.info("签名验证结果是:{}", succeed);String fileName =IOUtil.getResourcePath("/system.properties");byte[] fileBytes = readFileByBytes(fileName);//文件签名验证byte[] fileSign =RSASignDemo.rsaSign(fileBytes, RSASignDemo.privateKey);log.info("文件签名为:\n{}" , byteToHex(fileSign));//文件签名保存String signPath =SystemConfig.getKeystoreDir() + "/fileSign.sign";ByteUtil.saveFile(fileSign,signPath );Boolean verifyOK = RSASignDemo.verify(fileBytes, fileSign, RSASignDemo.publicKey);log.info("文件签名验证结果是:{}", verifyOK);//读取验证文件byte[] read = readFileByBytes(signPath);log.info("读取文件签名:\n{}" , byteToHex(read));verifyOK= RSASignDemo.verify(fileBytes, read, RSASignDemo.publicKey);log.info("读取文件签名验证结果是:{}", verifyOK);} catch (Exception e){System.err.println(e.getMessage());}}}
12.3 SSL/TLS运行过程
SSL/TLS协议运行的基本流程如下:
- 客户端向服务端索要并验证公钥。
- 双方协商生成“对话密钥”。
- 双方采用“对话密钥”进行加密通信。
前两步又称为“握手阶段”,每一个TLS连接都会以握手开始。“握手阶段”涉及四次通信,并且所有通信都是明文的。在握手过程中,客户端和服务端将进行以下四个主要阶段:
- 交换各自支持的加密套件和参数,经过协商后,双方就加密套件和参数达成一致。
- 验证对方(主要指服务端)的证书,或使用其他方式进行服务端身份验证
- 对将用于保护会话的共享主密钥达成一致
-
12.3.1 SSL/TLS第一阶段握手
SSL/TLS“握手”第一个阶段的工作为:由客户端发一个Client Hello报文给服务端,并且第一个阶段只有这一个数据帧(报文)。Client Hello数据帧的内容大致包括以下信息:
客户端支持的SSL/TLS协议版本,比如TLS 1.2版。
- 一个客户端生成的随机数,这是握手过程中的第一个随机数,称之为Random_C
- 客户端支持的签名算法、加密方法、摘要算法(比如RSA公钥签名算法)。
- 客户端支持的压缩方法
12.3.2 SSL/TLS第二阶段握手
SSL/TLS握手第二个阶段的工作为:服务端对客户端的Client Hello请求进行响应。在收到客户端请求(Client Hello)后,服务端向客户端发出回应,这个阶段的服务端回应帧(报文)一般包含4个回复帧:Server Hello帧、Certificate帧、Server Key Exchange帧、Server Hello Done帧。12.3.3 SSL/TLS第三阶段握手
12.3.4 SSL/TLS第四阶段握手
12.4 详解Keytool工具
SSL/TSL在握手过程中,客户端需要服务端提供身份证书(也叫数字证书),有的场景下甚至要求客户端也提供身份证书。安全数字证书主要包含自己的身份信息(如所有人的名称),以及对外的公钥。12.4.1 数字证书与身份识别
12.4.2 存储密钥与证书文件格式
12.4.3 使用Keytool工具管理密钥和证书
12.5 使用Java程序管理密钥与证书
12.5.1 使用Java操作数据证书所涉及的核心类
12.5.2 使用Java程序创建密钥与仓库
```java keytool -genkey -alias server -keypass 123456 -keyalg RSA -keysize 2048 -validity 365 -keystore f:\server.jks -storepass 123456 -dname “CN=server”
这里实现了一个KeyStoreHelper帮助类,用于帮助创建密钥和证书,并且保存到密钥仓库文件,其代码节选如下:```javapackage com.crazymakercircle.keystore;//省略importpublic class KeyStoreHelper{private static final byte[] CRLF = new byte[]{'\r', '\n'};/*** 存储密钥仓库的文件*/private String keyStoreFile;/*** 获取KeyStore信息所需的密码*/private String storePass;/*** 设置指定别名条目的密码,也就是私钥原始密码*/private String keyPass;/*** 每个KeyStore都关联一个独一无二的别名,这个别名通常不区分大小写*/private String alias;/*** 指定证书拥有者信息* 例如:"CN=名字与姓氏,OU=组织单位名称,O=组织名称,L=城市或区域名称,ST=州或省份名称,C=用两字母代表的国家或地区代码"*/private String dname ;KeyStore keyStore;private static String keyType = "JKS";public KeyStoreHelper(String keyStoreFile, String storePass,String keyPass, String alias, String dname){this.keyStoreFile = keyStoreFile;this.storePass = storePass;this.keyPass = keyPass;this.alias = alias;this.dname = dname;}/*** 创建密钥和证书并且保存到密钥仓库文件*/public void createKeyEntry() throws Exception{KeyStore keyStore = loadStore();CertHelper certHelper = new CertHelper(dname);/*** 生成证书*/Certificate cert = certHelper.genCert();cert.verify(certHelper.getKeyPair().getPublic());PrivateKey privateKey = certHelper.getKeyPair().getPrivate();//访问仓库时需要用到仓库密码char[] caPasswordArray = storePass.toCharArray();/*** 设置密钥和证书到密钥仓库*/keyStore.setKeyEntry(alias, privateKey,caPasswordArray, new Certificate[]{cert});FileOutputStream fos = null;try{fos = new java.io.FileOutputStream(keyStoreFile);/*** 密钥仓库保存到文件*/keyStore.store(fos, caPasswordArray);} finally{closeQuietly(fos);}}/*** 从文件加载KeyStore密钥仓库*/public KeyStore loadStore() throws Exception{log.debug("keyStoreFile: {}", keyStoreFile);if (!new File(keyStoreFile).exists()){createEmptyStore();}KeyStore ks = KeyStore.getInstance(keyType);java.io.FileInputStream fis = null;try{fis = new java.io.FileInputStream(keyStoreFile);ks.load(fis, storePass.toCharArray());} finally{closeQuietly(fis);}return ks;}/*** 建立一个空的KeyStore仓库*/private void createEmptyStore() throws Exception{KeyStore keyStore = KeyStore.getInstance(keyType);File parentFile = new File(keyStoreFile).getParentFile();if (!parentFile.exists()){parentFile.mkdirs();}java.io.FileOutputStream fos = null;keyStore.load(null, storePass.toCharArray());try{fos = new java.io.FileOutputStream(keyStoreFile);keyStore.store(fos, storePass.toCharArray());} finally{closeQuietly(fos);}}//…}
使用此KeyStoreHelper类完成创建服务端(如Netty服务器)密钥并且保存到服务端密钥仓库文件,其代码如下:
package com.crazymakercircle.secure.Test.keyStore;//省略importpublic class ServerKeyStoreTester{/*** 存储密钥的文件*/private String keyStoreFile=SystemConfig.getKeystoreDir() + "/server.jks";/*** 访问KeyStore时所需的密码*/private String storePass = "123456";/*** 设置指定别名条目的密码,也就是私钥密码*/private String keyPass = "123456";/*** 每个KeyStore都关联一个独一无二的别名,这个别名通常不区分字母大小写*/private String alias= "server_cert";/*** 指定证书拥有者信息* 例如:"CN=名字与姓氏,OU=组织单位名称,O=组织名称,L=城市或区域名称,ST=州或省份名称,C=用两字母代表的国家或地区代码"*/private String dname = "C=CN,ST=Province,L=city,O=crazymaker,OU=crazymaker.com,CN=server";/*** 创建密钥和证书并且保存到密钥仓库文件*/@Testpublic void testCreateKey() throws Exception{KeyStoreHelper keyStoreHelper = new KeyStoreHelper(keyStoreFile,storePass, keyPass, alias, dname);//创建密钥和证书keyStoreHelper.createKeyEntry();}/*** 在服务端仓库,打印仓库的所有证书*/@Testpublic void testPrintEntries() throws Exception{String dir = SystemConfig.getKeystoreDir();log.debug(" client dir = " + dir);KeyStoreHelper keyStoreHelper = new KeyStoreHelper(keyStoreFile, storePass, keyPass, alias, dname);//打印仓库的所有证书keyStoreHelper.doPrintEntries();}//…}
12.5.3 使用Java程序导出证书文件
keytool -export -alias server -keystore f:/server.jks -storepass 123456 -file server.cer
在帮助类KeyStoreHelper中使用Java代码实现数字证书文件(“.cer”文件)导出的代码,其方法名称为exportCert,代码如下:
package com.crazymakercircle.keystore;//省略importpublic class KeyStoreHelper{//省略成员属性/*** 导出证书* @param outDir 导出的目标目录*/public boolean exportCert(String outDir) throws Exception{assert (StringUtils.isNotEmpty(alias));assert (StringUtils.isNotEmpty(keyPass));KeyStore ks = loadStore();调用此KeyStoreHelper类的exportCert()方法,导出创建服务端(如Netty服务器)密钥的数字证书,其测试用例代码如下:package com.crazymakercircle.secure.Test.keyStore;//省略import@Slf4jpublic class ServerKeyStoreTester{/*** 服务端密钥仓库测试用例*/@Testpublic void testExportCert() throws Exception{String dir = SystemConfig.getKeystoreDir();log.debug("dir = " + dir);KeyStoreHelper keyStoreHelper = new KeyStoreHelper(keyStoreFile,storePass, keyPass, alias, dname);boolean ok = keyStoreHelper.exportCert(dir);log.debug("Export Cert ok = " + ok);}…}
12.5.4 使用Java程序将数字证书导入信任仓库
keytool -import -trustcacerts -alias server -file server.cer -keystore f:/client.jks -storepass 123456
还是在KeyStoreHelper类中使用Java实现导入数字证书到信任仓库的代码,其方法的名称为importCert,代码如下:
package com.crazymakercircle.keystore;//省略importpublic class KeyStoreHelper{//省略成员属性/*** 导入数字证书到信任仓库*/public void importCert(String importAlias, String certPath)…{if (null == keyStore){keyStore = loadStore();}InputStream inStream = null;if (certPath != null){inStream = new FileInputStream(certPath);}//将证书按照别名增加到仓库中boolean succeed = addTrustedCert(importAlias, inStream);if (succeed){log.debug("导入成功");} else{log.error("导入失败");}}/*** 将证书按照别名增加到仓库中*/private boolean addTrustedCert(String alias, InputStream in)throws Exception{if (alias == null){throw new Exception("Must.specify.alias");}//如果别名已经存在,则抛出异常if (keyStore.containsAlias(alias)){throw new Exception("别名已经存在");}//从输入流中读取到证书X509Certificate cert = null;try{cert = (X509Certificate) generateCertificate(in);} catch (ClassCastException | CertificateException ce){throw new Exception("证书读取失败");}//根据别名进行设置keyStore.setCertificateEntry(alias, cert);//写回到仓库文件char[] caPasswordArray = storePass.toCharArray();java.io.FileOutputStream fos = null;try{fos = new java.io.FileOutputStream(keyStoreFile);keyStore.store(fos, caPasswordArray);} finally{closeQuietly(fos);}return true;}…}调用KeyStoreHelper类的importCert()方法把创建服务端的数字证书导入到客户端的密钥仓库。接下来进行一下自测,其测试用例代码如下:package com.crazymakercircle.keystore;//省略import/*** 客户端密钥仓库测试类**/@Slf4j@Datapublic class ClientKeyStoreTester{//省略成员属性/*** 在客户端仓库导入服务器的证书*/@Testpublic void testImportServerCert() throws Exception{String dir = SystemConfig.getKeystoreDir();log.debug(" client dir = " + dir);KeyStoreHelper keyStoreHelper = new KeyStoreHelper(keyStoreFile, storePass, keyPass, alias, dname);/*** 服务器证书的文件*/String importAlias = "server_cert";String certPath = SystemConfig.getKeystoreDir() +"/" + importAlias + ".cer";//导入服务器证书keyStoreHelper.importCert(importAlias, certPath);}/*** 在客户端仓库打印仓库的所有证书*/@Testpublic void testPrintEntries() throws Exception{String dir = SystemConfig.getKeystoreDir();log.debug(" client dir = " + dir);KeyStoreHelper keyStoreHelper = new KeyStoreHelper(keyStoreFile, storePass, keyPass, alias, dname);//打印仓库的所有证书keyStoreHelper.doPrintEntries();}…}
