安全网络通信(SSL&JSSE)

目录

一、概念介绍

1.SSL简介

2.加密通信

3.安全证书

4.SSL握手

二、keytool工具生成证书

三、JSSE简介

1.KeyStore、KeyManager与TrustManager类

2.SSLContext类

3.SSLServerSocketFactory和SSLSocketFactory类

4.SSLSocket类

4.1.设置加密套件

4.2.处理握手结束事件

4.3.管理SSL会话

4.4.客户模式

5.SSLServerSocket类

三、基于SSL的安全服务器与客户端

1.服务器端实现

2.客户端实现

四、SSLEngine结合NIO实现非阻塞的安全通信

1.SSL握手实现

2.服务器端实现

3.客户端实现

五、加签与验签

1.概念介绍

2.Java提供的加签验签API


在网络上,信息由源主机发送到目标主机的传输过程中会经过一些其他计算机,在数据传输的过程中,可以利用工具,将网络中正在传播的数据截获,进行一些其他操作。比如在使用网银时,网络上的信息可能被非法分子监听,从而导致个人隐私泄露,也可能替换用户发出的原始信息。基于安全考虑,Netscape公司提出了SSL协议,旨在数据信息能在网络上安全保密地传输。

Java安全套接字扩展(Java Secure Socket Extension,JSSE)为基于SSL和TLS协议地Java网络应用程序提供了API及参考实现。使用JSSE,能够保证采用各种应用层协议(比如HTTP、FTP等)地客户程序与服务器程序安全地交换数据。

一、概念介绍

1.SSL简介

SSL(Secure Socket Layer,安全套接字层)是一种保证网络上地两个节点进行安全通信地协议。它是介于传输层和应用层之间的。IEIF(Internet Engineering Task Force)国际组织对SSL作了标准化,指定了RFC2246规范,并将其称为传输层安全(Transport Layer Security, TLS)(升级版SSL)。SSL和TLS都建立在TCP/IP基础之上,采用加密技术来实现安全通信,保证通信数据的保密性和完整性,并且保证通信双方可以验证对方身份,一些应用层协议,都可以采用SSL来保证安全通信,比如建立在SSL协议上地HTTP被称为HTTPS协议。

2.加密通信

加密技术的基本原理是:数据从一端发送到另一端发送到另一端时,发送者先对数据进行加密,然后把密文发送给接收者。这样,在网络上传输的是经过加密的数据,如果有人截获了这批数据,由于没有解密的密钥,就无法获得真正的原始数据。接收者接收到加密数据后,先对数据进行解密,然后处理。

3.安全证书

除了对数据加密通信,SSL还 采用了身份认证机制,确保通信双方可以验证对方的真实身份。就像”电子身份证“一样,不过SSL是通过安全证书来证明客户或服务器的身份。当客户通过安全的连接和服务器通信时,服务器会先向客户端出示它的安全证书,这个证书声明该服务器是安全的,而且的确是这个服务器。每个证书在全世界范围内都是唯一的,其他非法服务器无法假冒原始服务器的身份。

对于单个客户来说,到公认的权威机构去获取安全证书是一件麻烦事。为了扩大客户群并且便于客户的访问,许多服务器不要求客户出示安全证书。在某些情况下,服务器也会要求客户出示安全证书,以便核实该客户的身份,这主要是在B2B(Business to Business)事务中。

获取安全证书有两种方式:

  • 获取权威机构获得证书;
  • 创建自我签名证书。

从权威机构获得证书

安全证书采用加密技术制作而成,他人几乎无法伪造。安全证书由国际权威机构颁发,申请安全证书时,必须支付一定的费用。一个安全证书只对一个IP地址有效,如果系统环境中有多个IP地址,就必须为每个IP地址都购买安全证书。

创建自我签名证书

在某些场合,通信双方只关心在网络上可以被安全传输,并不需要对方进行身份验证,这种情况下,可以使用自我签名证书,JDK提供的keytool工具就可以创建这样的证书。这样的证书就像自己制作名片一样,缺乏权威性,达不到身份认证的目的。(假设你向对方递交名片,名片上写的身份信不信只能由对方自己去判断)。但是无论是从权威机构获得的证书还是自己制作的证书,采用的加密技术都是一样的,使用这些证书,都可以实现安全地加密通信

4.SSL握手

安全证书既包含了用于加密数据的密钥,又包含了用于证实身份的数字签名。安全证书采用公钥加密技术。公钥加密指使用一对非对称的密钥进行加密或解密。每一对密钥由公和私钥组成。公钥被公布,私钥是隐秘的,不公开。用于公钥加密的数据只能被私钥解密。反过来,使用私钥加密的数据只能被公钥解密。这个非对称的特性使得公钥加密很有用。

安全证书中包含了这一对非对称的密钥,只有安全证书的所有者才知道私钥。如下图,当A将自己的安全证书发送给B时,实际上发给B的是公钥,接着B可以向A发送用公钥加密的数据,只有A才能使用私钥对数据解密,从而获得A发送的原始数据。

客户和服务器通信时,首先要进行SSL握手,SSL握手主要完成以下任务:

  • 协商使用的加密套件。加密套件中包含一组加密参数,这些参数指定了加密算法密钥的长度等信息。
  • 验证对方身份。(可选操作)
  • 确定使用的加密算法。

SSL握手的过程采用非对称加密方法传递数据,由此来建立一个安全的会话。SSL握手完成后,通信双方将采用对称加密方法传递实际的应用数据。(对称加密即通信双方采用同样的密钥来加密数据)。

SSL握手的具体流程如下:

  1. 客户将自己的SSL版本号、加密参数、与会话有关的数据以及其他一些必要信息发送到服务器;
  2. 服务器将自己的SSL版本号、加密参数、与会话有关的数据以及其他一些必要信息发送给客户,同时发送给客户的还有服务器的证书。如果服务器需要验证客户身份,那么服务器还会发出要求客户提供安全证书的请求。
  3. 客户端验证服务器证书,如果验证失败,就提示不能建立SSL连接。若成功,则继续下一步;
  4. 客户端为本次会话生成预备主密码(pre-master secret),并将其用服务器公钥加密后发送给服务器;
  5. 如果服务器要求验证客户身份,那么客户端还要再对另外一些数据签名后,将其与客户端证书一起发送给服务器;
  6. 如果服务器要求验证客户身份,则检查签署客户证书的CA(证书机构)是否可信。如果不再信任列表中,则结束本次会话。若检查通过,那么服务器用自己的私钥解密收到的预备主密码,并用它通过某些算法生成本次会话的主密码
  7. 客户端与服务器均使用此主密码生成本次会话的会话密钥(对称密钥)在双方SSL握手结束后传递任何消息均使用此会话密钥。这样做的主要原因是对称加密比非对称加密的运算量低一个数量级以上,能够显著提高双方会话时的运算速度;
  8. 客户端通知服务器此后发送的消息都使用这个会话密钥进行加密,并通知服务器客户端已经完成了本次SSL握手;
  9. 服务器通知客户端此后发送的消息都使用这个会话密钥进行加密,并通知客户端服务器已经完成本次SSL握手;
  10. 本次握手结束,会话已经建立。在接下来的会话过程中,双方使用同一个会话密钥分别对发送以及接收的信息进行加密和解密

二、keytool工具生成证书

获得安全证书有两种方式,一种是到权威机构购买,还有一种是创建自我签名的证书。下面使用JDK内置的证书制作工具keytool创建自我签名的证书。,该工具位置为<JDK 根目录>\bin\keytool.exe、

keytool工具提出了密钥库的概念。密钥库中可以包含多个条目。每个条目包括一个自我签名的安全证书以及一对非对称密钥

基于SSL安全协议的客户端与服务器端通信。首先服务器端需要准备自己的安全证书,在SSL握手时需要将安全证书发送给客户端,客户端可以验证服务器的身份。如下为创建密钥库的命令:

keytool -genkeypair -alias ssl_server -keyalg RSA -keystore ./server_ks.keystore

//以上命令将生成一个密钥库,这个密钥库中有一个名为"ssl_server"的证书条目
-genkeypair:生成一对非对称密钥
-alias:指定条目以及密钥对的别名,该别名是公开的
-keyalg:指定加密算法,上面是RSA算法
-keystore:设定密钥库文件的存放路径以及文件名字

执行上面命令后如下:

这个时候就已经生成了server_ks.keystore这个文件,它是服务端保存证书的仓库。

通过如下命令查看server_ks.keystore密钥库的信息,会列出所包含的证书的信息。

keytool -list -v -keystore ./server_ks.keystore -storepass "password"

通过上述操作就为服务端生成了一个自我签名的数字证书。由于这个不是权威机构颁发的,在SSL握手时,客户端相不相信这个证书那只能由客户端自己判断,所以需要为客户端也生成一个证书仓库,这个仓库用于存放客户端信任的证书。在客户端的程序中将这个信任的证书仓库设置进去就行。

首先为客户端生成一个空白的证书仓库(用于存储客户端信任的证书)

//1.生成客户端信任的证书仓库client_trust_ks.keystore,包含一个名为ssl_client的证书条目
keytool -genkeypair -alias ssl_client -keyalg RSA -keystore ./client_trust_ks.keystore

//执行下面命令将这个ssl_client的证书删除掉,此时客户端信任的证书仓库中就是空白的。
keytool -delete -alias ssl_client -keystore ./client_trust_ks.keystore

接下来我们要将服务器端的证书导出来(做一个副本),并将其导入到客户端信任的证书仓库中。

//将ssl_server这个证书导出到./ssl_server.crt文件中(注意:这个文件只包含密钥对中的公钥,不包含私钥)
keytool -export -alias ssl_server -keystore ./server_ks.keystore -file ./ssl_server.crt -storepass "password"

//执行下面命令进行导入。他会询问你是否信任此证书,输入‘Y’即可(导入的也只包含公钥,不包含私钥)
keytool -import -alias ssl_server -keystore ./client_trust_ks.keystore -file ./ssl_server.crt -storepass "password"

SSL协议通信涉及密钥储存的文件格式比较多,很容易搞混,例如xxx.cer、xxx.pfx、xxx.jks、xxx.keystore、xxx.truststore等格式文件。那么这些究竟分别是什么?

  • .crt.cer格式文件俗称证书,但这个证书中没有私钥,只包含了公钥;
  • .pfx格式文件也称为证书,它一般供浏览器使用,而且它不仅包含了公钥,还包含了私钥,当然这个私钥是加密的,不输入密码是解不了密的;
  • .jks格式文件表示java密钥存储器(javakey store),它可以同时容纳N个公钥跟私钥,是一个密钥库;
  • .keystore格式文件其实跟.jks基本是一样的,只是不同公司叫法不太一样,默认生成的证书存储库格式;
  • .truststore格式文件表示信任证书存储库,它仅仅包含了通信对方的公钥,当然你可以直接把通信对方的jks作为信任库(就算如此你也只能知道通信对方的公钥,要知道密钥都是加密的,你无从获取,只要算法不被破解)。

有些时候我们需要把pfx或cert转化为jks以便于用java进行ssl通信,例如一个银行只提供了pfx证书,而我们想用java进行ssl通信时就要将pfx转化为jks格式。 

三、JSSE简介

JSSE封装了底层复杂的安全通信细节,使得开发人员能方便地用它来开发安全地网络应用程序。JSSE地API允许采用第三方提供地实现,该实现可作为插件集成到JSSE中。这些插件必须支持Oracle指定地加密套件。(加密套件包括一组加密参数,这些参数指定了加密算法地密钥长度等信息。)如:

SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA
采用SSL协议,密钥交换算法为DHE,加密算法为RSA

 JSSE的主要类图如下

1.KeyStore、KeyManager与TrustManager类

在进行安全通信时,要求客户端与服务器端都支持SSL或TCL协议。客户端与服务器端可能都需要设置用于证实自身身份的安全证书,还要设置信任对方的哪些安全证书。更常见的情况是,服务器端只需要设置用于证实自身身份的安全证书,而客户端只需要设置信任服务器的哪些安全证书。

KeyStore类用于存放包含安全证书的密钥库。创建KeyStore对象方式如下:

public static void main(String[] args) throws Exception {
    // 密钥库口令
    String passphrase = "password";
    // 密钥库对象,JKS是Java支持的密钥库类型JavaKeyStore的缩写。
    // 通过KeyStore.getDefaultType()也可以获得平台默认的密钥库类型。
    KeyStore keyStore = KeyStore.getInstance("JKS");
    char[] password = passphrase.toCharArray();
    // 加载密钥库(如果想创建一个空的密钥库,那么下面两个参数可以为null)
    keyStore.load(new FileInputStream("E:\\server_ks.keystore"), password);
    // 加载进来之后就可以操作了
    // 证书条目名称
    String alias = "ssl_server";

    // 如下几个对象中方法可根据API查得
    // 证书对象
    Certificate certificate = keyStore.getCertificate(alias);
    // 获取公钥对象
    PublicKey publicKey = certificate.getPublicKey();
    // 获取私钥对象,这里填私钥密码。(注意:这里需要输入该证书条目中私钥的密码,此处与密钥库密码一致)
    PrivateKey privateKey = ((KeyStore.PrivateKeyEntry) keyStore.getEntry(alias,new KeyStore.PasswordProtection(passphrase.toCharArray()))).getPrivateKey();
    // 将密钥以Base64编码并输出
    String strPrivateKey = Base64.getEncoder().encodeToString(privateKey.getEncoded());
    String strPublicKey = Base64.getEncoder().encodeToString(publicKey.getEncoded());
    System.out.println("私钥:" + strPrivateKey);
    System.out.println("公钥:" + strPublicKey);
}

KeyManager接口的任务是选择用于证实自身身份的安全证书,把他发送给对方。使用KeyManagerFactory创建KeyManager对象。

// 参数为所请求的信任管理算法的标准名称
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
keyManagerFactory.init(keyStore, password);
KeyManager[] keyManagers = keyManagerFactory.getKeyManagers();

TrustManager接口的任务是决定是否信任对方的安全证书(该接口是一个空接口,其子接口为X509TrustManager,该接口只有一个实现类X509ExtendedTrustManager)。TruesManagerFactory负责创建TrustManager对象。

TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("SunX509");
trustManagerFactory.init(keyStore);
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();

2.SSLContext类

SSLContext负责设置与安全通信有关的各种信息,比如使用的协议(SSL或TLS),自身的安全证书以及对方的安全证书。SSLContext还负责构造SSLServerSocketFactorySSLSocketFactorySSLEngine对象。初始化方式如下:

SSLContext sslCtx = SSLContext.getInstance("TLS");
sslCtx.init(keyManagers, trustManagers, null);
ServerSocketFactory ssf = sslCtx.getServerSocketFactory();
/*
    javax.net.ssl.SSLContext.init(KeyManager[] km, TrustManager[] tm, SecureRandom random)
    参数random用于设置安全随机数,如果该参数为null,init()方法就会采用默认的SecureRandom实现。
    参数km和tm也可以为null。
    若km == null,则创建默认的KeyManager对象以及与之关联的keyStore对象,KeyStore对象从系统属性javax.net.ssl.keyStore中获取安全证书,如果不存在这样的属性,则KeyStore对象的内容为空。
    若tm == null,则创建一个默认的TrustManager对象以及与之关联的KeyStore对象,KeyStore对象按照如下步骤获取安全证书:
    1.先尝试从系统属性javax.net.ssl.trustStore中获取安全证书。
    2.如果上一步失败,就尝试把<JDK根目录>/lib/security/jssecacerts文件作为安全证书
    3.如果上一步失败,就尝试把<JDK根目录>/lib/security/cacerts文件作为安全证书
    4.如果上一步失败,则KeyStore对象的内容为空
*/

3.SSLServerSocketFactory和SSLSocketFactory类

SSLServerSocketFactory负责创建SSLServerSocket对象,SSLSocketFactory负责创建SSLSocket对象。SSLServerSocketFactory对象和SSLSocketFactory对象有两种创建方式:

  • 调用SSLContext类的getServerSocketFactory()方法/getSocketFactory()方法。如上。
  • 调用SSLServerSocketFactory类/SSLSocketFactory类的静态getDefault()方法。返回一个默认的SSServerSocketFactory对象,他与一个默认的SSLContext对象关联。getDeafult()通过sslContext.init(null,null,null)方式初始化这个默认的SSLContext对象。

4.SSLSocket类

SSLSocket是Socket的子类,因此两者有很多相似之处。此外SSLSocket还具有与安全通信有关的方法。

4.1.设置加密套件

客户端与服务器在握手阶段需要协商实际使用的加密套件,以下两种情况都会导致握手失败

  • 不存在双方都在使用的相同的加密套件
  • 尽管存在这样的套件,但是还有一方或双方没有使用该加密套件的安全证书。
sslSocket.getSupportedCipherSuites();    //该方法会返回当前sslSocket对象所支持的加密套件组。
sslSocket.setEnabledCipherSuites(suites); //该方法用于设置当前sslSocket对象可使用的加密套件组。
//可使用的加密套件组应该是支持的加密套件组的子集。

4.2.处理握手结束事件

SSL握手需要花费很长时间,当SSL握手完成,会发出一个HandshakeCompletedEvent事件,该事件由HandshakeCompletedListener接口负责监听。SSLSocket类的addHandshakeCompletedListener()方法负责注册HandshakeCompletedListener监听器。如:

//握手结束后执行
sslSocket.addHandshakeCompletedListener(new HandshakeCompletedListener() {
    @Override
    public void handshakeCompleted(HandshakeCompletedEvent event) {
        SSLSession sslSession = event.getSession(); // 获得会话SSLSession
        String suite = event.getCipherSuite(); // 获取实际使用的加密套件
        SSLSocket sSocket = event.getSocket(); // 获取发出该事件的SSLSocket
    }
});

4.3.管理SSL会话

一个客户程序可能会向一个服务器的同一个端口打开多个安全套接字。如果对每一个安全连接都进行SSL握手,就会大大降低通信效率。为了提高安全通信效率,SSL协议允许多个SSLSocket共享同一个SSL会话。在同一个会话中,只有第一个打开的SSLSocket需要进行SSL握手,负责生成密钥以及交换密钥,其余的SSLSocket都共享密钥信息。

在一段合理的时间范围内,如果客户程序像一个服务器的同一个端口打开多个安全套接字,JSSE就会自动重用SSL会话。SSLSession接口表示SSL会话。

SSLSocket的setEnableSessionCreation(boolean flag)方法决定SSLSocket是否允许创建新的会话。默认为true。

  • falg==true,则对于新创建的SSLSocket,如果当前已经有可用的会话,就直接加入该会话,如果没有,则创建一个新的。
  • flag==false,则对于新创建的SSLSocket,如果当前已经有可用的会话,就直接加入该会话,如果没有,则无法与对方进行安全通信

SSLSocket的startHandshake()方法显式地执行一次SSL握手。该方法具有如下用途:

  • 使得会话使用新地密钥;
  • 使得会话使用新地加密套件;
  • 重新开始一个会话。
// 为保证不重用原先地会话,应该先将原先地会话失效
sslSocket.getSession().invalidate();
sslSocket.startHandshake();

4.4.客户模式

由于多数情况下客户端无须向服务器证实自己地身份,因此当一个通信端无须向对方证实自己身份时,就称它处于客户模式,否则称它处于服务器模式。通信双方只能有一方处于服务器模式,另一方则处于客户模式。(无论是服务器还是客户端,都可以处于客户模式或者服务器模式)

SSLSocket地setUserClientMode(boolean mode)方法用来设置客户模式或者服务器模式。

  • mode == true,表示处于客户模式,即无须向对方证实自己的身份。
  • mode == false,表示处于服务器模式,即需要向对方证实自己的身份。

当SSL握手开始,就不允许调用该方法,否则会导致非法参数异常。

当SSLSocket处于服务器模式,还可以通过以下方法来决定是否要求对方提供身份认证:

  • setWantClientAuth(boolean want):当参数为true时,表示希望对方提供身份认证。如果对方未出示,则连接不会中断,通信可继续。
  • setNeedClientAuth(boolean need):当need参数未true时,表示要求对方必须提供身份认证。若对方未出示,则连接中断。

5.SSLServerSocket类

SSLServerSocket是ServerSocket类的子类,除ServerSocket提供的方法外,它还具有与安全通信有关的方法。这些方法与SSLSocket类中的同名方法具有相同的作用。

三、基于SSL的安全服务器与客户端

1.服务器端实现

public class SecureServer {
    private SSLServerSocket sslServerSocket;
    private int port = 54199;

    public SecureServer() {
	// 开启日志可跟踪底层SSL握手流程
        System.setProperty("javax.net.debug", "all");
    }

    public void startUp() throws Exception {
        SSLContext sslContext = createSSLContext();        
        SSLServerSocketFactory ssf = sslContext.getServerSocketFactory();
        sslServerSocket = (SSLServerSocket) ssf.createServerSocket(port);
        System.out.println("服务器已启动");
        // 服务器端默认处于:服务器模式,并且不需要验证对方身份

	// 获取支持的加密套件
        String[] suites = sslServerSocket.getSupportedCipherSuites();
	// 设置可用的加密套件
	sslServerSocket.setEnabledCipherSuites(suites);
	service();
    }

    private SSLContext createSSLContext() throws Exception {
        // 服务器证书仓库
	String keyStoreFileName = "C:\\User\\YHJ\\server_ks.keystore";
	String passphrase = "password";
	char[] password = passphrase.toCharArray();

	KeyStore ks = KeyStore.getInstance("JKS");
	ks.load(new FileInputStream(keyStoreFileName), password);
	KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
	kmf.init(ks, password);

	SSLContext sslContext = SSLContext.getInstance("SSL");
	// 参数2为TrustManager[],此处设置null,就是没有设置服务器信任的证书。
	// 既然服务器没设置,那么客户端也就不用提供安全证书了。
	// 如果需要客户端提供安全证书,服务器端可创建TrustManagerFactory,
	// 并由他创建TrustManager,TrustManager根据与之关联的KeyStore中的信息,决定是否相信客户端提供的安全证书
	sslContext.init(kmf.getKeyManagers(), null, null);

	return sslContext;
    }

    private void service() {
        Socket socket = null;
        DataInputStream dis = null;
        DataOutputStream dos = null;
        try {
	    socket = sslServerSocket.accept();
	    System.out.println("接收到客户端:" + socket.getRemoteSocketAddress() + "的连接");
            dis = new DataInputStream(socket.getInputStream());
	    dos = new DataOutputStream(socket.getOutputStream());
	    String message = null;
	    while (true) {
	        // 读取客户端发送的消息
		message = dis.readUTF();
		if ("bye".equals(message)) {
		    close(socket, dis, dos);
		    return;
		}
		dos.writeUTF("来自服务器的回应:" + message);
	    }

	} catch (IOException e) {
	    e.printStackTrace();
	}
    }
}

2.客户端实现

public class SecureClient {
    private String host = "localhost";
    private int port = 54199;
    private SSLSocket sslSocket;

    public SecureClient() {
        System.setProperty("javax.net.debug", "all");
    }

    public void conntect() throws Exception {
        SSLContext sslContext = createSSLContext();
        SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
	sslSocket = (SSLSocket) sslSocketFactory.createSocket();
	// 获取客户端支持的加密套件
	String[] suites = sslSocket.getSupportedCipherSuites();
	sslSocket.setEnabledCipherSuites(suites);
	// 客户端设置为客户模式(不需要向服务器证实自己的身份)
	sslSocket.setUseClientMode(true);
	// 要求服务器必须提供身份认证
	sslSocket.setNeedClientAuth(true);

	sslSocket.connect(new InetSocketAddress(host, port));
	System.out.println("成功连接服务器");
	talk();
    }

    private void talk() {
        DataInputStream dis = null;
        DataOutputStream dos = null;
        try {
	    dis = new DataInputStream(sslSocket.getInputStream());
	    dos = new DataOutputStream(sslSocket.getOutputStream());
	    dos.writeUTF("你好服务器");
	    System.out.println(dis.readUTF());
	    dos.writeUTF("bye");
	} catch (IOException e) {
	    e.printStackTrace();
	}
    }

    private SSLContext createSSLContext() throws Exception {
        String passphrase = "password";
        char[] password = passphrase.toCharArray();

        String keyStoreFileName = "E:\\client_trust_ks.keystore";
	KeyStore keyStore = KeyStore.getInstance("JKS");
	keyStore.load(new FileInputStream(keyStoreFileName), password);
	TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
	tmf.init(keyStore);

	SSLContext sslContext = SSLContext.getInstance("SSL");
	sslContext.init(null, tmf.getTrustManagers(), null);
	return sslContext;
    }
}

四、SSLEngine结合NIO实现非阻塞的安全通信

由于NIO中没有提供SSLServerSocketChannel和SSLSocketChannel,所以没法像SSLServerSocket一样直接使用,不过提供了SSLEngine可以结合NIO实现非阻塞安全通信。SSLEngine封装了与安全通信有关的细节,它负责了底层ssl协议的握手、加密、解密、关闭会话等等操作,该类内部维护了安全通信的各个状态。由于安全通信过程中收发的数据都是加密后的数据,暂且将加密之后的密文数据称为网络数据,而加密之前的数据就称为应用程序数据。SSLEngine类提供了将应用程序数据加密为网络数据的API(wrap()打包)和将网络数据解密为网络数据的API(unwrap()解包),在执行打包时,该类会自己加入SSL握手数据,在解包时可以除去SSL握手数据。

SSLEngine在握手过程中定义了5种HandshakeStatus状态(维护在SSLEngineResult类中,除FINISHED外其他四种都可以通过SSLEngine对象获取):

  • NEED_UNWRAP:表示等待解包,即等待接收对端的数据。
  • NEED_WRAP:表示等待打包,即将数据发送给对端。
  • NEED_TASK:当解包或者打包完毕之后可能需要处理一些额外的任务,这些任务都是比较耗时或者可能阻塞的,例如访问密钥文件、连接远程证书认证服务、密钥管理器使用何种认证方式作为客户端认证等等操作。该状态是NEED_UNWRAPNEED_WRAP之间中间状态。
  • FINISHED:握手刚刚完成。SSLEngine握手一旦被触发,那么就不要不停的wrap和unwrap。这个时候是不需要应用程序数据的,它会自己交换一些握手需要的数据,比如认证、协商加密套件等等。通过数次wrap和unwrap之后直到状态变为FINISHED。(注意wrap()和unwrap()方法都会返回SSLEngineResult对象,FINISHED状态只能由该类获取)
  • NOT_HANDSHAKING:当前不是握手状态。当握手完成时,会将状态设置为NOT_HANDSHAKING。

注意:握手时是不需要应用程序数据的,就算存储应用程序数据的ByteBuffer是空的也没事。只不过wrap和unwrap方法定义如下:

unwrap(ByteBuffer src, ByteBuffer dst) //dst存储应用程序数据
wrap(ByteBuffer src, ByteBuffer dst)    //src存储应用程序数据

 不能将存储应用程序的ByteBuffer设置为null,但是它却可以是空的没数据。这个对握手没有影响。

除HandshakeStatus之外,在SSLEngineResult类种还维护者另一个枚举类Status,它用于表示wrap和unwrap操作之后的结果状态:

  • BUFFER_OVERFLOW:表示目标缓冲区容量不足无法存放解包之后的数据
  • BUFFER_UNDERFLOW:表示没有足够的数据让SSLEngine来解包
  • CLOSE:SSLEngine被关闭。(sslEngine.closeOutbound())
  • OK:一切正常。

在结合NIO编程时,当服务器接收到一个客户端的连接请求后,就要开始进行握手,这个过程是同步的,所以先不要吧read和write事件也注册到selector上,当完成握手后,才注册这两个事件,并把socket设置成非阻塞。当select到socket可读时先调用unwrap方法,可写时先调用wrap方法。

下面是基于NIO实现安全通信的示例:

不论是客户端还是服务器端都需要进行SSL握手操作,关闭时也需要协商后关闭。所以首先定义一个抽象父类将共同的内容提取出来,如下:

1.SSL握手实现

public abstract class AbstractSSLNIO {
    protected ByteBuffer appInput;
    protected ByteBuffer appOutput;
    protected ByteBuffer netInput;
    protected ByteBuffer netOutput;

    protected abstract void createBuffers(SSLEngine sslEngine);
    protected abstract SSLEngine createSSLEngine(SSLContext sslContext);
    protected abstract SSLContext createSSLContext() throws Exception;

    private void clearBuffers() {
        netInput.clear();
        netOutput.clear();
        appInput.clear();
        appOutput.clear();
    }

    // 处理额外的任务
    protected void doTask(SSLEngine sslEngine) throws Exception {
        if (sslEngine.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_TASK) {
            Runnable task = null;
	    while ((task = sslEngine.getDelegatedTask()) != null) {
	        task.run();
	    }

	    HandshakeStatus hsStatus = sslEngine.getHandshakeStatus();
	    if (hsStatus == HandshakeStatus.NEED_TASK) {
	        throw new Exception("handshake shouldn't need additional tasks");
	    }
	}
    }
    
    // 处理SSL握手
    protected void doHandShake(SocketChannel socketChannel, SSLEngine sslEngine) throws Exception {
        // 每次握手前清空一下缓冲区,防止旧数据影响。
	clearBuffers();
	// 开始进行握手
	sslEngine.beginHandshake();
	SSLEngineResult result = null;
        System.out.println("开始进行握手.........");
	while (true) {
	    // FINISHED只能由SSLEngineResult获得,而不能由sslEngine.getHandshakeStatus获得
	    if (result != null && result.getHandshakeStatus() == HandshakeStatus.FINISHED) {
	        System.out.println(result.getHandshakeStatus());
		break;
	    }
	    HandshakeStatus hsStatus = sslEngine.getHandshakeStatus();
	    System.out.println(sslEngine.getHandshakeStatus());
	    if (hsStatus == HandshakeStatus.NEED_UNWRAP) {
	        result = doUnWrap(sslEngine, socketChannel);
	    } else if (hsStatus == HandshakeStatus.NEED_WRAP) {
	        result = doWrap(sslEngine, socketChannel);
	    }
	}
    }

    // 结束SSL通讯
    protected void doSSLClose(SocketChannel socketChannel, SSLEngine sslEngine) throws Exception {
        if (sslEngine == null || socketChannel == null || socketChannel.socket().isClosed()) {
	    return;
	}
	sslEngine.closeOutbound();
        System.out.println("准备关闭SSLEngine........");
	try {
	    socketChannel.configureBlocking(true);
	} catch (IOException e) {
	    e.printStackTrace();
	}
	while (true) {
	    HandshakeStatus hsStatus = sslEngine.getHandshakeStatus();
            System.out.println(hsStatus);
	    if (hsStatus == HandshakeStatus.NOT_HANDSHAKING) {
		// SSL关闭达成一致之后关闭socketChannel
		socketChannel.close();
                System.out.println("SSL通讯关闭");
		break;
	    } else if (hsStatus == HandshakeStatus.NEED_UNWRAP) {
	        doUnWrap(sslEngine, socketChannel);
	    } else if (hsStatus == HandshakeStatus.NEED_WRAP) {
		doWrap(sslEngine, socketChannel);
	    }
	}
    }
    // 解包
    protected SSLEngineResult doUnWrap(SSLEngine sslEngine, SocketChannel socketChannel) throws Exception {
        appInput.clear();
        socketChannel.read(netInput);
        netInput.flip();
        SSLEngineResult result = null;
        do {
            result = sslEngine.unwrap(netInput, appInput);
	    doTask(sslEngine);
	    // 在握手或者关闭SSL条件下,如果需要解包,则状态为NEED_UNWRAP。
	    // 在服务器与客户端交换应用数据条件下,如果需要解包,则状态为NOT_HANDSHAKING。
	} while ((sslEngine.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NEED_UNWRAP
	        || sslEngine.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING)
	        && netInput.hasRemaining());
	netInput.clear();
    
        return result;
    }

    // 打包
    protected SSLEngineResult doWrap(SSLEngine sslEngine, SocketChannel socketChannel) throws Exception {
        appOutput.flip();
        SSLEngineResult result = sslEngine.wrap(appOutput, netOutput);
        doTask(sslEngine);
        netOutput.flip();
        socketChannel.write(netOutput);
        netOutput.clear();
        appOutput.clear();
        
        return result;
    }
}

2.服务器端实现

public class SSLEngineServer extends AbstractSSLNIO {
    private static final int SERVER_PORT = 54199;
    private static final String passphrase = "password";
    private static final String keyStoreFileName = "E:\\server_ks.keystore";
    private static final Charset charset = Charset.forName("UTF-8");

    private Selector selector;

    private SSLContext sslContext;
    private boolean bufferFinished;
    private boolean isShutdown;

    private void createSocket() throws IOException {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress(SERVER_PORT));
        selector = Selector.open();
        serverSocketChannel.socket().setReuseAddress(true);
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    }
    
    @Override
    protected void createBuffers(SSLEngine sslEngine) {
        SSLSession sslSession = sslEngine.getSession();
        // 获取使用此会话时预期的最大应用程序数据的当前大小。
        int appBufferMax = sslSession.getApplicationBufferSize();
        // 获取使用此会话时预期的最大SSL / TLS数据包的当前大小
        int netBufferMax = sslSession.getPacketBufferSize();
        appInput = ByteBuffer.allocate(appBufferMax + 50);
        appOutput = ByteBuffer.allocate(appBufferMax + 50);
        netInput = ByteBuffer.allocateDirect(netBufferMax);
        netOutput = ByteBuffer.allocateDirect(netBufferMax);

	bufferFinished = true;
    }
    
    @Override
    protected SSLEngine createSSLEngine(SSLContext sslContext) {
        SSLEngine sslEngine = sslContext.createSSLEngine();
	sslEngine.setUseClientMode(false);
        sslEngine.setEnabledCipherSuites(sslEngine.getSupportedCipherSuites());
        return sslEngine;
    }

    @Override
    protected SSLContext createSSLContext() throws Exception {
        KeyStore ks = KeyStore.getInstance("JKS");

        char[] password = passphrase.toCharArray();
	ks.load(new FileInputStream(keyStoreFileName), password);

	KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
	kmf.init(ks, password);

	TrustManager tm = new X509TrustManager() {
            // 对客户端全部信任。该方法用于检查客户端证书,可以在该方法内完成效验的逻辑。若为空方法,表示信任
	    @Override
	    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {}
            @Override
	    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {}
            @Override
	    public X509Certificate[] getAcceptedIssuers() {return null;}
        };

        SSLContext sslContext = SSLContext.getInstance("TLS");
	sslContext.init(kmf.getKeyManagers(), new TrustManager[] { tm }, null);
	return sslContext;
    }

    public void startUp() throws Exception {
        sslContext = createSSLContext();
        createSocket();

        System.out.println("启动服务器");
        while (!isShutdown) {
	    // 该方法会阻塞
            selector.select();
	    
	    SelectionKey selectionKey = null;
	    Set<SelectionKey> selectionKeys = selector.selectedKeys();
	    Iterator<SelectionKey> iterator = selectionKeys.iterator();

	    while (iterator.hasNext()) {
	        selectionKey = iterator.next();
	        iterator.remove();

		if (selectionKey.isAcceptable()) {
		    dealAcceptable(selectionKey);
		} else if (selectionKey.isReadable()) {
		    dealReadable(selectionKey);
		} else if (selectionKey.isWritable()) {
		    dealWritable(selectionKey);
		}
	    }
	}
    }

    private void dealAcceptable(SelectionKey selectionKey) throws Exception {
        // 连接就绪事件
        ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
	// 取出这个连接
	SocketChannel socketChannel = serverSocketChannel.accept();

	// 对于每一个连接都需要创建一个SSLEngine
	SSLEngine sslEngine = createSSLEngine(sslContext);
	if (!bufferFinished) {
	    createBuffers(sslEngine);
	}

	doHandShake(socketChannel, sslEngine);

	// 握手完成之后,将socketChannel设置为非阻塞模式,并注册读就绪事件(将这个sslEngine作为附件对象)
	socketChannel.configureBlocking(false);
	socketChannel.register(selector, SelectionKey.OP_READ, sslEngine);
	System.out.println("握手完成后的状态:" + sslEngine.getHandshakeStatus());
    }

    private void dealWritable(SelectionKey selectionKey) throws Exception {
       doWrap((SSLEngine) selectionKey.attachment(), (SocketChannel) selectionKey.channel());
        // 写完之后注销写事件,否则会空耗CPU
        selectionKey.interestOps(SelectionKey.OP_READ);
    }

    private void dealReadable(SelectionKey selectionKey) throws Exception {
        SSLEngine sslEngine = (SSLEngine) selectionKey.attachment();
        if (sslEngine.getHandshakeStatus() == HandshakeStatus.NOT_HANDSHAKING) {
            SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
	    // 这里是客户端与服务器正常的应用数据通信
	    SSLEngineResult result = doUnWrap(sslEngine, socketChannel);

	    if (result.getStatus() == SSLEngineResult.Status.OK) {
		appInput.flip();
	        String message = charset.decode(appInput).toString();
	        System.out.println("接收到客户端发来的消息:" + message);
	        appOutput.put(("server:" + message).getBytes());
	        appInput.clear();
	        // 注册写就绪事件(通过该方式添加的事件,selectionKey的interest是在每次selector.select()操作的时候
		// 注册到系统进行监听的,故在selector.select()调用之后修改的interest在下一次select()时才会生效
		selectionKey.interestOps(selectionKey.interestOps() | SelectionKey.OP_WRITE);
	    } else if (result.getStatus() == SSLEngineResult.Status.CLOSED) {
		// 必须先取消该selectionKey,否则socketChannel无法转换为阻塞模式。
		// 且取消selectionKey也就不再跟踪该socketChannel上的事件了
		selectionKey.cancel();
		doSSLClose(socketChannel, sslEngine);
	    }
	}
    }
    
    public void shutdown() {
        isShutdown = true;
    }
    
    public static void main(String[] args) throws Exception {
        SSLEngineServer server = new SSLEngineServer();
        server.startUp();
    }
}

3.客户端实现

public class SSLEngineClient extends AbstractSSLNIO {
    private static final String SERVER_IP = "localhost";
    private static final int SERVER_PORT = 54199;
    private static final String passphrase = "password";
    private static final String keyStoreFileName = "E:\\client_trust_ks.keystore";
    private static final Charset charset = Charset.forName("UTF-8");

    private SocketChannel socketChannel;
    private Selector selector;

    private SSLContext sslContext;
    private SSLEngine sslEngine;

    private void service() throws Exception {
        while (selector.select() > 0) {
	    Set<SelectionKey> selectionKeys = selector.selectedKeys();
	    Iterator<SelectionKey> iterator = selectionKeys.iterator();
	    SelectionKey selectionKey = null;
	    while (iterator.hasNext()) {
	        selectionKey = iterator.next();
	        iterator.remove();
		if (selectionKey.isReadable()) {
		    dealReadable();
		}
                //客户端不用注册写就绪事件,因为客户端是一致可以写的,客户端从控制台读取到消息之后直接写入
	    }
	}
    }
    
    private void dealReadable() throws Exception {
        if (sslEngine.getHandshakeStatus() == SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING) {
	    SSLEngineResult result = doUnWrap(sslEngine, socketChannel);
	    if (result.getStatus() == SSLEngineResult.Status.OK) {
	        appInput.flip();
	        String message = charset.decode(appInput).toString();
	        System.out.println("接收到服务器的消息:" + message);
	        appInput.clear();
	    } else if (result.getStatus() == SSLEngineResult.Status.CLOSED) {
	        System.out.println("服务器端关闭SSLEngine");
	        // TODO只有服务器主动宕机时才关闭SSL通讯,否则一般都是客户端主动关闭通讯
	    }

	}
    }
    
    public void receiveFromConsole() throws Exception {
        Scanner scanner = new Scanner(System.in);
        String message = null;
        while (scanner.hasNext()) {
            message = scanner.nextLine();
	    appOutput.put(charset.encode(message + "_end"));
	    if (message.equals("bye")) {
	        // 关闭选择器
		selector.close();
		doSSLClose(socketChannel, sslEngine);
	        break;
	    } else {
		// 打包发送
		doWrap(sslEngine, socketChannel);
	    }
	
	}
	scanner.close();
    }
    
    // 连接服务器
    public void connect() throws Exception {
	sslContext = createSSLContext();
	sslEngine = createSSLEngine(sslContext);
	createBuffers(sslEngine);
        socketChannel = SocketChannel.open();
	selector = Selector.open();
	// 阻塞式连接服务器
	socketChannel.connect(new InetSocketAddress(SERVER_IP, SERVER_PORT));
	System.out.println("连接服务器成功");
	// SSL握手
	doHandShake(socketChannel, sslEngine);
	System.out.println("握手完成后的状态:" + sslEngine.getHandshakeStatus());
	socketChannel.configureBlocking(false);
	socketChannel.register(selector, SelectionKey.OP_READ);
        
        new Thread(new Runnable() {

	    @Override
	    public void run() {
	        try {
		      receiveFromConsole();
		} catch (Exception e) {
		    e.printStackTrace();
		}
	    }
	}).start();

	service();
    }
    
    @Override
    protected void createBuffers(SSLEngine sslEngine) {
       //省略buffers的创建(与服务器的一样)
    }

    @Override
    protected SSLEngine createSSLEngine(SSLContext sslContext) {
       //省略SSLEngine的创建:这里设置为客户端模式,且必须让服务器提供身份认证,并设置加密套件
    }

    @Override
    protected SSLContext createSSLContext() throws Exception {
        //省略SSLContext的创建
    }

    public static void main(String[] args) throws Exception {
        SSLEngineClient client = new SSLEngineClient();
        client.connect();
    }
}

启动服务器,客户端连接服务器之后显式如下,可以看到SSL握手以及SSL关闭过程中状态的变化:

五、加签与验签

1.概念介绍

上面介绍的主要内容是基于SSL的加密通信,加密解密的主要作用是保证数据传输的安全性。通过加密传输数据能够避免被轻易的拦截数据,也可以保证拦截数据的人不能读取数据内容,即防止信息泄露。而加签主要作用的数据防篡改、防否认(抵赖)、防伪造

  • 加密解密:发送方通过公钥加密数据,接收方通过自己的私钥解密数据;
  • 加签验签:发送方通过自己的私钥加密数据形成签名摘要,将原始数据与签名摘要一同发送给接收方;接收方接收到消息之后,拆分出原始数据与签名摘要,接收者使用发送方提供的公钥对原始数据进行运算形成摘要值,比对发送方发送过来的签名摘要是否与自己生成的一致;若一致则验签成功,否则失败(表示数据被篡改过)。

举一个例子说明加签验签的作用:假设现在有一个网站,发送一个HTTP请求,新用户注册在前端页面输入账号密码将请求提交到后台,假如这个请求被中间人拦截,它将拦截到的账号密码改变之后再提交到后台,那么后台以为这是发送者发来的数据,将这个改变了的账号密码保存起来。如果该请求进行加签,然后后台进行验签,那么中间人拦截修改之后,后台验签就会失败。加签验签就是为了防止数据被篡改,我的确定这个数据是你发送过来的。

当然HTTP协议是明文传输的,就算加签验签可以防止信息被篡改,但是避免不了明文传输数据被拦截之后的信息泄露,它直接建立再TCP之上,HTTPS是利用SSL加密通信的,可以防止信息泄露。(HTTPS = HTTP + SSL)

2.Java提供的加签验签API

具体介绍可参考Java官方API文档,如下模拟上述例子使用相关API进行加签验签操作。

首先用非对称密钥进行加签验签:

// 私钥
public static final String privateKeyStr = "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCjvhQeFhN0xMUjOGEh7jdL0zXriqfjSCN86/xZ9dPmpuchv4C7NfAhksHwpI8dThCK4uk0w3cGPh1eC9T+m8wE8/caAaTbX7SzTferYUL8prNdeX7+omc9B/MFHARe1yDVTKYYevgncbSWwxnyOP+s/lFWzwPfWv7Nb/NjTfwyTJzZqTvZrlGKdV76EoV/ENkwaky+UK9k/bDrQVOcOhI65DLMqVxGKzFVm09cwxJIWcwEg0/SlhcZW7Mkx197ZDBUs5b7JOu/ELvW7EgIadk9BW/mlL9dYHrIoDYbvr+5kZBIbkAnRxqrheezf8Zj+FmdcF0KZ4kC9ypaJQj5HIcPAgMBAAECggEACKoqTl1Esk0nUXHOUglVC8IlfE1eDPzoQ9gJBomnqPD0EikcGswD5QLrGFzc5eQXpAHV7WDaj44B59QgdUixRyHR1aJ4otMxsLtTe9ItxUjc/PlM6xSU+fnNtluBL7OanS7tKyZ2sz+lGsVAPj0d6n2auElEHzGlgiGGc57Q1ah6BEVZb9XdfkYbvfIlzK/4O/+NscfScK/7GoBahphaaUbKfjplP0qt2QQkb7aLYr5WNnJdmukFsbeFkX6H0slBQj0w+EZxIpfHhADrt7enwRdfOXfxMbp6mu0V7VAGrZbX/UJt9lIn7Ui680Gh9Bf6Yb1PwMnb2q5xSRk2D9vUAQKBgQD+vHE8z9Uoer9Hva/JHf02u+iokToupVM6r7IPAh+yoAq1Pb9XvnPI8fxS0+oyyKA7NN/2pxxxA+51zQ9ZSQSqMmgzo9swJMWT6z61SRr+BSsLzwH28btQgdVo8PpmUNe80OaerffABoSWXDrtKiqmnuuXQZy8PSoz3eTWBu+ggQKBgQCkjg8fbkq93iKaCXWm6RDou2MaezkIiShDQ0bZypw36xMV18fOE2KUs8mxi+AGrMYxFd6xqC5PXflAOGmuG9paeKDHn/fWsPZB66VTgx54iuOFZUqKSk29g1h387PQlFBMOaO9LIfhVFj+9hTs4auPb1W23aMvpgl+7zjhE7JfjwKBgQCyV8ZD2BIZXA6tMH1wpeFQv7A8zCha3f5uOPHFliwRCAFXMJrU2I8BoYGRj7IIyfN8YCFo80hzS1hY+y8CyJ9OdSoE9XQQub3mOK3XiA4wx/eCNfTfKhCFZdC+vu/gUDsqg2rir1OkCiwrhPjwcDQaIWs5o4MTOxNf6rs6qPJ9gQKBgBFQoWa9REAWT63FsQuPGqhij1sJ/EZZqFLrkVj5aNa5dZimy+9zOrxK7r4v6bLKG1Cu7G/lMnpHeSK4zOX1KFS8vK5Ett9wX3kwQoU3Fs24T5iJv/4tM2DtoTF+Az2/XXOn3QLa3LFpHrQ1UJ5cYKEHJg3H5JPu9LDWmzWTzQEhAoGBAOIWfbJQKO/JT2E4zL/ArW8XfRgRxYx6iZGIbYwndhVzri6qhWF1+0oqPilXoUn6TVd972C45AkDJj//8rRTHxIe116CV13A097Tr+AKdIZHzJFE2y+3UOcsbL+RcYpsovigo+b99IJ2huF6Ta2xwieIKhtHOsWQl5k1h9IaXrsT";
// 公钥
public static final String publicKeyStr = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo74UHhYTdMTFIzhhIe43S9M164qn40gjfOv8WfXT5qbnIb+AuzXwIZLB8KSPHU4QiuLpNMN3Bj4dXgvU/pvMBPP3GgGk21+0s033q2FC/KazXXl+/qJnPQfzBRwEXtcg1UymGHr4J3G0lsMZ8jj/rP5RVs8D31r+zW/zY038Mkyc2ak72a5RinVe+hKFfxDZMGpMvlCvZP2w60FTnDoSOuQyzKlcRisxVZtPXMMSSFnMBINP0pYXGVuzJMdfe2QwVLOW+yTrvxC71uxICGnZPQVv5pS/XWB6yKA2G76/uZGQSG5AJ0caq4Xns3/GY/hZnXBdCmeJAvcqWiUI+RyHDwIDAQAB";

1.生成待签名串

// 生成待签名串
public static String genSignStr(JSONObject jsonObject) {
    StringBuffer signBuffer = new StringBuffer();

    // 按照key做首字母升序排列
    List<String> keys = new ArrayList<String>(jsonObject.keySet());

    Collections.sort(keys, String.CASE_INSENSITIVE_ORDER);
        for (int i = 0; i < keys.size(); i++) {
	    String key = (String) keys.get(i);
	    if ("sign".equals(key)) {
		continue;
	    }
	    String value = jsonObject.getString(key);
	    // 空串不参与签名
	    if (value == null || value.equalsIgnoreCase("null") || value.equals("")) {
	        continue;
	    }
	    signBuffer.append((i == 0 ? "" : "&") + key + "=" + value);
	}

	String signSrc = signBuffer.toString();
        if (signSrc.startsWith("&")) {
	    signSrc = signSrc.replaceFirst("&", "");
	}

	return signSrc;
}

2.加签

/**
 * 加签
 * 
 * @param signSrc       待签名串
 * @param strPrivateKey 私钥
 * @param algorithm     签名算法
 */
public static String sign(String signSrc, String strPrivateKey, String algorithm) throws Exception {
    byte[] keyBytes = Base64.getDecoder().decode(strPrivateKey);
    // PKCS8EncodedKeySpec类代表私钥的ASN.1编码
    KeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
    // 根据指定的签名算法返回一个KeyFactory
    KeyFactory keyFactory = KeyFactory.getInstance("RSA");
    // 从提供的密钥规范(密钥材料)生成私钥对象。
    PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
    // Signature类用于向应用程序提供数字签名算法的功能。 数字签名用于数字数据的认证和完整性保证。
    Signature signature = Signature.getInstance(algorithm);
    // 初始化此对象进行签名。
    signature.initSign(privateKey);
    // 使用指定的字节数组更新要签名的数据。
    signature.update(signSrc.getBytes("UTF-8"));
    // 返回所有更新的数据的签名字节。
    byte[] bytes = signature.sign();
    return Base64.getEncoder().encodeToString(bytes);
}

 3.验签

/**
 * 验签  
 * @param signSrc      待签名串
 * @param signMessage  元数据加签后的字符串
 * @param strPublicKey 公钥
 * @param algorithm    签名算法
 */
public static boolean verify(String signSrc, String signMessage, String strPublicKey, String algorithm) throws Exception {
    byte[] keyBytes = Base64.getDecoder().decode(strPublicKey);
    KeySpec keySpec = new X509EncodedKeySpec(keyBytes);
    KeyFactory keyFactory = KeyFactory.getInstance("RSA");
    PublicKey publicKey = keyFactory.generatePublic(keySpec);
    Signature signature = Signature.getInstance(algorithm);
    // 初始化此对象进行验证。
    signature.initVerify(publicKey);
    signature.update(signSrc.getBytes("UTF-8"));
    // 验证
    return signature.verify(Base64.getDecoder().decode(signMessage));
}

测试:

public class User {
    private String username;
    private String password;
    private String sign;

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }
    //省略getter和setter
}
public static void main(String[] args) throws Exception {
    // 1.生成待签名串signSrc
    User user = new User("yhj123456", "1234567");
    String userJsonStr = JSONObject.toJSONString(user);
    // 此处生成的signSrc == password=12345678&username=yhj123456
    String signSrc = SignUtil.genSignStr(JSONObject.parseObject(userJsonStr));
    
    // 2.加签
    String signMessage = SignUtil.sign(signSrc, SignUtil.privateKeyStr, "SHA256withRSA");
    user.setSign(signMessage);
    
    //-------请求发送至后台--------------

    // 3.后台拆分出待签名串和签名串进行验签
    boolean isOk = SignUtil.verify(signSrc, signMessage, SignUtil.publicKeyStr, "SHA256withRSA");        //true
}

参考:

https://blog.csdn.net/u011179993/article/details/49274423

https://blog.csdn.net/u011077027/article/details/100731436

https://blog.csdn.net/woniu211111/article/details/108114402?utm_source=app&app_version=4.8.0&code=app_1562916241&uLinkId=usr1mkqgl919blen

https://blog.csdn.net/weixin_33971130/article/details/85858134

 

  • 5
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Java网络编程(第三版)中文版(不看后悔) JAVA Network Programming,Third Edition 原出版社: O'Reilly 作者: (美)Elliotte Rusty Harold [作译者介绍] 译者: 朱涛江[同译者作品] 林剑 丛书名: O'Reilly Java系列 出版社:中国电力出版社 编辑推荐 “直到找到这本书之后,我才开始理解Java网络编程。” ——Bruce Eckel,《Thinking in Java》的作者 内容简介回到顶部↑《Java网络编程》第三版会为你介绍Java网络API的最新特性。本书讨论了JDK 1.4和1.5(现在已命名为J2SE 5)中所做的所有修改和增补。本书内容全面,涵盖了从网络基础知识到远程方法调用(RMI)等各方面的内容,书中章节涉及到TCP和UDP socket、服务器socket、URL和URI、组播以及特殊用途的API(如JavaMail)等等。本书展示了如何使用JSSE编写安全的网络应用程序,解释了如何使用NIO API编写超高性能的服务器。它还涵盖了Java对网络代理、Web cookie和URL缓存的支持。 《Java网络编程》不仅仅是对API的解释:它还展示了如何使用API。本书有很多示例,包含了几千行可以实际工作的代码(所有代码都可以在线获得),实现了功能完整的网络客户端和服务器。无论是希望编写特殊用途的web服务器、安全的在线订单接收程序、简单的组播代理还是电子邮件客户端,都会找到可供学习和借用的代码。 本书适合熟悉Java语言的读者的编程人员和计算机专业的学生阅读。 前言 1 第一章 Java网络编程的原因 13 网络程序的功能 14 安全性 27 等等!还有更多! 29 第二章 基本网络概念 30 网络 30 网络的分层 32 IP、TCP和UDP 37 Internet 40 客户/服务器模型 46 Internet标准 47 第三章 基本Web概念 56 URI 56 HTML、SGML和XML 63 HTTP 65 MIME媒体类型 69 服务器端程序 74 第四章 流 78 输出流 79 .输入流 83 过滤器流 87 阅读器和书写器 101 第五章 线程 116 运行线程 118 返回线程中的信息 122 同步 133 死锁 139 线程调度 140 线程池 153 第六章 查找Internet地址 159 InetAddress类 161 Inet4Address和Inet6Address 177 NetworkInterface类 178 一些有用的程序 181 第七章 URL和URI 192 URL类 192 URLEncoder和URLDecoder类 216 URI类 222 代理 230 通过GET方法与服务器端程序通信 233 访问受口令保护的网站 237 第八章 Swing中的HTML 245 组件上的HTML 245 JEditorPane 247 解析HTML 256 cookie 274 第九章 客户端Socket 283 socket基础 283 用Telnet研究协议 284 Socket类 286 Socket异常 312 Socket地址 313 示例 314 第十章 服务器socket 332 ServerSocket类 332 一些有用的服务器 349 第十一章 安全Socket 370 保护通信 371 创建安全客户端Socket 374 SSLSocket类的方法 378 创建安全的服务器Socket 383 SSLServerSocket类的方法 388 第十二章 非阻塞I/O 391 一个示例客户端 392 一个示例服务器 396 缓冲区 402 通道 421 就绪选择 427 第十三章 UDP数据报和Socket 431 UDP协议 431 DatagramPacket类 433 DatagramSocket类 442 一些有用的应用程序 456 DatagramChannel 469 第十四章 组播socket 478 何为组播socket? 479 使用组播socket 487 两个简单示例 495 第十五章 URLConnection 501 打开URLConnection 502 读取服务器的数据 503 读取首部 505 配置连接 514 配置客户端的请求HTTP首部 523 向服务器写入数据 525 内容处理器 530 Object方法 532 URLConnection的安全考虑 533 猜测MIME内容类型 533 HttpURLConnection 537 缓存 552 JarURLConnection 557 第十六章 协议处理器 560 何为协议处理器? 560 URLStreamHandler类 564 编写协议处理器 571 更多协议处理器示例和技术 576 URLStreamHandlerFactory接口 583 第十七章 内容处理器 588 何为内容处理器? 590 ContentHandler类 592 ContentHandlerFactory接口 603 FITS图片格式的内容处理器 606 第十八章 远程方法调用 617 何为远程方法调用? 617 实现 623 在运行时加载类 631 java.rmi包 634 java.rmi.registry包 640 java.rmi.server包 642 第十九章 JavaMail API 648 何为JavaMail API? 649 发送电子邮件 651 接收邮件 661 口令认证 666 地址 670 URLName类 674 Message类 677 Part接口 689 多部分消息和附件 699 MIME消息 703 文件夹 705

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值