อธิบาย Base64, การเข้ารหัส URL และ JWT
ทั้งสามอย่างปรากฏในทุกที่ที่ข้อมูลต้องเดินทางในรูปข้อความ ทั้งใน URL, ส่วนหัว HTTP, JSON และโทเค็นเข้าสู่ระบบ และไม่มีอย่างใดซ่อนอะไรไว้เลย คู่มือนี้อธิบายว่าแต่ละอย่างทำงานอย่างไร คุณจะพบมันที่ไหน และข้อผิดพลาดที่เป็นต้นเหตุของปัญหาส่วนใหญ่
การเข้ารหัส (encoding) ไม่ใช่การเข้ารหัสลับ (encryption)
การเข้ารหัสแบบ encoding เปลี่ยนวิธีเขียนข้อมูลเพื่อให้ผ่านช่องทางที่รับได้เฉพาะอักขระบางชุด ใครก็ถอดกลับได้เพราะไม่มีกุญแจเกี่ยวข้อง การเข้ารหัสลับก็เปลี่ยนรูปข้อมูลเช่นกัน แต่มีเพียงผู้ถือกุญแจที่คืนข้อมูลเดิมได้ ส่วนแฮชเป็นอีกเรื่องหนึ่ง คือลายนิ้วมือทางเดียวที่ถอดกลับไม่ได้เลย ตามที่อธิบายไว้ใน อธิบายฟังก์ชันแฮช
Base64 และการเข้ารหัส URL เป็น encoding ส่วน JWT มักมีลายเซ็น ซึ่งพิสูจน์ว่าใครออกโทเค็นและเนื้อหาไม่ถูกแก้ไข แต่เนื้อหาเองถูกเข้ารหัสแบบ encoding เท่านั้น หากค่าใดต้องเป็นความลับ ค่านั้นต้องใช้การเข้ารหัสลับ ไม่ใช่รูปแบบใดในสามแบบนี้
Base64 ทำงานอย่างไร
Base64 อ่านข้อมูลทีละสามไบต์ (24 บิต) แล้วแบ่งเป็นสี่กลุ่ม กลุ่มละหกบิต แต่ละกลุ่มเป็นตัวเลข 0 ถึง 63 และเขียนเป็นอักขระหนึ่งตัวจากชุดอักขระ 64 ตัว ได้แก่ A–Z แทน 0–25, a–z แทน 26–51, 0–9 แทน 52–61 แล้วตามด้วย + และ / คำว่า “Man” แสดงขั้นตอนได้ดังนี้
| ขั้นตอน | M | a | n |
|---|---|---|---|
| ค่าของไบต์ | 77 | 97 | 110 |
| บิต | 01001101 | 01100001 | 01101110 |
| สี่กลุ่ม กลุ่มละ 6 บิต | 010011 · 010110 · 000101 · 101110 = 19 · 22 · 5 · 46 | ||
| Base64 | T W F u → TWFu | ||
หากความยาวของข้อมูลเข้าไม่ใช่พหุคูณของสาม กลุ่มสุดท้ายจะถูกเติมให้ครบและทำเครื่องหมายด้วย padding ไบต์ที่เหลือหนึ่งไบต์ให้อักขระสองตัวกับ == ไบต์ที่เหลือสองไบต์ให้อักขระสามตัวกับ = เช่น “Ma” กลายเป็น TWE= และ “M” กลายเป็น TQ==
เพราะทุกสามไบต์กลายเป็นสี่อักขระ Base64 จึงทำให้ข้อมูลใหญ่ขึ้นประมาณหนึ่งในสาม บวกกับการขึ้นบรรทัดใหม่ที่บางรูปแบบแทรกเข้าไป รูปภาพขนาด 3 MB ที่ส่งเป็นไฟล์แนบอีเมลจะเดินทางเป็นข้อความราว 4 MB และรูปภาพที่ฝังในสไตล์ชีตเป็น data URL จะหนักกว่าไฟล์เดิมราวหนึ่งในสาม
คุณจะพบ Base64 ที่ไหน
- ไฟล์แนบอีเมล (MIME) เป็นข้อความ Base64 ที่ตัดบรรทัดละ 76 อักขระ
- Data URL เช่น
data:image/png;base64,iVBORw0…ใส่ไฟล์ขนาดเล็กลงใน HTML หรือ CSS โดยตรง - การยืนยันตัวตนแบบ HTTP Basic ส่ง
user:passwordเป็น Base64 ในส่วนหัว Authorization ซึ่งซ่อนรหัสผ่านได้แค่จากการมองผ่าน ๆ จึงปลอดภัยเฉพาะเมื่อใช้ผ่าน HTTPS - ใบรับรองและกุญแจ ในไฟล์ PEM เป็น Base64 ระหว่างบรรทัด
-----BEGIN …-----และ-----END …-----โดยตัดบรรทัดละ 64 อักขระ - JSON และ XML ส่งค่าไบนารี เช่น ลายเซ็นหรือภาพขนาดย่อ เป็นสตริง Base64 เพราะไม่มีชนิดข้อมูลไบนารี
Base64URL และ padding
เครื่องหมาย + และ / ของ Base64 มาตรฐานมีความหมายใน URL และพาธไฟล์ ชุดอักขระที่สองคือ Base64URL จึงใช้ - และ _ แทน และมักตัด padding = ออกด้วย เพราะตัวถอดรหัสคำนวณความยาวเองได้ JWT, โทเค็น API จำนวนมาก และชื่อไฟล์บางแบบใช้ Base64URL หากต้องการเปลี่ยน Base64 มาตรฐานเป็น Base64URL ให้แทน + ด้วย -, แทน / ด้วย _ แล้วลบ = ที่ท้ายออก ส่วนการแปลงกลับให้ทำการแทนที่ย้อนกลับ แล้วเติม = จนความยาวเป็นพหุคูณของสี่
Percent-encoding ใน URL
URL มีอักขระได้เพียงชุดจำกัด และบางตัวในนั้นกำหนดโครงสร้างของที่อยู่ ได้แก่ / และ ? แยกพาธกับคิวรี, & และ = แยกพารามิเตอร์ และ # เริ่มต้นแฟรกเมนต์ เมื่อค่าใดมีอักขระเหล่านี้ หรือมีอักขระใดก็ตามที่อยู่นอก ASCII ค่านั้นจะถูก percent-encode คือทุกไบต์ในรูป UTF-8 จะเขียนเป็น % ตามด้วยเลขฐานสิบหกสองหลัก “café” กลายเป็น caf%C3%A9 เพราะ é คือสองไบต์ C3 A9 ส่วนอักษรไทยหนึ่งตัวใช้สามไบต์ใน UTF-8 จึงกลายเป็นสามชุด เช่น “ก” กลายเป็น %E0%B8%81
ตัวอักษร ตัวเลข และ - . _ ~ ไม่ต้องเข้ารหัสเลย อักขระต่อไปนี้ก่อปัญหาบ่อยที่สุด
| อักขระ | เมื่อเข้ารหัส | ทำไมจึงสำคัญ |
|---|---|---|
| ช่องว่าง | %20 | ใช้ใน URL ไม่ได้ |
| & | %26 | จะเริ่มพารามิเตอร์ใหม่ |
| = | %3D | จะจบชื่อพารามิเตอร์ |
| ? | %3F | จะเริ่มคิวรี |
| # | %23 | จะเริ่มแฟรกเมนต์ ซึ่งไม่ถูกส่งไปยังเซิร์ฟเวอร์ |
| / | %2F | จะเพิ่มส่วนของพาธ |
| + | %2B | ถูกอ่านเป็นช่องว่างในข้อมูลฟอร์ม |
| % | %25 | จะเริ่มลำดับ escape |
ให้เข้ารหัสแต่ละค่าแยกกัน แล้วจึงประกอบ URL ใน JavaScript encodeURIComponent ทำเช่นนี้กับค่าเดียวและเข้ารหัส / ? & = # ด้วย ขณะที่ encodeURI มีไว้สำหรับที่อยู่ทั้งหมดและปล่อยอักขระเหล่านั้นไว้ตามเดิม การใช้ encodeURI กับค่าเดียวเป็นสาเหตุที่พบบ่อยของสตริงคิวรีที่เสีย
เครื่องหมายบวก ช่องว่าง และการเข้ารหัสซ้ำ
ฟอร์ม HTML ส่งช่องข้อมูลเป็น application/x-www-form-urlencoded ซึ่งช่องว่างกลายเป็น + แต่ในไวยากรณ์ URL ทั่วไป + เป็นแค่เครื่องหมายบวก ดังนั้นการค้นหา “C++” ที่ส่งเป็น q=C++ จะไปถึงเซิร์ฟเวอร์ที่ถอดรหัสข้อมูลฟอร์มเป็น “C” ตามด้วยช่องว่างสองช่อง ค่าที่ถูกต้องคือ q=C%2B%2B เมื่อคุณถอดรหัสสตริงคิวรีด้วยตนเอง ให้ตรวจก่อนว่าสร้างขึ้นด้วยกฎใด
การเข้ารหัสซ้ำเกิดขึ้นเมื่อค่าที่เข้ารหัสแล้วถูกเข้ารหัสอีกรอบ %20 กลายเป็น %2520 เพราะ % เองกลายเป็น %25 หากคุณเห็น %25 ตามด้วยเลขฐานสิบหกสองหลักใน URL แสดงว่ามีขั้นตอนหนึ่งในสายงานที่เข้ารหัสสองครั้ง ให้ถอดรหัสหนึ่งครั้งแล้วหาว่าขั้นตอนที่เกินมาอยู่ตรงไหน
JWT มีอะไรอยู่ข้างใน
JSON Web Token (JWT) ส่งข้อความยืนยัน (claim) เช่น ผู้ใช้เป็นใครและโทเค็นใช้ได้ถึงเมื่อใด จากระบบที่ออกโทเค็นไปยังระบบที่รับ รูปแบบที่มีลายเซ็นซึ่งพบบ่อยประกอบด้วยสามส่วน Base64URL ที่คั่นด้วยจุด ได้แก่ ส่วนหัว เพย์โหลด และลายเซ็น เมื่อถอดรหัสแล้ว สองส่วนแรกเป็น JSON ธรรมดา
Header: {"alg": "HS256", "typ": "JWT"}
Payload: {"sub": "user-42", "role": "editor", "iat": 1767225600, "exp": 1767229200}
มาตรฐาน (RFC 7519) กำหนด claim ที่ลงทะเบียนไว้จำนวนหนึ่ง และแอปพลิเคชันเพิ่ม claim ของตนเอง เช่น role ด้านบน
| Claim | ความหมาย |
|---|---|
| iss | ผู้ออก: ใครเป็นผู้สร้างโทเค็น |
| sub | หัวเรื่อง: มักเป็น ID ของผู้ใช้หรือบัญชี |
| aud | ผู้รับ: บริการที่โทเค็นนี้มีไว้ใช้ |
| exp, nbf, iat | หมดอายุ, ห้ามใช้ก่อน, ออกเมื่อ: เวลา Unix หน่วยวินาที |
| jti | ID ของโทเค็น ใช้ตรวจการนำกลับมาใช้ซ้ำหรือเพิกถอนโทเค็นเดียว |
เพราะเพย์โหลดถูกเข้ารหัสแบบ encoding เท่านั้น ใครก็ตามที่ถือโทเค็นจึงอ่านได้ อย่าใส่ข้อมูลส่วนบุคคลหรือความลับ และทำให้โทเค็นมีขนาดเล็ก เพราะโทเค็นถูกส่งไปกับทุกคำขอ และเซิร์ฟเวอร์จำนวนมากจำกัดขนาดของส่วนหัวคำขอ
มีลายเซ็นไม่ได้แปลว่าเป็นความลับ: JWT ถูกตรวจสอบอย่างไร
ลายเซ็นคำนวณจากส่วนหัวและเพย์โหลด สำหรับ HS256 ผู้ออกและผู้รับใช้ความลับร่วมกันหนึ่งค่าและใช้ HMAC กับ SHA-256 สำหรับ RS256 หรือ ES256 ผู้ออกลงนามด้วยกุญแจส่วนตัว และผู้รับตรวจด้วยกุญแจสาธารณะที่คู่กัน ผู้รับจึงสร้างโทเค็นเองไม่ได้
เซิร์ฟเวอร์ที่รับโทเค็นต้องตรวจลายเซ็นด้วยอัลกอริทึมและกุญแจที่คาดไว้ และตรวจ exp, nbf, iss และ aud ก่อนจะเชื่อ claim ใด ๆ การถอดรหัสเพียงอย่างเดียวแบบที่เครื่องมือของ LeedyBox ทำ ข้ามขั้นตอนเหล่านี้ทั้งหมด มันแสดงว่าในโทเค็นมีอะไร ไม่ได้บอกว่าโทเค็นเป็นของจริงหรือไม่ ไลบรารีควรปฏิเสธอัลกอริทึม “none” ซึ่งหมายถึงโทเค็นที่ไม่มีลายเซ็นด้วย เว้นแต่ตั้งใจรับโทเค็นที่ไม่มีลายเซ็นจริง ๆ หากต้องซ่อนเนื้อหาเอง โทเค็นจะถูกเข้ารหัสลับ (JWE) และประกอบด้วยห้าส่วนที่อ่านไม่ได้หากไม่มีกุญแจ
ปัญหาที่พบบ่อยและวิธีแก้
| อาการ | สาเหตุที่น่าจะเป็น | วิธีแก้ |
|---|---|---|
| ถอด Base64 แล้วได้ข้อความอ่านไม่ออก | ข้อมูลเป็นไบนารี หรือเป็นข้อความในการเข้ารหัสอักขระแบบอื่น | ตรวจว่าสิ่งที่ถูกเข้ารหัสคืออะไร และบันทึกข้อมูลไบนารีเป็นไฟล์ |
| “อักขระไม่ถูกต้อง” ขณะถอดรหัส | มีคำนำหน้า data:, เครื่องหมายคำพูด หรือเป็นชุดอักขระ Base64URL | ลบทุกอย่างจนถึงเครื่องหมายจุลภาค และใช้ตัวถอดรหัสที่รับ - และ _ |
| พารามิเตอร์มาถึงไม่ครบ | มี & หรือ # ที่ไม่ได้เข้ารหัสอยู่ในค่า | เข้ารหัสแต่ละค่าด้วย encodeURIComponent |
| มี %2520 ใน URL | ค่าถูกเข้ารหัสสองครั้ง | เข้ารหัสครั้งเดียว ณ จุดที่ประกอบ URL |
| โทเค็นถูกปฏิเสธทั้งที่ exp ยังไม่ถึง | ผู้รับหรือผู้ออกผิด นาฬิกาคลาดเคลื่อน หรือกุญแจต่างกัน | เทียบ aud, iss และกุญแจลายเซ็นกับค่าที่เซิร์ฟเวอร์คาดไว้ |
| วันหมดอายุอยู่ในอีกหลายพันปี | exp เขียนเป็นมิลลิวินาทีแทนวินาที | เก็บ exp, nbf และ iat เป็นวินาที |
คำถามที่พบบ่อย
Base64 ปกป้องรหัสผ่านหรือคีย์ API ได้ไหม
ไม่ได้ ใครก็ถอด Base64 กลับได้ในเสี้ยววินาที ข้อมูลรับรองที่เข้ารหัสเป็น Base64 เช่น ในไฟล์กำหนดค่าหรือใน URL เปิดเผยพอ ๆ กับข้อความธรรมดา
ใส่ข้อมูลส่วนบุคคลในเพย์โหลดของ JWT ได้ไหม
ไม่ควร ใครก็ตามที่ถือโทเค็นอ่านเพย์โหลดได้ รวมถึงส่วนขยายของเบราว์เซอร์ ล็อก และพร็อกซีที่เห็นคำขอ ให้ใส่ ID ไว้ในโทเค็นแล้วดึงรายละเอียดจากเซิร์ฟเวอร์
การเข้ารหัส URL ป้องกันการโจมตีแบบแทรกโค้ด (injection) ได้ไหม
ได้เพียงในแง่ที่ค่านั้นทำลายโครงสร้างของ URL ไม่ได้ เซิร์ฟเวอร์ยังต้องตรวจสอบและ escape ค่าที่ถอดรหัสแล้วให้เหมาะกับที่ที่นำไปใช้ เช่น SQL, HTML หรือคำสั่งเชลล์
ทำไมสตริง Base64 จึงยาวกว่าไฟล์
ทุกสามไบต์กลายเป็นสี่อักขระ ข้อความจึงใหญ่ขึ้นราว 33% และการขึ้นบรรทัดใหม่เพิ่มอีกเล็กน้อย สำหรับไฟล์ขนาดใหญ่ การส่งไบต์โดยตรงมีประสิทธิภาพกว่าการใช้ Base64