文章目录
- 1. 客户端模式
- 2. 创建安全服务器Socket
- 3. 配置SSLServerSocket
1. 客户端模式
大多数安全通信中,服务器需要使用适当的证书认证自己。不过客户端不需要(淘宝需要向用户证明它确实是淘宝,但我不需要向淘宝证明我自己的身份)。但这种不对称的行为可能会导致客户端与服务器之间的通信不安全,可能面临中间人攻击、数据泄露以及安全漏洞的利用风险。为了避免类似的问题,可以要求Socket自行认证,这种策略不适用于向一般公众开放的服务。不过,在某些高安全性的内部应用程序中这是合理的。setUserClientMode()
方法确定Socket是否需要在第一次握手的时候使用认证。参数为true表示Socket处于客户端模式,因此不会进行自行认证。传入false会进行自行认证。
public abstract void setUseClientMode(boolean mode)throws IllegalArgumentException
这个熟悉对于任何一个Socket都只能设置一次,如果再次设置为抛出IllegalArgumentException
,它的相应的get方法可以查看现在这个属性的状态。服务端的安全Socket(即由SSLServerSocket的accept()方法返回的Socket),可以使用setNeedClientAuth()
方法,要求与它连接的所有客户端都要进行自行认证(或不认证)。
2. 创建安全服务器Socket
前面介绍的安全的客户端Socket,这里介绍SSL的服务器Socket,它们是javax.net.SSLServerSocket
类的实例,由抽象工厂javax.net.SSLServerSocketFactory
创建:
public abstract class SSLServerScoektFactory extends ServerSocketFactory
类似于客户端SSLSocketFactory
,SSLServerScoektFactory
的实例也是由getDefault
静态方法返回。它同样也有三个重载的creat方法来获得SSLServerSocket的实例:
public abstract ServerSocket createServerSocket(int port) throws IOException
public abstract ServerSocekt createServerSocket(int port, int queueLength) throws IOException
public abstract ServerSocket createServeerSocket(int port, int queueLength, InetAddress interface) throws IOException
创建安全的服务器Socket时,getDefault返回的工厂只支持服务器认证。它并不支持加密。在Sun的参考实现(不同实现流程不同),要由一个com.sun.net.ssl.SSLContext对象负责创建已经充分配置和初始化的安全服务器Socket,必须完成以下步骤:
- 使用keytool生成公开密钥和证书
- 花钱请可信任的第三方认证你的证书
- 为你使用的算法创建一个SSLContext
- 为你要使用的证书源创建一个TrustManagerFactory
- 为你要使用的密钥类型创建一个KeyManagerFactory
- 为密钥和证书数据库创建一个KeyStore对象(Oracle默认的是JKS)
- 为密钥和证书填充KeyStore对象。例如使用加密所用的口令短语从文件系统加载
- 用KeyStore及其口令短语初始化KeyManagerFactory`
- 用KeyManagerFactory中的密钥管理器(必要)、TrustManagerFactory中的信任管理器和一个随机源来初始化上下文(如果愿意接受默认值,后两个可为null)
下面的例子展示了上面的过程:
public class QuizCardBuilder {
public static void main(String[] args) {
try {
//SSLContext 是一个用于创建和管理 SSL/TLS 安全套接字的类。通过SSLContext,你可以配置SSL/TLS的相关参数,如信任管理器、密钥管理器、安全协议等。
SSLContext context = SSLContext.getInstance("SSL");
//KeyManagerFactory 是一个用于管理密钥管理器的工厂类。它负责加载密钥材料并生成用于创建 KeyManager 的 KeyManagerFactory 对象。
//使用 KeyManagerFactory.getInstance("SunX509") 可以获取支持该算法的 KeyManagerFactory 实例。然后,你可以使用该实例来初始化
// KeyStore 并生成 KeyManager 数组。
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
//KeyStore 是用于存储密钥和证书的类,它提供了一种安全地管理和存储密钥材料的方式。通过 KeyStore,你可以加载、保存和操作密钥和证书。
KeyStore ks = KeyStore.getInstance("JKS");
//出于安全考虑,每个密钥库都必须用口令进行加密,在从磁盘加载前必须提供这个口令,口令短语以char[]数组形式存储,所以可以很快地从内存删除,而不是等待垃圾回收
//用户输入的密码将以字符数组的形式返回,而不是作为字符串,这是为了提高安全性,避免密码在内存中被以明文的形式暴露。
char[] password = new char[]{'2','0','1','2','1','2','6','8','4','6','a'};
//通过调用 load() 方法并提供密钥库文件和密码,可以将密钥库文件的内容加载到 ks 这个 KeyStore 对象中,以便后续的密钥和证书的管理和使用
ks.load(new FileInputStream("/Users/jackchai/Desktop/自学笔记/java项目/leetcode/leetcodetest/out/production/leetcodetest/jnp4e.keys"), password);
//通过调用 init() 方法并提供 KeyStore 对象和密码,可以初始化 KeyManagerFactory 对象,使其准备好生成 KeyManager 数组,用于后续的 SSL/TLS 通信
kmf.init(ks, password);
//通过调用 init() 方法并提供相应的参数,可以初始化 SSLContext 对象,使其准备好进行 SSL/TLS 通信。
//第一个参数是一个 KeyManager 数组,用于提供与客户端身份验证相关的密钥管理器。
//第二个参数是一个 TrustManager 数组,用于提供服务器端证书验证相关的信任管理器。在这里,为了简化,将其设置为 null,表示不进行服务器端证书验证
//第三个参数是一个随机数生成器 SecureRandom 对象,用于产生随机数以供加密操作使用。在这里,将其设置为 null,表示使用默认的随机数生成器
context.init(kmf.getKeyManagers(), null, null);
//擦除口令
Arrays.fill(password, '0');
//由SSLContext对象创建安全的服务器Socket工厂
SSLServerSocketFactory factory = context.getServerSocketFactory();
SSLServerSocket server = (SSLServerSocket) factory.createServerSocket(8080);
//增加匿名(未认证)密码组
//获取服务器支持的协议列表
String[] supported = server.getSupportedProtocols();
String[] anonCipherSuitesSupported = new String[supported.length];
int numanonCipherSuitesSupported = 0;
//遍历支持的协议列表,检查每个协议是否包含"anon"子字符串,如果是,则将该协议添加到匿名加密套件数组中,并将numanonCipherSuitesSupported增加1
for (int i = 0; i < supported.length; i++) {
if (supported[i].indexOf("_anon_") > 0) {
anonCipherSuitesSupported[numanonCipherSuitesSupported++] = supported[i];
}
}
//获取当前启用的加密套件列表,并创建一个新的加密套件列表newEnabled,长度为旧列表长度加上匿名加密套件的数量。
// 然后,使用System.arraycopy()方法将旧的加密套件列表和匿名加密套件列表复制到新的加密套件列表中
String[] oldEnabled = server.getEnabledCipherSuites();
String[] newEnabled = new String[oldEnabled.length + numanonCipherSuitesSupported];
System.arraycopy(oldEnabled, 0, newEnabled, 0, oldEnabled.length);
System.arraycopy(anonCipherSuitesSupported, 0, newEnabled, oldEnabled.length, numanonCipherSuitesSupported);
server.setEnabledCipherSuites(newEnabled);
//匿名加密套件(Anonymous Cipher Suites)是一组加密算法和协议,用于在SSL/TLS通信中实现匿名性。通常,SSL/TLS握手过程中,
// 服务器和客户端都要进行身份验证,以确保通信的安全性和可信性。
// 然而,匿名加密套件提供了一种匿名通信的选项,其中客户端可以保持匿名状态而不进行身份验证
//现在所有的设置工作已经完成
//可以集中进行实际通信了
while (true) {
//这个Socket是安全的
//但从代码中看不出任何现象
try (Socket theconneciotn = server.accept()) {
InputStream in = theconneciotn.getInputStream();
int c;
while ((c = in.read()) != -1) {
System.out.write(c);
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
} catch (KeyStoreException e) {
throw new RuntimeException(e);
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (CertificateException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
} catch (UnrecoverableKeyException | KeyManagementException e) {
throw new RuntimeException(e);
}
}
}
下面使用keytool在控制台生成密钥文件
jnp4e.keys
keytool -genkey -alias ourstore -keystore jnp4e.keys 是一个 keytool 命令,用于生成密钥对并存储到指定的密钥库文件中。逐个参数解释如下:
- keytool: keytool 是 Java 提供的一个用于管理密钥和证书的工具。
- genkey: 该选项指示 keytool 生成一个新的密钥对。
- alias ourstore: -alias 选项指定密钥的别名,这里的别名是 “ourstore”。密钥别名用于在密钥库中唯一标识密钥对。
- keystore jnp4e.keys: -keystore 选项指定密钥库的文件名,这里的文件名是 “jnp4e.keys”。密钥库是用于存储密钥对和证书的二进制文件。
使用上述命令,keytool 会生成一个新的密钥对,并将其存储在名为 “jnp4e.keys” 的密钥库文件中。生成的密钥对将使用指定的别名 “ourstore” 标识。密钥库文件可以用于后续的加密通信、身份验证或数字签名等操作。
完成后我们可以看到我们的密钥文件
3. 配置SSLServerSocket
一旦成功地创建并初始化了一个SSLServerSocke它,只使用java.net.ServerSocket
继承的方法就可以编写很多应用程序。与SSLSocket类似,SSLServerSocket提供了选择密码组、管理会话和确定客户端是否需要自行认证的方法。
- 选择密码组
SSLServerSocket
类也有3个方法可以确定支持和启用哪些密码组:
public abstract String[] getSupportedCipherSuites()
public abstract String[] getEnableCipherSuites()
public abstract void setEnableCipherSuites(String[] suites)
用法和SSLSocket的密码组类似,都是规定了通信时候的加密算法和协议的规则
- 会话管理
客户端和服务器必须都同意建立一个会话。服务端使用setEnableSessionCreation()
方法指定是否允许会话,另外使用getEnableSessionCreation()
方法确定当前是否允许建立会话
public abstract void setEnableSessionCreation(boolean allowSessions)
public abstract boolean getEnableSessionCreation()
默认情况下是允许建立会话的,如果服务器禁止使用会话,需要会话的客户端仍然能够连接。只不过它不会得到一个会话,而必须为每一个Socket再次完成握手。类似的,如果客户端拒绝会话而服务器允许,它们仍然能在没有会话的情况下相互对话。
- 客户端模式
SSLServerSocket
类有两个方法可以确定和指定是否要求客户端向服务器认证自己,通过向setNeedClientAuth()
方法传递true,可以指定只有客户端能够认证自己的连接才会被接受。get方法可以查看这个属性的当前状态。