一次IOS通知推送問題排查全過程

原創:打碼日記(微信公眾號ID:codelogs),歡迎分享,轉載請保留出處。

發現問題

在上周一個將要下班的夜晚,測試突然和我打招呼,說IOS推送的修復更新上線後存在問題,後台報錯。

連忙跑到測試那裡看報錯詳情,報錯如下:
image_2022-05-04_20220504143349

重現問題

看到這個報錯後,在網上搜索了一下,這種錯誤一般都是因為客戶端不信任服務端SSL證書導致的,回想工作以來,好像遇到這種問題好多次了,只要將證書導入一下就好了。

由於不能冒然在線上修改解決問題,於是獲取推送相關信息(如:設備token)後,到自己電腦上去測試,看是否能重現問題。

啪啦啪啦,代碼修改完畢,點運行坐等錯誤出現。

5秒鐘過後,發現推送消息發送成功了,沒有出現報錯,有點懵逼!心裏想,代碼都是完全一樣的啊,怎麼線上報錯,我這卻是好的呢???

糾結了一會,於是開始靜下心來分析:

  1. 代碼肯定是一樣的,應該不是表面上的代碼原因。
  2. 其次推送設備也是一樣的,應該也不是手機問題。
  3. 那麼。。。

就在沒有頭緒之際,我又掃了一眼工程目錄,發現了jdk8,但我們線上系統使用的是jdk7啊。

image_2022-05-04_20220504143511

於是我將自己工程的jdk8換成jdk7試一下,同樣的報錯終於出現了!

那為什麼jdk8沒問題,jdk7卻報錯呢???

尋找原因

圍繞着網上的說法和之前的經驗,這hand_failure錯誤應該是SSL/TLS握手過程中不信任服務端證書導致的,那辦法很簡單,去蘋果官網找到蘋果服務端提供的證書,導入到java的證書信任庫cacert文件中即可。

image_2022-05-04_20220504143534

於是,下載了證書文件GeoTrust_Global__CA.cer,並用jdk自帶的keytool工具將證書導入到cacert文件中,如下:
image_2022-05-04_20220504143548
如上,導入過程中,發現提示已經有這個證書了,當時沒想那麼多,再導一次試試吧!

導入後,再次運行代碼,結果還是報錯!

心裏又慌亂起來,沒招了,於是又百度/google去了,但搜索出來的結論幾乎都是證書信任問題,和自己的招式一樣!

image_2022-05-04_20220504143607

同時,也了解了一下SSL/TLS協議相關知識,大致握手過程如下:

image_2022-05-04_20220504143625

詳細如下:

  1. 客戶端發送clientHello消息,告訴服務端我使用的TLS版本與加密套件等。

  2. 服務器返回serverHello消息,告訴自己選擇哪個TLS協議版本與加密套件等。

  3. 服務器發送Certification消息,將自己的數字證書(包括服務器名稱、CA和公鑰)作為消息內容發給客戶端。

  4. 客戶端Certificate verify校驗服務器的數字證書的有效性。

  5. 客戶端Change cipher spec選擇加密套件並生成會話密鑰(客戶端與服務器之間後續的數據傳輸將使用此會話密鑰)。

  6. 客戶端、服務器之間發送加密數據。

另外,jvm有一個-Djavax.net.debug=SSL的參數,可以把SSL/TLS的握手過程都在控制台通過日誌顯示出來,如下:

image_2022-05-04_20220504143642

Ok,既然知道了握手過程,那就把SSL/TLS的日誌顯示出來看一下吧,看看是什麼階段出現的問題,如下:

image_2022-05-04_20220504143658
這說明GeoTrust_Global.cer證書確實已經添加到信任庫中了,而詳細的SSL日誌如下:

image_2022-05-04_20220504143712

如上圖,報錯發生在clientHello發送之後,在讀服務端返回的serverHello消息時,卻讀到的是Alert:handshake_failure警告信息,然後SSL握手就中斷了,就好像你在和服務器打招呼,然後服務器回復了一個滾蛋一樣!

百思不得其解?

準備放棄

一會,我看到測試走了過來。

測試:「問題解決的怎麼樣了,啥情況啊」

:「還不知道,本來以為很簡單,實際上並沒有,jdk8可以,7不行」

測試:「好吧」

:」要不升級jdk8吧「

測試:」能行嗎「

:」……能行,jdk兼容性都很好,而且其它系統都已經升級過了,這個系統本來也計劃要升,問題不大「

測試:」……好的「

於是,測試去升級jdk8了,本來我應該站在測試身後等待問題被解決,但我還是想在這期間查一查根本原因,於是又琢磨了起來。

發現真相

回想起,這個系統以前本來是jdk6,就是因為蘋果強制開發者必須使用Https,且需要TLSv1.2版本,而jdk7才開始支持TLSv1.2,所以這個系統才升級到jdk7,那現在這個報錯會不會…

於是,我趕緊使用TLSv1.2試試,添加jvm參數-Dhttps.protocols=TLSv1.2即可強制https使用TLSv1.2版本協議,如下:

image_2022-05-04_20220504143729

加完後再次運行,報錯依舊,那還有那裡可能會有問題呢?

我突然靈光一閃,使用jdk7運行一次,再使用jdk8運行一次,將兩次的SSL日誌對比一下,看看哪裡不一樣,說不定能找到線索!

於是我馬上試運行並對比兩次SSL日誌,發現兩次SSL日誌中TLS協議使用的加密套件不同,如下圖:

jdk7的調試信息如下 :
image_2022-05-04_20220504143747
jdk8的調試信息如下:
image_2022-05-04_20220504143802
jdk8的TLS握手在服務端ServerHello之後,選擇的加密套件是TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,而jdk7中可供選擇的加密套件中沒有TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384

問題差不多清晰了,應該是jdk7中的加密套件,蘋果服務器覺得太老不安全,都不支持了,那麼如果我使用jdk8,並將加密套件強製為jdk7中的加密套件呢,應該也是會報錯的!

於是,我還是使用jdk8,並添加了jvm參數-Dhttps.cipherSuites=SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA,SSL_RSA_WITH_RC4_128_SHA,SSL_RSA_WITH_RC4_128_MD5,SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA,SSL_RSA_WITH_3DES_EDE_CBC_SHA,以強制jdk8使用jdk7的加密套件,如下:

image_2022-05-04_20220504143817

再次運行後,果然報錯了!所以應該是jdk8支持了一些新的加密套件,而蘋果服務端只認這些新的加密套件導致的。

於是,我去jdk官網,查了一下jdk8的新增特性,發現TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384中AES加密算法的GCM模式在jdk8中才支持,如下:

image_2022-05-04_20220504143832

至此,問題原因全部搞清楚了,測試升級jdk8後也報告推送正常,此時已是10點多,兩人寫寫日報趕緊回家去了……

往期內容

密碼學入門
時區的坑,不想再踩了!
常見的Socket網絡異常場景分析
字符編碼解惑