인터넷에는 이미 SSL에 대한 많은 원칙과 소개가 있습니다. Java에서 인증서를 생성하고 SSL 통신을 구성하기 위해 keytool을 사용하는 방법에 대한 튜토리얼도 많이 있습니다. 하지만 SSL 서버와 SSL 클라이언트를 직접 만들지 못한다면 우리는 Java 환경에서 SSL 통신이 어떻게 구현되는지 깊이 이해하지 못할 수도 있습니다. SSL의 다양한 개념에 대한 지식도 사용할 수 있는 범위 내에서 제한될 수 있습니다. 이 기사에서는 간단한 SSL 서버와 SSL 클라이언트를 구성하여 Java 환경에서 SSL의 통신 원리를 설명합니다.
먼저 일반적인 Java 소켓 프로그래밍을 살펴보겠습니다. Java로 소켓 서버와 클라이언트의 예를 작성하는 것은 비교적 간단합니다.
서버는 매우 간단합니다. 포트 8080을 수신하고 클라이언트가 보낸 문자열을 반환합니다. 다음은 서버 코드입니다.
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(); } } }
클라이언트도 매우 간단합니다. 서버에 요청을 시작하고 "hello" 문자열을 보낸 다음 서버에서 응답을 받습니다. 다음은 클라이언트 코드입니다.
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(); } }
서버를 실행하고 클라이언트를 실행하면 "hello"라는 응답을 받게 됩니다.
이렇게 간단한 네트워크 통신 코드 집합을 SSL 통신을 사용하도록 변환해 보겠습니다. SSL 통신 프로토콜에서 우리는 먼저 서버에 디지털 인증서가 있어야 한다는 것을 알고 있습니다. 클라이언트는 서버에 연결되면 이 인증서를 받게 됩니다. 그런 다음 클라이언트는 인증서가 신뢰할 수 있는지 여부를 판단합니다. 의사소통의 열쇠. 인증서를 신뢰할 수 없으면 연결이 실패합니다.
따라서 먼저 서버용 디지털 인증서를 생성해야 합니다. Java 환경에서는 keytool을 이용하여 디지털 인증서를 생성하고, 이러한 인증서는 인증서 창고인 저장소(store) 개념으로 저장된다. keytool 명령을 호출하여 서버용 디지털 인증서를 생성하고 서버에서 사용하는 인증서 저장소를 저장해 보겠습니다.
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
이러한 방식으로 서버 인증서 bluedash-ssl-demo-server를 server_ksy 저장소 파일에 저장합니다. 이 글에서는 keytool의 사용법에 대해 자세히 설명하지 않습니다. 위 명령을 실행하면 다음과 같은 결과가 나옵니다.
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]
그런 다음 서버가 이 인증서를 사용하고 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(); } } }
보시다시피 소켓은 서버 준비 및 설정 작업이 대폭 늘어났으며, 추가된 코드는 주로 인증서를 가져와서 사용하는 데 사용됩니다. 또한 사용된 소켓은 SSLServerSocket이 되었고 포트는 8443으로 변경되었습니다(필수 사항은 아니며 사용자 정의를 따르기 위한 것임). 또한 가장 중요한 점은 서버 인증서의 CN이 서버의 도메인 이름과 일치해야 한다는 것입니다. 우리 인증서 서비스의 도메인 이름은 localhost이므로 클라이언트도 서버에 연결할 때 localhost를 사용해야 합니다. SSL에 따라 연결됩니다. 프로토콜 표준과 도메인 이름이 인증서의 CN과 일치하지 않으면 인증서가 보안되지 않으며 통신이 제대로 작동하지 않는다는 의미입니다.
서버에서는 원래 클라이언트를 사용할 수 없으며 SSL 프로토콜을 사용해야 합니다. 서버의 인증서는 자체적으로 생성되고 신뢰할 수 있는 기관의 서명이 없기 때문에 클라이언트는 서버 인증서의 유효성을 확인할 수 없으며 통신이 필연적으로 실패하게 됩니다. 따라서 클라이언트가 모든 신뢰 인증서를 저장할 웨어하우스를 만든 다음 서버 인증서를 이 웨어하우스로 가져와야 합니다. 이런 방식으로 클라이언트가 서버에 연결되면 서버의 인증서가 신뢰 목록에 있는지 확인하고 정상적으로 통신할 수 있습니다.
지금 해야 할 일은 클라이언트 인증서 웨어하우스를 생성하는 것입니다. keytool은 빈 웨어하우스만 생성할 수 없기 때문에 서버와 마찬가지로 인증서와 웨어하우스(클라이언트 인증서와 웨어하우스)를 생성합니다. :
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
결과는 다음과 같습니다.
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]
다음으로 서버 인증서를 내보내고 클라이언트 창고로 가져와야 합니다. 첫 번째 단계는 서버 인증서를 내보내는 것입니다:
keytool -export -alias bluedash-ssl-demo-server -keystore ./server_ks -file server_key.cer
실행 결과는 다음과 같습니다:
Enter keystore password: server Certificate stored in file <server_key.cer>
그런 다음 내보낸 인증서를 클라이언트 인증서 웨어하우스로 가져옵니다:
keytool -import -trustcacerts -alias bluedash-ssl-demo-server -file ./server_key.cer -keystore ./client_ks
결과는 다음과 같습니다:
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
좋아, 준비가 완료되었습니다. 클라이언트 코드를 작성해 보겠습니다.
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; } }
보시다시피 일부 클래스를 SSL 통신 클래스로 바꾸는 것 외에도 클라이언트는 또한 인증서 저장소를 신뢰하기 위한 코드를 더 많이 사용합니다. 이상으로 SSL 단방향 핸드셰이크 통신을 완료했습니다. 즉, 클라이언트는 서버의 인증서를 확인하고 서버는 클라이언트의 인증서를 확인하지 않습니다.
위는 Java 환경에서 SSL 단방향 핸드셰이크의 전체 과정입니다. 클라이언트의 로그 출력 수준을 DEBUG:
System.setProperty("javax.net.debug", "ssl,handshake");
로 설정했기 때문에 SSL 통신의 전체 프로세스를 볼 수 있습니다. 이러한 로그는 SSL을 통해 네트워크 연결을 설정하는 전체 프로세스를 보다 구체적으로 이해하는 데 도움이 될 수 있습니다. 규약. .
로그와 결합하여 SSL 양방향 인증의 전체 프로세스를 살펴보겠습니다.
1단계: 클라이언트가 ClientHello 메시지를 보내고 SSL 연결 요청을 시작하며 서버가 SSL 옵션(암호화)을 지원하는 방법 등).
*** ClientHello, TLSv1
2단계: 서버가 요청에 응답하고 ServerHello 메시지로 응답한 후 클라이언트와 SSL 암호화 방법을 확인합니다.
*** ServerHello, TLSv1
3단계: 서버가 공개 키를 다음에 게시합니다. 클라이언트.
第四步: 客户端与服务端的协通沟通完毕,服务端发送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@ 。
客户端与服务端互相认证证书的情景,可参考下图:
위 내용은 Java에서 SSL 통신 원칙의 샘플 코드 공유의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!