1. พื้นหลัง
HTTP เป็นโปรโตคอลสาธารณะที่ช่วยให้สามารถอ่านเนื้อหาได้และข้อมูลจากไคลเอนต์และเซิร์ฟเวอร์จะถูกส่งผ่านข้อความธรรมดาอย่างสมบูรณ์ ภายใต้พื้นหลังนี้ข้อมูลอินเทอร์เน็ตทั้งหมดที่ต้องอาศัยโปรโตคอล HTTP นั้นมีความโปร่งใสซึ่งนำความเสี่ยงด้านความปลอดภัยของข้อมูลที่ดีมาใช้ มีสองแนวคิดในการแก้ปัญหานี้:
ประเภทแรกมีแอพพลิเคชั่นที่หลากหลายในความเป็นจริงมากกว่าที่คิดไว้ ทั้งสองฝ่ายแลกเปลี่ยนคีย์ออฟไลน์และไคลเอนต์ใช้ ciphertext เมื่อส่งข้อมูลซึ่งส่งผ่านทางอินเทอร์เน็ตผ่านโปรโตคอล HTTP โปร่งใส หลังจากได้รับคำขอเซิร์ฟเวอร์ถอดรหัสและได้รับข้อความธรรมดาในลักษณะที่ตกลงกันไว้ แม้ว่าเนื้อหาประเภทนี้จะถูกแย่งชิง แต่ก็ไม่สำคัญเพราะบุคคลที่สามไม่ทราบวิธีการเข้ารหัสและการถอดรหัส อย่างไรก็ตามวิธีนี้มีความพิเศษเกินไปและทั้งไคลเอนต์และเซิร์ฟเวอร์จำเป็นต้องใส่ใจเกี่ยวกับการเข้ารหัสพิเศษและตรรกะการถอดรหัส
ด้าน C/S ประเภทที่สองอาจไม่สนใจเกี่ยวกับตรรกะพิเศษด้านบน พวกเขาเชื่อว่าการส่งและรับเป็นข้อความธรรมดาเนื่องจากส่วนการเข้ารหัสและการถอดรหัสได้รับการประมวลผลโดยโปรโตคอลเอง
การตัดสินจากผลลัพธ์ดูเหมือนว่าจะไม่มีความแตกต่างระหว่างสองโซลูชัน แต่จากมุมมองของวิศวกรซอฟต์แวร์ความแตกต่างนั้นใหญ่มาก เนื่องจากประเภทแรกต้องการระบบธุรกิจในการพัฒนาฟังก์ชั่นการเข้ารหัสและการถอดรหัสสำหรับการตอบสนองและจำเป็นต้องใช้คีย์แบบออฟไลน์อินเทอร์แอคทีฟประเภทที่สองจึงไม่มีปริมาณการพัฒนา
HTTPS เป็นรูปแบบการรักษาความปลอดภัยที่ได้รับความนิยมมากที่สุดสำหรับ HTTP ซึ่งสร้างขึ้นครั้งแรกโดย NetScape ใน HTTPS URL เริ่มต้นด้วย https: // แทนที่จะเป็น http: // เมื่อใช้ HTTPS คำขอและการตอบสนอง HTTP ทั้งหมดจะถูกเข้ารหัสก่อนที่จะถูกส่งไปยังเครือข่ายซึ่งจะถูกนำไปใช้ที่เลเยอร์ SSL
2. วิธีการเข้ารหัส
ข้อมูลข้อความธรรมดาจะถูกเข้ารหัสผ่านเลเยอร์ SSL จากนั้นส่งไปยังอินเทอร์เน็ตเพื่อส่งซึ่งแก้ปัญหาความปลอดภัยของข้อมูลดั้งเดิมของโปรโตคอล HTTP โดยทั่วไปวิธีการเข้ารหัสข้อมูลจะถูกแบ่งออกเป็นการเข้ารหัสแบบสมมาตรและการเข้ารหัสแบบอสมมาตร
2.1 การเข้ารหัสแบบสมมาตร
การเข้ารหัสแบบสมมาตรหมายถึงการใช้คีย์เดียวกันกับการเข้ารหัสและการถอดรหัส อัลกอริทึมทั่วไป ได้แก่ DES และ AES ฯลฯ และเวลาอัลกอริทึมเกี่ยวข้องกับความยาวคีย์
ข้อเสียที่ใหญ่ที่สุดของคีย์สมมาตรคือพวกเขาจำเป็นต้องรักษาคีย์สมมาตรจำนวนมากและต้องการการแลกเปลี่ยนแบบออฟไลน์ ในการเข้าร่วมเครือข่ายที่มีเอนทิตี N (N-1) จำเป็นต้องมีปุ่ม
2.2 การเข้ารหัสแบบอสมมาตร
การเข้ารหัสแบบอสมมาตรหมายถึงวิธีการเข้ารหัสตามคีย์สาธารณะ/ส่วนตัว อัลกอริทึมทั่วไปรวมถึง RSA ซึ่งโดยทั่วไปการเข้ารหัสช้ากว่าการเข้ารหัสแบบสมมาตร
การเข้ารหัสแบบสมมาตรมีขั้นตอนหนึ่งมากกว่าการเข้ารหัสแบบอสมมาตรนั่นคือเพื่อให้ได้คีย์สาธารณะเซิร์ฟเวอร์แทนที่จะเป็นคีย์ที่เก็บรักษาไว้โดยแต่ละคีย์
อัลกอริทึมการเข้ารหัสทั้งหมดขึ้นอยู่กับทฤษฎีจำนวนหนึ่งและผลกระทบคือผลการเข้ารหัสไม่สามารถกลับคืนสภาพได้ นั่นคือผ่านคีย์ส่วนตัวเท่านั้น ciphertext ที่เข้ารหัสโดยคีย์สาธารณะจะถูกถอดรหัส
ภายใต้อัลกอริทึมนี้จำนวนคีย์ในเครือข่ายทั้งหมดจะลดลงอย่างมากและแต่ละคนต้องการเพียงแค่รักษาคีย์ของ บริษัท นั่นคือในเครือข่ายของเอนทิตีจำนวนคีย์คือ 2N
ข้อเสียคือมันทำงานช้า
2.3 การเข้ารหัสไฮบริด
มีฉากหนึ่งในภาพยนตร์เรื่อง "God of Cookery" ของ Stephen Chow ซึ่งซึ่ง Underworld อยู่ในจุดที่ร้อนแรงโดยอ้างถึงปัญหาของการแบ่งแชสซีระหว่างกุ้งฉี่และลูกเนื้อวัว เทพเจ้าแห่งอาหารกล่าวว่า "มันลำบากจริงๆมันผสมกันเพื่อทำบอลเนื้อฉี่โง่ ๆ !"
ข้อได้เปรียบของการเข้ารหัสแบบสมมาตรคือความเร็วที่รวดเร็วและข้อเสียคือต้องมีการแลกเปลี่ยนที่สำคัญ ข้อได้เปรียบของการเข้ารหัสแบบไม่สมมาตรคือไม่จำเป็นต้องใช้คีย์แบบโต้ตอบและข้อเสียคือความเร็วช้า เพียงผสมเข้าด้วยกันและใช้ให้ดี
การเข้ารหัสแบบไฮบริดเป็นวิธีการเข้ารหัสที่ใช้โดยโปรโตคอล HTTPS คีย์สมมาตรจะถูกแลกเปลี่ยนก่อนผ่านการเข้ารหัสแบบไม่สมมาตรจากนั้นข้อมูลจะถูกส่งผ่านคีย์สมมาตร
เนื่องจากจำนวนการส่งข้อมูลมีขนาดใหญ่กว่าจำนวนข้อมูลที่ใช้สำหรับการแลกเปลี่ยนคีย์ในระยะแรกของการสร้างการเชื่อมต่อผลกระทบด้านประสิทธิภาพของการเข้ารหัสแบบไม่สมมาตรอาจเล็กน้อยและในเวลาเดียวกันปรับปรุงประสิทธิภาพ
3. https handshake
จะเห็นได้ว่าขึ้นอยู่กับโปรโตคอล HTTP ดั้งเดิม HTTPS ได้เพิ่มการประมวลผลเลเยอร์ความปลอดภัย:
4. การสนับสนุนของ HttpClient สำหรับโปรโตคอล HTTPS
4.1 รับโรงงานเชื่อมต่อ SSL และอุปกรณ์ตรวจสอบชื่อโดเมน
ในฐานะวิศวกรซอฟต์แวร์สิ่งที่เราใส่ใจคือวิธีการใช้ "HTTPS Protocol" ในรหัสอย่างไร ในการสำรวจความลึกลับของซอร์สโค้ด httpClient ทุกอย่างเริ่มต้นด้วย httpclientbuilder
Public CloseablehttpClient build () {// ละเว้นรหัสบางส่วน httpClientConnectionManager connmanagerCopy = this.connManager; // หากระบุตัวจัดการพูลการเชื่อมต่อให้ใช้หนึ่งที่ระบุมิฉะนั้นสร้างค่าเริ่มต้นใหม่ถ้า (connmanagercopy == null) {layeredConnectionsocketFactory sslsocketFactoryCopy = this.sslsocketFactory; ถ้า (sslsocketFactoryCopy == null) {// ถ้าเปิดใช้งานตัวแปรสภาพแวดล้อมการใช้งานรุ่น HTTPS และการควบคุมรหัสผ่านอ่านสตริงสุดท้าย [] supportProtocols = SystemProperties? split (system.getProperty ("https.protocols")): null; สตริงสุดท้าย [] SupportedCiphersuites = SystemProperties? split (system.getProperty ("https.ciphersuites")): null; // หากไม่ได้ระบุให้ใช้ตัวตรวจสอบชื่อโดเมนเริ่มต้นและจะตรวจสอบว่าชื่อโดเมนตรงกับใบรับรองที่ส่งคืนโดยเซิร์ฟเวอร์ในเซสชัน SSL Hostnameverifier HostnameverifierCopy = this.hostnameverifier; if (hostnameverifierCopy == null) {hostnameverifierCopy = ใหม่ defaulthostnameverifier (publicsuffixMatcherCopy); } // หากมีการกำหนด SSLContext จะสร้างโรงงานเชื่อมต่อ SSL ที่กำหนดเองมิฉะนั้นจะใช้โรงงานเชื่อมต่อเริ่มต้นถ้า (SSLContext! = NULL) {SSLSocketFactoryCopy = SSLConnectionsOcketFactory (SSLContext, Support } else {if (systemproperties) {sslsocketFactoryCopy = ใหม่ sslconnectionsocketFactory ((SSLSocketFactory) SSLSocketFactory.getDefault (), สนับสนุน Protocols, สนับสนุน } else {sslsocketFactoryCopy = ใหม่ sslConnectionsOcketFactory (sslContexts.createdefault (), hostnameverifierCopy); }}} // ลงทะเบียนโรงงานเชื่อมต่อ SSL ในตัวจัดการพูลเชื่อมต่อ เมื่อจำเป็นต้องมีการเชื่อมต่อ HTTPS การเชื่อมต่อ SSL จะถูกสร้างขึ้นตามโรงงานเชื่อมต่อ SSL ข้างต้น @suppresswarnings ("ทรัพยากร") ขั้นสุดท้าย poolinghttpClientConnectionManager poolingmgr = new PoolinghttpClientConnectionManager (RegistryBuilder. <การเชื่อมต่อ PlainConnectionsocketFactory.getSocketFactory ()). register ("https", sslsocketfactorycopy). build (), null, null, dnsresolver, conntimetolive, conntimetolivetimeunit! = null // ละเว้นรหัสบางส่วน}}รหัสข้างต้นสร้าง SSLConnectionsOcketFactory SSLConnectionsocketFactory และลงทะเบียนในการเชื่อมต่อ Pool Manager สำหรับการผลิตการเชื่อมต่อ SSL ในภายหลัง การอ้างอิงสำหรับการรวมการเชื่อมต่อ: http://www.vevb.com/article/141015.htm
ที่นี่มีการใช้ส่วนประกอบสำคัญหลายอย่างเมื่อกำหนดค่า SSLConnectionsOcketFactory ชื่อโดเมน Hostnameverifier และบริบท SSLContext
ในหมู่พวกเขา Hostnameverifier ใช้เพื่อตรวจสอบว่าใบรับรองเซิร์ฟเวอร์ตรงกับชื่อโดเมนหรือไม่ มีการใช้งานมากมาย Defaulthostnameverifier ใช้กฎการตรวจสอบเริ่มต้นแทนที่ BrowserCompathostnameverifier และ StricThostnameverifier ในเวอร์ชันก่อนหน้า Noophostnameverifier แทนที่อนุญาตให้ Allowallhostnameverifier โดยใช้กลยุทธ์ของการไม่ตรวจสอบชื่อโดเมน
โปรดทราบว่า มีความแตกต่างบางอย่างที่นี่ BrowserCompathostameverifier สามารถจับคู่ชื่อโดเมนย่อยหลายระดับและ "*.foo.com" สามารถจับคู่ "abfoo.com" Stricthostnameverifier ไม่สามารถจับคู่ชื่อโดเมนย่อยหลายระดับได้เฉพาะกับ "a.foo.com"
หลังจาก 4.4, httpClient ใช้ defaulthostnameverifier ใหม่เพื่อแทนที่สองกลยุทธ์ข้างต้นและเก็บกลยุทธ์ที่เข้มงวดเพียงหนึ่งเดียวและ Stricthostnameverifier เนื่องจากกลยุทธ์ที่เข้มงวดคือกลยุทธ์ของ IE6 และ JDK เองกลยุทธ์ที่ไม่ใช่การ จำกัด จึงเป็นกลยุทธ์ของ Curl และ Firefox นั่นคือการใช้งาน HTTPClient เริ่มต้นไม่รองรับกลยุทธ์การจับคู่ชื่อโดเมนหลายระดับหลายระดับ
SSLContext เก็บข้อมูลคีย์ที่เกี่ยวข้องกับคีย์ซึ่งเกี่ยวข้องโดยตรงกับธุรกิจและมีความสำคัญมาก นี่คือการวิเคราะห์แยกต่างหากในภายหลัง
4.2 วิธีรับการเชื่อมต่อ SSL
จะรับการเชื่อมต่อจากพูลเชื่อมต่อได้อย่างไร? กระบวนการนี้ได้รับการวิเคราะห์ในบทความก่อนหน้า ฉันจะไม่วิเคราะห์ที่นี่ โปรดดูการเชื่อมต่อ: //www.vevb.com/article/141015.htm
หลังจากได้รับการเชื่อมต่อจากกลุ่มการเชื่อมต่อหากการเชื่อมต่อไม่ได้อยู่ในสถานะที่กำหนดคุณจะต้องสร้างการเชื่อมต่อก่อน
รหัสของชิ้นส่วน defaulthttpClientConnectionOperator คือ:
Public Void Connect (ManagedhttpClientConnection Conn, โฮสต์ httphost สุดท้าย, ขั้นสุดท้าย inetsocketaddress localaddress, int connecttimeout สุดท้าย, socketconfig final socketconfig, บริบท httpcontext สุดท้าย) โยน ioexception {// ก่อนหน้านี้ การค้นหาที่นี่ได้รับการใช้งาน HTTPS นั่นคือ SSLConnectionsOcketFactory การค้นหาขั้นสุดท้าย <ConnectionSocketFactory> รีจิสทรี = getSocketFactoryRegistry (บริบท); การเชื่อมต่อขั้นสุดท้าย SF = registry.lookup (host.getSchemename ()); if (sf == null) {โยน unsupportedschemeException ใหม่ (host.getSchemename () + "โปรโตคอลไม่ได้รับการสนับสนุน"); } // หากที่อยู่อยู่ในรูปแบบ IP สามารถใช้งานได้โดยตรงไม่เช่นนั้นใช้ DNS Parser เพื่อแยกวิเคราะห์ IP ที่สอดคล้องกับชื่อโดเมนเพื่อรับ inetaddress สุดท้าย [] ที่อยู่ = host.getAddress ()! = null? ใหม่ inetaddress [] {host.getAddress ()}: this.dnsresolver.resolve (host.getHostname ()); พอร์ต int สุดท้าย = this.schemeportresolver.resolve (โฮสต์); // ชื่อโดเมนอาจสอดคล้องกับหลาย IP และพยายามเชื่อมต่อตามลำดับ (int i = 0; i <address.length; i ++) {ที่อยู่ inetaddress สุดท้าย = ที่อยู่ [i]; บูลีนสุดท้ายสุดท้าย = i == ที่อยู่ความยาว - 1; // นี่เป็นเพียงซ็อกเก็ตที่สร้างขึ้นและไม่มีการเชื่อมต่อกับซ็อกเก็ตถุงเท้า = sf.createsocket (บริบท); // ตั้งค่าพารามิเตอร์เลเยอร์ TCP บางตัว Sock.SetSotimeout (SocketConfig.getSotimeout ()); Sock.setreuseaddress (SocketConfig.issoreUseaddress ()); Sock.settcpNodeLay (SocketConfig.istcpNodelay ()); Sock.Setkeepalive (SocketConfig.issokeepalive ()); if (socketConfig.getRcvBufSize ()> 0) {sock.setReceiveBuffersize (socketConfig.getRcvBufSize ()); } if (socketConfig.getSndBufSize ()> 0) {sock.setsendBuffersize (SocketConfig.getSndBufSize ()); } int สุดท้าย linger = socketConfig.getSolinger (); if (linger> = 0) {sock.setsolinger (จริง, linger); } conn.bind (ถุงเท้า); ขั้นสุดท้าย inetSocketAddress remoteaddress = ใหม่ inetSocketAddress (ที่อยู่, พอร์ต); if (this.log.isdebugenabled ()) {this.log.debug ("เชื่อมต่อกับ" + remoteaddress); } ลอง {// สร้างการเชื่อมต่อผ่าน SSLConnectionsOcketFactory และผูกกับ Conn Sock = SF.ConnectSocket (ConnectTimeOut, Sock, Host, Remoteaddress, LocalAddress, บริบท); conn.bind (ถุงเท้า); if (this.log.isdebugenabled ()) {this.log.debug ("การเชื่อมต่อที่สร้างขึ้น" + conn); } กลับ; } // ละเว้นรหัสบางส่วน}}ในรหัสข้างต้นเราจะเห็นว่างานเตรียมนั้นก่อนที่จะสร้างการเชื่อมต่อ SSL นี่เป็นกระบวนการทั่วไปและสิ่งเดียวกันนี้เป็นจริงสำหรับการเชื่อมต่อ HTTP ทั่วไป กระบวนการพิเศษของการเชื่อมต่อ SSL สะท้อนอยู่ที่ไหน
ซอร์สโค้ดของ SSLConnectionsocketFactory มีดังนี้:
@Override ซ็อกเก็ตสาธารณะ ConnectSocket (ขั้นสุดท้าย Int ConnectTimeout ซ็อกเก็ตขั้นสุดท้าย, โฮสต์ httphost สุดท้าย, Inetsocketaddress Final Remoteaddress, Inetsocketaddress ครั้งสุดท้าย LocalAddress, บริบท httpcontext สุดท้าย) โยน ioexception args.notnull (remoteaddress, "ที่อยู่ระยะไกล"); ซ็อกเก็ตซ็อกเก็ตสุดท้าย = ซ็อกเก็ต! = null? ซ็อกเก็ต: CreateSocket (บริบท); if (localaddress! = null) {sock.bind (localaddress); } ลอง {if (ConnectTimeOut> 0 && sock.getSotimeout () == 0) {sock.setSotimeout (ConnectTimeout); } if (this.log.isdebugenabled ()) {this.log.debug ("การเชื่อมต่อซ็อกเก็ตกับ" + remoteaddress + "ด้วยหมดเวลา" + ConnectTimeOut); } // สร้างการเชื่อมต่อ Sock.Connect (RemoteAddress, ConnectTimeOut); } catch (สุดท้าย ioexception ex) {ลอง {sock.close (); } catch (สุดท้าย ioexception ละเว้น) {} โยน ex; } // หากเป็น SSLSocket ในปัจจุบันให้ทำการจับมือ SSL และการตรวจสอบชื่อโดเมนถ้า (Socks Instanceof SSLSocket) {สุดท้าย SSLSocket SSLSOCK = (SSLSocket) Sock; this.log.debug ("เริ่มจับมือกัน"); sslsock.starthandshake (); VerifyHostName (SSLSock, host.getHostname ()); ถุงเท้ากลับ; } else {// ถ้าไม่ใช่ sslsocket ให้ห่อเป็น sslsocket return createLayeredSocket (sock, host.getHostname (), remoteaddress.getport (), บริบท); }} @Override ซ็อกเก็ตสาธารณะ createLayeredSocket (ซ็อกเก็ตซ็อกเก็ตสุดท้าย, เป้าหมายสตริงสุดท้าย, พอร์ต int สุดท้าย, บริบท httpcontext สุดท้าย) โยน ioexception {// ห่อซ็อกเก็ตปกติเป็น sslsocket, socketfactory ถูกสร้างขึ้นตาม sslcontext ใน httpclientbuilder (SSLSocket) this.socketFactory.createOctocket (ซ็อกเก็ต, เป้าหมาย, พอร์ต, จริง); // หากรุ่น SSL Layer Protocol และอัลกอริทึมการเข้ารหัสจะใช้สูตรให้ใช้รูปแบบที่ระบุมิฉะนั้นค่าเริ่มต้นถ้า (supportedProtocols! = null) {sslsock.setenabledProtocols (supportedProtocols); } else {// หากไม่ได้ตั้งค่าโปรโตคอลที่รองรับอย่างชัดเจนให้ลบ SSL Protocol ทั้งหมดสตริงสุดท้าย [] AllProtocols = SSLSock.getEnabledProtocols (); รายการสุดท้าย <String> enabledProtocols = new ArrayList <String> (allProtocols.length); สำหรับ (โปรโตคอลสตริงสุดท้าย: AllProtocols) {if (! Protocol.startswith ("SSL")) {enabledProtocols.add (โปรโตคอล); }} if (! enabledprotocols.isempty ()) {sslsock.setenabledprotocols (enabledprotocols.toarray (สตริงใหม่ [enabledprotocols.size ()])); }} if (supportedCiphersuites! = null) {sslsock.setenabledCiphersuites (supportedCiphersuites); } if (this.log.isdebugenabled ()) {this.log.debug ("เปิดใช้งานโปรโตคอล:" + array.aslist (sslsock.getenabledprotocols ())); this.log.debug ("เปิดใช้งานชุดรหัส:" + array.aslist (sslsock.getenabledciphersuites ())); } preperesocket (sslsock); this.log.debug ("เริ่มจับมือกัน"); // SSL Handshake sslsock.starthandshake (); // หลังจากการจับมือสำเร็จแล้วให้ตรวจสอบว่าใบรับรองที่ส่งคืนนั้นสอดคล้องกับชื่อโดเมน VerifyHostName (SSLSock, Target) หรือไม่ ส่งคืน sslsock; -ดังที่เห็นได้สำหรับการสื่อสาร SSL ขั้นแรกให้สร้างการเชื่อมต่อซ็อกเก็ตปกติจากนั้นทำการจับมือ SSL แล้วตรวจสอบความสอดคล้องของใบรับรองและชื่อโดเมน การดำเนินการที่ตามมาคือการสื่อสารผ่าน SSLSocketimpl รายละเอียดโปรโตคอลสะท้อนให้เห็นในคลาส SSLSocketImpl แต่ส่วนนี้ของรหัส JDK ไม่ใช่โอเพนซอร์ส ผู้ที่สนใจสามารถดาวน์โหลดซอร์สโค้ด OpenJDK ที่สอดคล้องกันเพื่อทำการวิเคราะห์ต่อไป
5. สรุปบทความนี้
โอเคข้างต้นเป็นเนื้อหาทั้งหมดของบทความนี้ ฉันหวังว่าเนื้อหาของบทความนี้จะมีค่าอ้างอิงบางอย่างสำหรับการศึกษาหรือที่ทำงานของทุกคน หากคุณมีคำถามใด ๆ คุณสามารถฝากข้อความไว้เพื่อสื่อสาร ขอบคุณสำหรับการสนับสนุน Wulin.com