ถึงแม้ว่าเจ้าของบล็อกจะไม่ค่อยชอบใช้ WebView ภายในแอปซักเท่าไร แต่ในบางครั้งก็เลี่ยงไม่ได้เพราะว่าฟีเจอร์บางตัวยังเป็นหน้าเว็ปอยู่ แต่มันดันเป็นหน้าเว็ป HTTPS ที่เจอปัญหา SSL Error นี่แหละ ดังนั้นต้องหาวิธีจัดการให้ถูกต้องแล้วล่ะ!!
เมื่อ WebView เจอปัญหา SSL Error
เกิดขึ้นได้เป็นปกติเมื่อ URL ปลายทางนั้นมีปัญหาเกี่ยวกับ SSL ซึ่งหลักๆก็คงไม่พ้นเรื่อง Certificate มีปัญหานี่แหละ (Expired บ้าง, Self-Signed บ้าง และอื่นๆอีกมากมาย) ซึ่งผลลัพธ์ที่ได้จะเป็นแบบนี้

หรือบางเวอร์ชันก็ขึ้นหน้าขาวโพลนไปเลย
แต่บางครั้งฝั่งแอปก็ต้องหาทางแสดงผลให้ได้เสียก่อน (ถึงแม้ว่าวิธีแก้ไขที่ถูกต้องคือต้องแก้จากทางฝั่ง Server) เพราะขนาดเวลาเปิดบน Chrome ยังมีให้กด Proceed เพื่อยืนยันการเข้าสู่หน้าเว็ปเลย

เว็ปสำหรับทดสอบ SSL Error
เนื่องจากจะต้องทดสอบกับ URL ซักแห่งที่มีปัญหา SSL Error ซึ่งจะไปหาตามหน้าเว็ปทั่วไปก็ใช่ว่าจะเจอกันได้ง่ายๆ จะให้สร้าง Server ขึ้นมาเทสเองก็ดูเหมือนจะเสียเวลาเกินไปหน่อย จนสุดท้ายเจ้าของบล็อกได้ไปเจอเว็ปไซต์แห่งหนึ่งที่มีไว้สำหรับทดสอบ SSL Error ทุกกรณีที่มีชื่อว่า https://badssl.com/

อยากจะทดสอบกับ SSL Error แบบไหนก็กดเลือกแล้วเอา URL มาใช้ได้เลย โคตรสะดวกกกกกกก
วิธีแก้ปัญหาที่ไม่ถูกต้อง (แต่ก็ได้ผลเหมือนกัน)
เมื่อเข้าหน้าเว็ปดังกล่าวไม่ได้ นักพัฒนาบางคนจึงใช้วิธีสั่ง Proceed ผ่านโค้ดของ WebViewClient แบบนี้
class MainActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
webView.webViewClient = CustomWebViewClient()
webView.loadUrl("ssl_error_url")
}
class CustomWebViewClient : WebViewClient() {
override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) {
handler.proceed()
}
}
}
เวลาที่เจอปัญหาเกี่ยวกับ SSL Error บน WebView จะสามารถสร้างคลาส WebViewClient แล้ว Override Method ที่ชื่อว่า onReceivedSslError
เพื่อดัก Event ดังกล่าวได้ ดังนั้นผู้ที่หลงเข้ามาอ่านจึงสามารถสั่ง Proceed จากคลาส SslErrorHandler
ที่มีอยู่ได้เลย
เมื่อลองทดสอบใหม่อีกครั้ง

เย้ เย้ เข้าได้แล้วววววววว
แต่หลังจากที่แอปขึ้น Google Play Store ไปได้ซักพักหนึ่ง ผู้ที่หลงเข้ามาอ่านก็จะได้รับอีเมลล์จากทาง Google Play ที่แจ้งปัญหาเกี่ยวกับ Security Warning โดยมีใจความแบบนี้

จริงๆข้อความจะยาวมาก แต่สรุปสั้นๆก็คือวิธีที่ใช้แก้ไขปัญหา SSL Error ใน WebView นั้นไม่ถูกต้อง เพราะการบังคับ Proceed ทุกครั้งแบบนี้จะทำให้แอปเกิดช่องโหว่ที่อาจจะถูกโจมตีด้วย วิธี Man-in-the-middle โดยที่ผู้ใช้ไม่สามารถหลีกเลี่ยงได้เลย ดังนั้นจึงควรแก้ไขซะ
วิธีแก้ปัญหาที่ถูกต้อง
เมื่อเกิด URL ที่จะแสดงผลใน WebView เกิดปัญหา SSL Error จะต้องแสดง Dialog แจ้งเตือนแก่ผู้ใช้ว่า URL ที่กำลังจะเข้านั้นไม่ปลอดภัย แล้วให้ผู้ใช้เป็นคนตัดสินใจแทนว่าจะปิดหน้าดังกล่าวหรือว่าเข้าใช้งานต่อ
SSL Error นั้นมีหลายสาเหตุ เพื่อให้แสดงข้อความตามประเภทของ SSL Error ผู้ที่หลงเข้ามาอ่าสามารถเช็คแล้วกำหนดข้อความตามที่ต้องการได้
private fun getSslErrorMessage(error: SslError): String = when (error.primaryError) {
SslError.SSL_DATE_INVALID -> "The certificate date is invalid."
SslError.SSL_EXPIRED -> "The certificate has expired."
SslError.SSL_IDMISMATCH -> "The certificate hostname mismatch."
SslError.SSL_INVALID -> "The certificate is invalid."
SslError.SSL_NOTYETVALID -> "The certificate is not yet valid"
SslError.SSL_UNTRUSTED -> "The certificate is untrusted."
else -> "SSL Certificate error."
}
ซึ่ง getSslErrorMessage(error: SslError)
มีไว้แสดงข้อความตามประเภทของ Error เพื่อแสดงผล Dialog ให้ผู้ใช้รับรู้นั่นเอง
ทีนี้ก็สร้าง Dialog ขึ้นมาเพื่อแจ้งให้ผู้ใช้รับรู้ โดยมีตัวเลือกระหว่าง Proceed กับ Cancel
class CustomWebViewClient : WebViewClient() {
override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) {
val message = getSslErrorMessage(error)
val proceed = "Proceed"
val cancel = "Cancel"
val builder: AlertDialog.Builder = Builder(view.context)
builder.setMessage(message)
.setPositiveButton(proceed, DialogInterface.OnClickListener { dialog, id -> handler.proceed() })
.setNegativeButton(cancel, DialogInterface.OnClickListener { dialog, id -> handler.cancel() })
builder.create().show()
}
private fun getSslErrorMessage(error: SslError): String = ...
}
ดังนั้นเวลาที่ WebView โหลดหน้าเว็ปใดๆก็ตามแล้วเกิดปัญหา SSL Error ก็จะแสดง Dialog แจ้งเตือนผู้ใช้แบบนี้

เพียงเท่านี้ผู้ใช้ก็สามารถเลือกได้ว่าจะปิด (Cancel) หรือเข้าใช้งานต่อ (Proceed)
สรุป
ในกรณีที่มีการใช้งาน WebView แล้วต้องเข้าหน้าเว็ปที่มีปัญหา SSL Error ไม่ว่าจะกรณีใดก็ตาม ผู้ที่หลงเข้ามาอ่านไม่ควรสั่ง Proceed เพื่อเข้าหน้าเว็ปทันที แต่ควรจะแสดง Dialog แจ้งเตือนแล้วให้ผู้ใช้เป็นคนตัดสินใจเลือกเองว่าจะเข้าใช้งานต่อหรือว่าปิดหน้านั้นๆทิ้งไปซะ ถึงแม้ว่าวิธีดังกล่าวจะไม่ได้ช่วยป้องกันช่องโหว่ Man-in-the-middle ก็ตาม (แต่ก็ช่วยป้องกัน Security Alert จาก Google Play นะ) แต่ผู้ใช้ก็สามารถตัดสินใจเลือกได้ด้วยตัวเองเหมือนกับบนหน้าเว็ปของ Chrome
แต่ทางที่ดีที่สุดก็คือ แก้ไขจากฝั่ง Server ตั้งแต่แรกเถอะครับ…