สร้างโปรแกรม Scan Thai ID Card ด้วยภาษา Go-lang
ก่อนอื่นเลย เราจะต้องรู้ก่อนว่า Service ที่เรากำลังจะสร้าง เราจะทำไปเพื่ออะไร ตอบแบบภาพรวมก็คือ โดยการเสียบบัตรเข้าไปที่เครื่องสแกน เพื่อดึงข้อมูลจากบัตรประชาชน แล้ว นำข้อมูลนั้นไปยืนยันหรือเอาไปแนบอ้างอิงธุรกรรมต่าง ๆ โดยไม่ต้องมาเสียเวลากรอกข้อมูลเอง และลดความเสี่ยงเจอบัตรประชาชนปลอม
ตัวอย่างหน่วยงานที่ควรใช้เพื่อให้ทำงานสะดวกขึ้นมีอะไรบ้างก็เช่น
ภาพจาก : ประชาชาติธุรกิจ
- หน่วยงานเกี่ยวกับธุรกรรมทางการเงิน เช่น เมื่อเราไปสมัครใช้งานบริการทางการเงินธนาคาร ซึ่งเราไม่เคยทำธุรกรรมกับธนาคาร บริการนี้จำเป็นต้องมีการเก็บข้อมูลจากบัตรประชาชน
- บริการจัดส่งพัสดุ เช่น เราไปส่งของที่ไปรษณีย์ พนักงานจะต้องขอข้อมูลบัตรประชาชน เพื่อที่จะตรวจยืนยันว่าเป็นบุคลที่ส่งของคนนี้จริง ๆ
- หน่วยงานต่าง ๆ ทั้งหน่วยงานราชการและเอกชน ที่ระบบเดิม ไม่ว่าจะเป็นฟอร์มกระดาษที่ต้องกรอกเอง หรือแบบฟอร์มอิเล็กทรอนิกส์ที่ยังมากรอกทีละช่อง
โปรแกรมมีการทำงานอย่างไรรายละเอียดเราจะดึงก็ข้อมูลจากบัตรได้อย่างไรเดี๋ยวเรามาทำไปด้วยกันครับ
เครื่องมือที่เราใช้มีอะไรบ้างมาดูกัน
ภาษา Go-lang
Visual Studio Code โปรแกรม IDE ใช้สำหรับเขียน Code Go-lang (หรือโปรแกรมอะไรก็ได้ที่ถนัดและสามารถใช้ทำกับ ภาษา GO ที่เลือกมาเพราะเป็นโปรแกรมที่ใช้กันทั่วไป มี extension ให้เราเลือกลงได้ และที่มันก็ฟรีด้วย)
เครื่องสแกนบัตร ในที่นี้ที่ยกตัวอย่างคือ ยี่ห้อ ACS รุ่น ACR39U-NFF โดยใช้ port ต่อผ่าน USB Type C ราคาโดยประมาณ 1,000 บาท (บวกลบเล็กน้อย)
ภาพจาก : R&D COMPUTER SYSTEM CO.,LTD.
เตรียมตัวก่อนเริ่มเขียนโปรแกรม
- เริ่มต้นจากติดตั้ง Go-lang (ดูวิธีเพิ่มเติมได้ที่ https://go.dev/doc/install)
- ดาวน์โหลด และติดตั้ง Visual Studio Code (Link Download: https://code.visualstudio.com/download)
- ผมจะแนะนำให้ติดตั้ง extension ของ GO ด้วยใน Visual Studio Code โดยไปที่ extension แล้วพิมพ์ค้นหาคำว่า GO จะเจอดังรูปด้านล่างแล้วเรากด Install ได้เลย
สร้าง Project ใหม่
1) สร้าง Folder หรือ Directory ใหม่ขึ้นมาด้วการรันคำสั่งใน Command Line >> mkdir go-scancard-api
2) สร้างไฟล์ go.mod ในโปรเจกต์ของเรา โดยรันคำสั่ง go mod init >> go mod init example/go-scancard-api
3) สร้างไฟล์ชื่อ main.go ใน Floder และเขียนโค้ด Hello World ง่ายๆ กันก่อน
package main
import ( “fmt” )
func main() { fmt.Println(“Hello World!”) } |
4) ลองรันโค้ดด้วยคำสั่งใน Command Line ใน folder โปเจ็ค
go run .
ผลลัพธ์ ขึ้นข้อความ Hello World! บนจอ terminal แล้ว( . ในที่นี้หมายถึง run ไฟล์ go ทั้งหมดใน project นี้ ทำไมเราใช้ เพราะว่าในขันตอนถัด ๆ ไป เราจะมีไฟล์ต่าง ๆ เพิ่มขึ้นมา)
สร้าง Service Scan Card ด้วย Library sscard
1) ก่อนอื่นเราจะเพิ่ม library เข้ามาใน Project เราก่อน
go get -u golang.org/x/net/html/charset
go get -u github.com/gogetth/sscard go get github.com/ebfe/scard |
2) สร้างไฟล์ exampleThaiIDCard.go และใส่โค้ดเริ่มต้นโดย main หลักจะเป็นฟังชั่น exampleThaiIDCard()
package main
import ( “fmt” “github.com/ebfe/scard” “github.com/gogetth/sscard” )
func exampleThaiIDCard() {
// Establish a PC/SC context context, err := scard.EstablishContext() if err != nil { fmt.Println(“Error EstablishContext:”, err) return }
// Release the PC/SC context (when needed) defer context.Release()
// List available readers readers, err := context.ListReaders() if err != nil { fmt.Println(“Error ListReaders:”, err) return }
// Use the first reader reader := readers[0] fmt.Println(“Using reader:”, reader)
// Connect to the card card, err := context.Connect(reader, scard.ShareShared, scard.ProtocolAny) if err != nil { fmt.Println(“Error Connect:”, err) return }
// Disconnect (when needed) defer card.Disconnect(scard.LeaveCard)
// Send select APDU selectRsp, err := sscard.APDUGetRsp(card, sscard.APDUThaiIDCardSelect) if err != nil { fmt.Println(“Error Transmit:”, err) return } fmt.Println(“resp sscard.APDUThaiIDCardSelect: “, selectRsp)
} |
3) ไฟล์ main.go เราจะแก้โดยการเอา exampleThaiIDCard() ไปใส่
package main
func main() { exampleThaiIDCard() } |
4) เราจะรันไฟล์ด้วยคำสั่ง go run . โดยตอนนี้จะทดสอบสถานะเครื่อง แต่ยังไม่ดึงข้อมูลจากบัตร
4.1 รันโดยไม่เสียบเครื่องอ่านบัตร
รันคำสั่ง
ผลลัพธ์ จะเห็นได้ว่าจะขึ้นแจ้ง error บอกว่า ไม่สามารถหาเครื่องอ่านบัตรเจอ
โดย error นี้มาจากฟั่งชั่นใน Code คือ context.ListReaders()
4.2 รันโดยเสียบเครื่องอ่านบัตรแต่ยังไม่เสียบ บัตรประชาชน เข้าไปด้วย
รันคำสั่ง
ผลลัพธ์ ตรงเส้นใต้สีเหลืองจะเห็นแล้วว่า หลังจากที่เราเสียบเครื่องอ่านบัตร เราได้ใช้เครื่องรุ่นอะไร และเส้นใต้สีแดงเป็นการแจ้งว่าบัตรไม่ได้ถูกเสียบกับเครื่องอ่านบัตรอยู่
4.3 รันโดยเสียบเครื่องอ่านบัตร แต่เราจะเสียบบัตรอื่นที่ไม่ใช่บัตรประชาชนเข้าไปครับ ตัวอย่างนี้ เสียบบัตรอื่นเข้าไปแทน หรือจะกลับด้านเอาด้านที่ไม่มี chip เสียบเข้าไปแทน
รันคำสั่ง
ผลลัพธ์ เราจะเห็นว่าโปรแกรมจะแจ้ง error บอกว่าไม่สามารถส่ง request ไปขอข้อมูลที่ chip ของบัตรได้
โดยในข้อ 4.2 – 4.3 ฟั่งชั่น context.Connect(reader, scard.ShareShared, scard.ProtocolAny) จะเป็นฟั่งชั่นของการอ่านบัตร
4.4 รันโดยเสียบเครื่องอ่านบัตรและเสียบบัตรประชาชนเข้าไปด้วย และทดลองเอาบัตรอื่นเสียบที่เครื่อง
รันคำสั่ง
ผลลัพธ์ เราจะเห็นว่าโปรแกรมอ่านบัตรเราเจอ และส่งเลข APDU ว่าเป็นบัตรประชาชนจริง ๆ
โดยฟั่งชั่น selectRsp, err := sscard.APDUGetRsp(card, sscard.APDUThaiIDCardSelect) จะเป็นตัวอย่างตัวเริ่ม return response ของบัตรแล้วการดึงข้อมูลแต่ละฟิลจะดึงยังไงเดี๋ยวเราไปดูในหัวข้อถัดไปกัน
5. เพิ่ม code ในส่วนของการอ่านข้อมูลบัตร ใส่ลง ในไฟล์ exampleThaiIDCard.go
5.1 เพิ่มฟั่งชั่น ConvertTIS620toUTF8(tis620 string) คือการแปลง TIS620 ก่อนเพราะถ้าเราดึง Field ตอน return ค่าออกมามันจะแถมอักขระพิเศษทั้งมองเห็นและไม่เห็น และจะเพี้ยนเมื่อนำไปใช้ เราจึงต้องลบมันออก
func ConvertTIS620toUTF8(tis620 string) (valueUTF8 string) {
tis620Valure := []byte(tis620) dec := charmap.Windows874.NewDecoder() makeUTF := make([]byte, len(tis620Valure)*3) n, _, err := dec.Transform(makeUTF, tis620Valure, false) if err != nil { return “This Filed can’t Convert Tis620 to UTF8” } valueUTF8 = string(makeUTF[:n]) valueUTF8 = strings.Trim(valueUTF8, “\u0000”) valueUTF8 = strings.ReplaceAll(valueUTF8, “#”, ” “)
valueUTF8 = strings.TrimSpace(valueUTF8) return } |
5.2 เพิ่ม code อ่านฟิล โค้ดเราจะใส่ไว้ในฟั่งชั่น exampleThaiIDCard()
ตัวอย่างเรายกมา 3 field
-
-
-
- CID เลขบัตรประชาชน
- fullnameEN ชื่อเต็ม ภาษาอังกฤษ
- fullnameTH ชื่อเต็ม ภาษาไทย
- birth วันเกิด
-
-
cid, err := sscard.APDUGetRsp(card, sscard.APDUThaiIDCardCID)
if err != nil { fmt.Println(“Error APDUGetRsp: “, err) return } fmt.Printf(“cid: _%s_\n”, string(cid))
fullnameEN, err := sscard.APDUGetRsp(card, sscard.APDUThaiIDCardFullnameEn) if err != nil { fmt.Println(“Error APDUGetRsp: “, err) return } fmt.Printf(“fullnameEN: _%s_\n”, ConvertTIS620toUTF8(string(fullnameEN)))
fullnameTH, err := sscard.APDUGetRsp(card, sscard.APDUThaiIDCardFullnameTh) fmt.Println(“len v = “, len(fullnameTH)) if err != nil { fmt.Println(“Error APDUGetRsp: “, err) return } fmt.Printf(“fullnameTH: _%s_\n”, ConvertTIS620toUTF8(string(fullnameTH)))
birth, err := sscard.APDUGetRsp(card, sscard.APDUThaiIDCardBirth) if err != nil { fmt.Println(“Error APDUGetRsp: “, err) return } fmt.Printf(“birth: _%s_\n”, ConvertTIS620toUTF8(string(birth)))
|
6. รันโปรแกรมดูผลลัพธ์ที่ดึงค่าฟิลที่เราระบุไว้
ผลลัพธ์ เราจะเห็นค่า Field ที่ return แล้วในบัตรประชาชน ตรง fullnameTH เราอย่างพึ่งตกใจไปทำไมเป็นข้อความไม่สมประกอบ เหตุผลคือมันแสดงในหน้าจอของ terminal ด้วยภาษาไทย แต่ถ้าเรา copy คำนั้นใส่ word หรือ notepad เราก็จะเห็นข้อความภาษาไทยปกติ
- มันดึงข้อมูลจาก field ต่าง ๆ ยังไงกัน
การดึงข้อมูลจาก chip ของบัตรประชาชน อธิบายหลักการง่าย ๆ คือ
1)เราจะส่ง request ระบุไปก่อนว่าจะดึง Field ไหน
2) เป็นจังหวะเราส่ง request ขอ response จาก Feild นั้นออกมา
โดยข้อมูลที่ส่งไปจะเป็นค่า hex ซึ่งใน Library sscard มีชุดคำสั่งค่า hex แต่ละ Field ไว้ให้แล้วสะดวกมาก ซึ่งอยู่ในไฟล์ apducmd_thidcard.go ของ Library
ตัวอย่างตัวแปลชุดคำสั่งดึง Field CID
แล้วเราจะรู้ที่มาที่ไปของเลข hax บัตรประชาชนไทยได้อย่างไร ผมขออนุญาติแนบบทความเกี่ยวกับเนื้อหานี้เพิ่มเติมนะครับ >>> Smart card: บัตรประชาชนยุคดิจิตอล | by patda9 | horganice | Medium
ตอนนี้เราพอรู้แล้วว่าแต่ละ Field เราจะดึงข้อมูลออกมาอย่างไร หลังจากนั้นเราค่อยไปเพิ่มในส่วน Field ที่อยากได้มาแสดงตามความต้องการ และต่อมาเราจะดึงรูปภาพของเราจากบัตรประชาชนกันครับ
8. การดึงรูปภาพจากบัตรประชาชน
8.1 ใส่โค้ด การดึงรูปจากบัตรประชาชน ใส่ในฟังชั่น exampleThaiIDCard()
cardPhotoJpg, err := sscard.APDUGetBlockRsp(card, sscard.APDUThaiIDCardPhoto, sscard.APDUThaiIDCardPhotoRsp)
if err != nil { fmt.Println(“Error: “, err) return }
//Write Image n2, err := sscard.WriteBlockToFile(cardPhotoJpg, “./idcPhoto.jpg”) if err != nil { fmt.Println(“Error WriteBlockToFile: “, err) return } fmt.Printf(“Img wrote %d bytes\n”, n2) |
ผลลัพธ์ ไฟล์รูปจะถูก save ไว้ที่โปรเจ็คของเรา ถ้าเรามาดูตรง Terminal เราจะเห็นว่ารูปภาพเราเขียนลงไปกี่ bytes
โดย sscard.WriteBlockToFile(cardPhotoJpg, “./idcPhoto.jpg”) จะประกอบด้วย Parameter 2 คือ 1.ตัวที่รับ Response จากฟั่งชั่นดึงรูป 2. กำหนด paht ที่เราจะ Save รูปลง กำหนดนามสกุลไฟล์ให้มันด้วย
9. Build ไฟล์ .exe ของโปรเจ็ค
รันคำสั่ง
go build |
ผลลัพธ์ เราจะได้ไฟล์ go-scan-thai-card.exe ที่โปรเจ็คของเราครับ
ที่กล่าวข้างต้นเราได้ทดสอบการใช้เครื่องสแกนบัตร และดึงข้อมูลจากบัตรจาก Field ต่าง ๆ ถ้าไม่เคยเล่นคำสั่ง ADPU จะเห็นว่าการส่งขอข้อมูลเป็นเทคนิคเฉพาะ โดยต้องรู้เกี่ยวกับเลขฐาน(hex) ด้วย และตอนนี้เราได้โปรแกรมที่นำไปใช้งานได้แล้ว แต่ยังมีการนำโปรแกรมไปใช้งานในจุดประสงค์อื่น ๆ เช่นนำไปรันไว้ใน server หรือเราจะสร้าง service ให้รันในเครื่อง ส่วนตรงนี้เราจะเปลี่ยนโปรแกรมธรรมดาให้เป็น service เดี๋ยวเราจะมาสร้าง service กันต่อในบทความต่อไปกันครับ 🙂
ทั้งนี้ ทางเราได้อัพโหลดโปรเจ็คนี้ขึ้นบน github ท่านที่สนใจสามารถสอบถามรายละเอียดเพิ่มเติมได้ที่ marketing@stream.co.th
สุดท้ายนี้ทางทีมงานสตรีมฯ ของเรายังมี โซลูชั่น เทคโนโลยี นวัตกรรม ด้านดิจิทัลอื่น ๆ อีกมากมาย ครอบคลุมเกือบทุกอุตสาหกรรม รอติดตามบทความต่อๆไปได้เลยครับ
เรียบเรียงโดย Piyapat Khajornpan
Stream I.T. Consulting Ltd.
Leave a Reply