Maison  >  Article  >  Java  >  Exemple de partage de code des principes de communication SSL sous Java

Exemple de partage de code des principes de communication SSL sous Java

黄舟
黄舟original
2017-03-25 10:51:001922parcourir

Il existe déjà de nombreux principes et introductions sur SSL sur Internet. Il existe également de nombreux tutoriels sur l'utilisation de keytool pour générer des certificats sous Java et configurer la communication SSL. Mais si nous ne pouvons pas créer nous-mêmes un serveur SSL et un client SSL, nous ne pourrons peut-être jamais comprendre en profondeur comment la communication SSL est implémentée dans l'environnement Java. La connaissance de divers concepts SSL peut également être limitée dans la mesure où ils peuvent être utilisés. Cet article explique le principe de communication de SSL dans l'environnement Java en construisant un simple serveur SSL et un client SSL.

Tout d’abord, passons en revue la programmation Java Socket standard. Il est relativement simple d'écrire un exemple de serveur et de client Socket en Java.

Le serveur est très simple : écoutez le port 8080 et retournez la string envoyée par le client. Voici le code du serveur :

package org.bluedash.tryssl;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
public class Server extends Thread {
    private Socket socket;
    public Server(Socket socket) {
        this.socket = socket;
    }
    public void run() {
        try {
            BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            PrintWriter writer = new PrintWriter(socket.getOutputStream());
            String data = reader.readLine();
            writer.println(data);
            writer.close();
            socket.close();
        } catch (IOException e) {

        }
    }
    public static void main(String[] args) throws Exception {
        while (true) {
            new Server((new ServerSocket(8080)).accept()).start();
        }
    }
}

Le client est également très simple : lancez une requête au serveur, envoyez une chaîne "bonjour", puis obtenez la réponse du serveur. Voici le code client :

package org.bluedash.tryssl;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class Client {
    public static void main(String[] args) throws Exception {
        Socket s = new Socket("localhost", 8080);
        PrintWriter writer = new PrintWriter(s.getOutputStream());
        BufferedReader reader = new BufferedReader(new InputStreamReader(s.getInputStream()));
        writer.println("hello");
        writer.flush();
        System.out.println(reader.readLine());
        s.close();
    }
}

Après avoir exécuté le serveur et exécuté le client, nous obtiendrons un retour "bonjour".

Il s'agit d'un ensemble si simple de code de communication réseau. Transformons-le pour utiliser la communication SSL. Dans le protocole de communication SSL, nous savons tous que le serveur doit d'abord avoir un certificat numérique. Lorsque le client se connecte au serveur, il obtiendra ce certificat. Si tel est le cas, le cryptage du canal d'échange. clé pour communiquer. Si le certificat n'est pas approuvé, la connexion échoue.

Par conséquent, nous devons d’abord générer un certificat numérique pour le serveur. Dans l'environnement Java, les certificats numériques sont générés à l'aide de keytool, et ces certificats sont stockés dans le concept de magasin, qui est l'entrepôt de certificats. Appelons la commande keytool pour générer un certificat numérique pour le serveur et enregistrons l'entrepôt de certificats qu'il utilise :

keytool -genkey -v -alias bluedash-ssl-demo-server -keyalg RSA -keystore ./server_ks 
-dname "CN=localhost,OU=cn,O=cn,L=cn,ST=cn,C=cn" -storepass server -keypass 123123

De cette façon, nous enregistrons le certificat du serveur bluedash-ssl-demo-server dans le magasin server_ksy fichier parmi. L'utilisation de keytool ne sera pas décrite en détail dans cet article. L'exécution de la commande ci-dessus obtiendra les résultats suivants :

Generating 1,024 bit RSA key pair and self-signed certificate (SHA1withRSA) with a validity of 90 days
        for: CN=localhost, OU=cn, O=cn, L=cn, ST=cn, C=cn
[Storing ./server_ks]

Ensuite, transformez notre code serveur pour permettre au serveur d'utiliser ce certificat et fournir une communication SSL :

package org.bluedash.tryssl;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.security.KeyStore;

import javax.net.ServerSocketFactory;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLServerSocket;

public class SSLServer extends Thread {
    private Socket socket;

    public SSLServer(Socket socket) {
        this.socket = socket;
    }

    public void run() {
        try {
            BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            PrintWriter writer = new PrintWriter(socket.getOutputStream());

            String data = reader.readLine();
            writer.println(data);
            writer.close();
            socket.close();
        } catch (IOException e) {

        }
    }

    private static String SERVER_KEY_STORE = "/Users/liweinan/projs/ssl/src/main/resources/META-INF/server_ks";
    private static String SERVER_KEY_STORE_PASSWORD = "123123";

    public static void main(String[] args) throws Exception {
        System.setProperty("javax.net.ssl.trustStore", SERVER_KEY_STORE);
        SSLContext context = SSLContext.getInstance("TLS");

        KeyStore ks = KeyStore.getInstance("jceks");
        ks.load(new FileInputStream(SERVER_KEY_STORE), null);
        KeyManagerFactory kf = KeyManagerFactory.getInstance("SunX509");
        kf.init(ks, SERVER_KEY_STORE_PASSWORD.toCharArray());
        context.init(kf.getKeyManagers(), null, null);
        ServerSocketFactory factory = context.getServerSocketFactory();
        ServerSocket _socket = factory.createServerSocket(8443);
        ((SSLServerSocket) _socket).setNeedClientAuth(false);

        while (true) {
            new SSLServer(_socket.accept()).start();
        }
    }
}

comme vous pouvez le voir , le travail de préparation et de configuration du socket côté serveur a été considérablement augmenté. La fonction du code ajouté est principalement d'importer et d'utiliser le certificat. De plus, le Socket utilisé est devenu SSLServerSocket, et le port a été changé en 8443 (ce n'est pas obligatoire, juste pour respecter les usages). De plus, le point le plus important est que le CN dans le certificat du serveur doit être cohérent avec le nom de domaine du serveur. Le nom de domaine de notre service de certificat est localhost, notre client doit donc également utiliser localhost lors de la connexion au serveur, sinon. il sera connecté selon SSL Si le standard du protocole et le nom de domaine ne correspondent pas au CN du certificat, cela signifie que le certificat n'est pas sécurisé et la communication ne fonctionnera pas correctement.

Avec le serveur, notre client d'origine ne peut pas être utilisé, et le protocole SSL doit être utilisé. Étant donné que le certificat du serveur est généré par nous-mêmes et n'est signé par aucune autorité de confiance, le client ne peut pas vérifier la validité du certificat du serveur et la communication échouera inévitablement. Nous devons donc créer un entrepôt pour que le client stocke tous les certificats de confiance, puis importer le certificat du serveur dans cet entrepôt. De cette façon, lorsque le client se connecte au serveur, il constatera que le certificat du serveur est dans sa liste de confiance et il pourra communiquer normalement.

Donc, ce que nous devons faire maintenant, c'est générer un entrepôt de certificats client. Parce que keytool ne peut pas générer simplement un entrepôt vide, donc tout comme le serveur, nous générons toujours un certificat plus un entrepôt (certificat client plus entrepôt) :

keytool -genkey -v -alias bluedash-ssl-demo-client -keyalg RSA -keystore ./client_ks 
-dname "CN=localhost,OU=cn,O=cn,L=cn,ST=cn,C=cn" -storepass client -keypass 456456

Les résultats sont les suivants :

Generating 1,024 bit RSA key pair and self-signed certificate (SHA1withRSA) with a validity of 90 days
        for: CN=localhost, OU=cn, O=cn, L=cn, ST=cn, C=cn
[Storing ./client_ks]

Ensuite, nous devons exporter le certificat du serveur et l'importer dans l'entrepôt du client. La première étape consiste à exporter le certificat du serveur :

keytool -export -alias bluedash-ssl-demo-server -keystore ./server_ks -file server_key.cer

Le résultat de l'exécution est le suivant :

Enter keystore password:  server
Certificate stored in file <server_key.cer>

Importez ensuite le certificat exporté dans l'entrepôt de certificats client :

keytool -import -trustcacerts -alias bluedash-ssl-demo-server -file ./server_key.cer -keystore ./client_ks

Les résultats sont les suivants :

Enter keystore password:  client
Owner: CN=localhost, OU=cn, O=cn, L=cn, ST=cn, C=cn
Issuer: CN=localhost, OU=cn, O=cn, L=cn, ST=cn, C=cn
Serial number: 4c57c7de
Valid from: Tue Aug 03 15:40:14 CST 2010 until: Mon Nov 01 15:40:14 CST 2010
Certificate fingerprints:
         MD5:  FC:D4:8B:36:3F:1B:30:EA:6D:63:55:4F:C7:68:3B:0C
         SHA1: E1:54:2F:7C:1A:50:F5:74:AA:63:1E:F9:CC:B1:1C:73:AA:34:8A:C4
         Signature algorithm name: SHA1withRSA
         Version: 3
Trust this certificate? [no]:  yes
Certificate was added to keystore

Bon, les préparatifs sont terminés, écrivons le code client :

package org.bluedash.tryssl;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;

public class SSLClient {

    private static String CLIENT_KEY_STORE = "/Users/liweinan/projs/ssl/src/main/resources/META-INF/client_ks";

    public static void main(String[] args) throws Exception {
        // Set the key store to use for validating the server cert.
        System.setProperty("javax.net.ssl.trustStore", CLIENT_KEY_STORE);

        System.setProperty("javax.net.debug", "ssl,handshake");

        SSLClient client = new SSLClient();
        Socket s = client.clientWithoutCert();

        PrintWriter writer = new PrintWriter(s.getOutputStream());
        BufferedReader reader = new BufferedReader(new InputStreamReader(s
                .getInputStream()));
        writer.println("hello");
        writer.flush();
        System.out.println(reader.readLine());
        s.close();
    }

    private Socket clientWithoutCert() throws Exception {
        SocketFactory sf = SSLSocketFactory.getDefault();
        Socket s = sf.createSocket("localhost", 8443);
        return s;
    }
}

Comme vous pouvez le voir, en plus de transformer certaines classes en communication SSL En plus de la classe, le client dispose également d'un code supplémentaire pour utiliser le magasin de certificats de confiance. Ci-dessus, nous avons terminé la communication SSL unidirectionnelle. Autrement dit : le client vérifie le certificat du serveur et le serveur ne vérifie pas le certificat du client.
Ce qui précède représente l'ensemble du processus de prise de contact SSL unidirectionnelle dans un environnement Java. Parce que nous définissons le niveau de sortie des journaux sur le client sur DEBUG :

System.setProperty("javax.net.debug", "ssl,handshake");

, nous pouvons voir l'ensemble du processus de communication SSL. Ces journaux peuvent nous aider à comprendre plus spécifiquement lors de l'établissement d'une connexion réseau via le protocole SSL. .L'ensemble du processus.
En combinaison avec le journal, examinons l'ensemble du processus d'authentification bidirectionnelle SSL :

Étape 1 : Le client envoie un message ClientHello, initie une demande de connexion SSL et indique le serveur les options SSL (cryptage) qu'il supporte, etc.).

*** ClientHello, TLSv1

Étape 2 : Le serveur répond à la requête, répond au message ServerHello et confirme la méthode de cryptage SSL avec le client :

*** ServerHello, TLSv1

Étape 3 : Le serveur publie sa propre clé publique client.

第四步: 客户端与服务端的协通沟通完毕,服务端发送ServerHelloDone消息:

*** ServerHelloDone

第五步: 客户端使用服务端给予的公钥,创建会话用密钥(SSL证书认证完成后,为了提高性能,所有的信息交互就可能会使用对称加密算法),并通过ClientKeyExchange消息发给服务器:

*** ClientKeyExchange, RSA PreMasterSecret, TLSv1

第六步: 客户端通知服务器改变加密算法,通过ChangeCipherSpec消息发给服务端:

main, WRITE: TLSv1 Change Cipher Spec, length = 1

第七步: 客户端发送Finished消息,告知服务器请检查加密算法的变更请求:

*** Finished

第八步:服务端确认算法变更,返回ChangeCipherSpec消息

main, READ: TLSv1 Change Cipher Spec, length = 1

第九步:服务端发送Finished消息,加密算法生效:

*** Finished

那么如何让服务端也认证客户端的身份,即双向握手呢?其实很简单,在服务端代码中,把这一行:

((SSLServerSocket) _socket).setNeedClientAuth(false);

改成:

((SSLServerSocket) _socket).setNeedClientAuth(true);

就可以了。但是,同样的道理,现在服务端并没有信任客户端的证书,因为客户端的证书也是自己生成的。所以,对于服务端,需要做同样的工作:把客户端的证书导出来,并导入到服务端的证书仓库:

keytool -export -alias bluedash-ssl-demo-client -keystore ./client_ks -file client_key.cer
Enter keystore password:  client
Certificate stored in file <client_key.cer>

keytool -import -trustcacerts -alias bluedash-ssl-demo-client -file ./client_key.cer -keystore ./server_ks
Enter keystore password:  server
Owner: CN=localhost, OU=cn, O=cn, L=cn, ST=cn, C=cn
Issuer: CN=localhost, OU=cn, O=cn, L=cn, ST=cn, C=cn
Serial number: 4c57c80b
Valid from: Tue Aug 03 15:40:59 CST 2010 until: Mon Nov 01 15:40:59 CST 2010
Certificate fingerprints:
         MD5:  DB:91:F4:1E:65:D1:81:F2:1E:A6:A3:55:3F:E8:12:79
         SHA1: BF:77:56:61:04:DD:95:FC:E5:84:48:5C:BE:60:AF:02:96:A2:E1:E2
         Signature algorithm name: SHA1withRSA
         Version: 3
Trust this certificate? [no]:  yes
Certificate was added to keystore

完成了证书的导入,还要在客户端需要加入一段代码,用于在连接时,客户端向服务端出示自己的证书:

package org.bluedash.tryssl;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.security.KeyStore;
import javax.net.SocketFactory;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;

public class SSLClient {
    private static String CLIENT_KEY_STORE = "/Users/liweinan/projs/ssl/src/main/resources/META-INF/client_ks";
    private static String CLIENT_KEY_STORE_PASSWORD = "456456";

    public static void main(String[] args) throws Exception {
        // Set the key store to use for validating the server cert.
        System.setProperty("javax.net.ssl.trustStore", CLIENT_KEY_STORE);
        System.setProperty("javax.net.debug", "ssl,handshake");
        SSLClient client = new SSLClient();
        Socket s = client.clientWithCert();

        PrintWriter writer = new PrintWriter(s.getOutputStream());
        BufferedReader reader = new BufferedReader(new InputStreamReader(s.getInputStream()));
        writer.println("hello");
        writer.flush();
        System.out.println(reader.readLine());
        s.close();
    }

    private Socket clientWithoutCert() throws Exception {
        SocketFactory sf = SSLSocketFactory.getDefault();
        Socket s = sf.createSocket("localhost", 8443);
        return s;
    }

    private Socket clientWithCert() throws Exception {
        SSLContext context = SSLContext.getInstance("TLS");
        KeyStore ks = KeyStore.getInstance("jceks");

        ks.load(new FileInputStream(CLIENT_KEY_STORE), null);
        KeyManagerFactory kf = KeyManagerFactory.getInstance("SunX509");
        kf.init(ks, CLIENT_KEY_STORE_PASSWORD.toCharArray());
        context.init(kf.getKeyManagers(), null, null);

        SocketFactory factory = context.getSocketFactory();
        Socket s = factory.createSocket("localhost", 8443);
        return s;
    }
}

通过比对单向认证的日志输出,我们可以发现双向认证时,多出了服务端认证客户端证书的步骤:

*** CertificateRequest
Cert Types: RSA, DSS
Cert Authorities:
<CN=localhost, OU=cn, O=cn, L=cn, ST=cn, C=cn>
<CN=localhost, OU=cn, O=cn, L=cn, ST=cn, C=cn>
*** ServerHelloDone

*** CertificateVerify
main, WRITE: TLSv1 Handshake, length = 134
main, WRITE: TLSv1 Change Cipher Spec, length = 1

在 @*** ServerHelloDone@ 之前,服务端向客户端发起了需要证书的请求 @*** CertificateRequest@ 。
在客户端向服务端发出 @Change Cipher Spec@ 请求之前,多了一步客户端证书认证的过程 @*** CertificateVerify@ 。
客户端与服务端互相认证证书的情景,可参考下图:

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn