รู้จักกับระบบยืนยันตัวตนผ่าน OIDC สำหรับมือใหม่
เวลาเราสร้างเว็บแอปพลิเคชัน สิ่งที่ขาดไม่ได้เลยคือระบบล็อกอิน (การยืนยันตัวตนเพื่อเข้าใช้งาน) แต่การเขียนระบบนี้เองตั้งแต่ต้นนั้นมีความเสี่ยงสูง โดยเฉพาะเรื่องความปลอดภัยที่ต้องดูแลอย่างเข้มงวด หลายคนจึงหันมาใช้ OpenID Connect (OIDC) ซึ่งเป็นโปรโตคอล (ชุดกฎระเบียบในการสื่อสาร) ที่ช่วยให้เราเชื่อมต่อกับผู้ให้บริการล็อกอินชื่อดังอย่าง Google หรือ Microsoft ได้เลยโดยไม่ต้องเก็บรหัสผ่านผู้ใช้ไว้ในฐานข้อมูลของเราเอง
สำหรับโปรแกรมเมอร์ที่ใช้งาน Jakarta EE (แพลตฟอร์มสำหรับสร้างเว็บแอปพลิเคชันระดับองค์กร) หลายคนอาจจะคิดว่าต้องใช้ Spring Boot เสมอถึงจะทำเรื่องนี้ได้ แต่นั่นไม่เป็นความจริงเลยครับ เราสามารถใช้เครื่องมือที่ชื่อว่า pac4j มาช่วยจัดการเรื่องความปลอดภัยในระดับ Servlet (ส่วนประกอบที่ทำหน้าที่ประมวลผลคำขอจากเว็บเบราว์เซอร์) ได้โดยตรง ซึ่งเบาและไม่ต้องตั้งค่าซับซ้อนเกินความจำเป็น
การเข้าใจกลไกนี้จะช่วยให้เราเป็นโปรแกรมเมอร์ที่ยืดหยุ่นขึ้น เพราะเราจะรู้ว่าระบบความปลอดภัยไม่ได้ขึ้นอยู่กับเฟรมเวิร์ก (โครงสร้างโปรแกรมสำเร็จรูป) เพียงอย่างเดียว แต่ขึ้นอยู่กับความเข้าใจในมาตรฐานการสื่อสารระหว่างแอปพลิเคชันของเรากับผู้ให้บริการยืนยันตัวตน หากเราเข้าใจจุดนี้ ต่อให้ในอนาคตเปลี่ยนไปใช้เทคโนโลยีอื่น เราก็ยังสามารถนำหลักการเดียวกันไปประยุกต์ใช้ได้เสมอครับ
เตรียมความพร้อมและติดตั้งไลบรารีที่จำเป็น
ก่อนจะเริ่มเขียนโค้ด เราต้องมีสภาพแวดล้อมที่รองรับก่อน โดยโปรเจกต์ของเราต้องใช้ Java 17 ขึ้นไป และใช้ Maven (เครื่องมือจัดการโปรเจกต์และดาวน์โหลดไฟล์เสริม) ในการจัดการไฟล์ทั้งหมด ส่วนฝั่งของเซิร์ฟเวอร์เราควรเลือกใช้ Servlet Container (โปรแกรมรันเว็บแอปพลิเคชัน) ที่รองรับมาตรฐาน Servlet 6.0 เช่น Tomcat 10.1 ขึ้นไป เพื่อให้ทำงานร่วมกับไลบรารีปัจจุบันได้ราบรื่น
ขั้นตอนแรกคือการเพิ่มไฟล์เสริม (Dependencies) เข้าไปในไฟล์ pom.xml ของเรา เพื่อดึงความสามารถของ jee-pac4j (ส่วนเชื่อมต่อสำหรับ Jakarta EE) และโมดูลสำหรับจัดการ OIDC มาใช้ การเพิ่มไฟล์เหล่านี้จะทำให้แอปพลิเคชันของเรามีเครื่องมือพร้อมสำหรับการรับส่งข้อมูลกับผู้ให้บริการภายนอกโดยไม่ต้องเขียนเองทั้งหมด
นี่คือตัวอย่างการเพิ่มไฟล์เสริมใน pom.xml ของโปรเจกต์ครับ
<dependencies>
<!-- ไลบรารีหลักสำหรับ Jakarta EE -->
<dependency>
<groupId>org.pac4j</groupId>
<artifactId>jakartaee-pac4j</artifactId>
<version>8.0.4</version>
</dependency>
<!-- โมดูลสำหรับจัดการ OIDC -->
<dependency>
<groupId>org.pac4j</groupId>
<artifactId>pac4j-oidc</artifactId>
<version>6.5.9</version>
</dependency>
</dependencies>
ในโค้ดข้างต้น ส่วน groupId คือชื่อกลุ่มของผู้พัฒนา ส่วน artifactId คือชื่อไลบรารีที่เราต้องการใช้งาน และ version คือเลขรุ่นของไลบรารี การเลือกใช้เลขรุ่นที่ถูกต้องจะช่วยลดปัญหาความเข้ากันไม่ได้ของซอฟต์แวร์ หลังจากเพิ่มโค้ดนี้แล้ว ให้สั่ง Build (กระบวนการรวบรวมไฟล์และตรวจสอบข้อผิดพลาด) โปรเจกต์ใหม่เพื่อให้ Maven ดาวน์โหลดไฟล์เหล่านี้ลงเครื่องของคุณ
ผลลัพธ์หลังจากทำขั้นตอนนี้เสร็จ คือแอปพลิเคชันของคุณจะมีคลาส (แม่แบบของวัตถุในภาษา Java) ต่างๆ จาก pac4j เข้ามาอยู่ในโปรเจกต์พร้อมให้เราเรียกใช้งาน หากคุณใช้ IDE (โปรแกรมที่ใช้เขียนโค้ด) อย่าง IntelliJ IDEA หรือ Eclipse ตัวโปรแกรมจะทำการดาวน์โหลดไฟล์เหล่านี้มาเก็บไว้ในโฟลเดอร์ Local Repository (ที่เก็บไฟล์เสริมในเครื่อง) ของคุณโดยอัตโนมัติครับ
สร้างการตั้งค่าความปลอดภัยด้วย SecurityConfigFactory
เมื่อเตรียมไฟล์พร้อมแล้ว ขั้นต่อไปคือการบอกแอปพลิเคชันว่าเราจะคุยกับใคร โดยเราต้องสร้างคลาสที่ทำหน้าที่เป็น ConfigFactory (โรงงานผลิตการตั้งค่า) ซึ่งจะทำหน้าที่สร้างการเชื่อมต่อกับผู้ให้บริการ OIDC เช่น Google หรือ Keycloak โดยคลาสนี้จะถูกเรียกใช้ตอนเริ่มต้นแอปพลิเคชันเพื่อเตรียมข้อมูลการล็อกอินไว้ให้ระบบ
เราต้องระบุข้อมูลสำคัญ 3 อย่าง ได้แก่ DiscoveryURI (ที่อยู่สำหรับดึงข้อมูลการตั้งค่าของผู้ให้บริการ), ClientId (รหัสประจำตัวแอปของเรา), และ Secret (รหัสลับที่ใช้ยืนยันความถูกต้อง) ข้อมูลเหล่านี้เปรียบเสมือนบัตรผ่านที่ทำให้ผู้ให้บริการยอมรับว่าแอปพลิเคชันของเราได้รับอนุญาตให้ขอข้อมูลผู้ใช้ได้จริง
นี่คือตัวอย่างโค้ดสำหรับการตั้งค่าครับ
public class SecurityConfigFactory implements ConfigFactory {
@Override
public Config build(final Object... parameters) {
// ตั้งค่าการเชื่อมต่อ OIDC
final var config = new OidcConfiguration()
.setDiscoveryURI("https://.../openid-configuration")
.setClientId("myclient")
.setSecret("mysecret");
// ส่งค่า Config กลับไปให้ระบบใช้งาน
return new Config("http://localhost:8080/callback", new OidcClient(config));
}
}
ในโค้ดส่วนนี้ setDiscoveryURI จะทำหน้าที่ไปดึงรายละเอียดจากผู้ให้บริการโดยอัตโนมัติ ทำให้เราไม่ต้องพิมพ์ค่าพอร์ตหรือที่อยู่ย่อยๆ เอง ส่วน new Config คือการสร้างจุดรับข้อมูลหลังจากล็อกอินเสร็จ โดยกำหนดให้ส่งกลับมาที่หน้า /callback ในแอปของเรา ซึ่งเป็นจุดสำคัญที่ต้องระบุให้ตรงกับที่ตั้งไว้ในระบบของผู้ให้บริการด้วย
ผลลัพธ์ที่ได้จากการทำขั้นตอนนี้ คือเราจะมีวัตถุ Config ที่บรรจุวิธีการยืนยันตัวตนไว้เรียบร้อยแล้ว หากมีการตั้งค่า ClientId หรือ Secret ผิดพลาด ระบบจะแจ้งเตือนตอนรันแอปพลิเคชัน ทำให้เราตรวจสอบได้ทันทีว่าการเชื่อมต่อกับผู้ให้บริการล็อกอินนั้นสำเร็จหรือไม่ก่อนที่จะเปิดให้ผู้ใช้งานจริงเข้ามาล็อกอินครับ
ลงทะเบียน Filter เพื่อปกป้องหน้าเว็บ
ตอนนี้เรามีวิธีการล็อกอินแล้ว แต่เรายังต้องบอกแอปพลิเคชันว่าหน้าไหนบ้างที่ต้องล็อกอินถึงจะเข้าได้ โดยเราจะใช้ Filter (ตัวกรองที่คอยดักจับคำขอจากผู้ใช้งาน) เข้ามาช่วยจัดการ Filter จะทำหน้าที่เหมือนยามเฝ้าประตู ถ้าใครเดินผ่านเข้ามาแล้วยังไม่ได้ล็อกอิน ยามจะเชิญให้ไปล็อกอินก่อนทันที
เราจะตั้งค่า Filter ผ่านไฟล์ web.xml ซึ่งเป็นไฟล์ตั้งค่าหลักของแอปพลิเคชัน Jakarta EE โดยเราจะประกาศยาม 3 คน คือ SecurityFilter (คอยตรวจว่าล็อกอินหรือยัง), CallbackFilter (คอยรับข้อมูลหลังจากล็อกอินผ่าน), และ LogoutFilter (คอยจัดการเรื่องการออกจากระบบ)
ตัวอย่างการตั้งค่าใน web.xml มีดังนี้ครับ
<filter>
<filter-name>securityFilter</filter-name>
<filter-class>org.pac4j.jee.filter.SecurityFilter</filter-class>
<init-param>
<param-name>configFactory</param-name>
<param-value>org.example.security.SecurityConfigFactory</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>securityFilter</filter-name>
<url-pattern>/protected/*</url-pattern>
</filter-mapping>
โค้ดส่วนนี้บอกให้ securityFilter ทำงานกับทุกไฟล์ที่อยู่ในโฟลเดอร์ /protected/ หากผู้ใช้พยายามเข้าถึงไฟล์เหล่านี้โดยไม่มีสิทธิ์ ยามจะดึง ConfigFactory ที่เราสร้างไว้ก่อนหน้ามาใช้งานเพื่อส่งผู้ใช้ไปยังหน้าล็อกอินของผู้ให้บริการ OIDC ทันที
เมื่อรันแอปพลิเคชันแล้ว ลองเข้าลิงก์ http://localhost:8080/protected/index.html ดูครับ หากทุกอย่างถูกต้อง เบราว์เซอร์จะเด้งไปหน้าล็อกอินของผู้ให้บริการทันที นี่คือสัญญาณว่ายามของเราทำงานได้ดีเยี่ยมแล้ว และถ้าล็อกอินสำเร็จ ระบบจะส่งคุณกลับมาที่หน้าเว็บเดิมที่คุณพยายามเข้าถึงในตอนแรกครับ
ข้อควรระวังสำหรับมือใหม่
จุดที่มือใหม่มักพลาดบ่อยที่สุดคือการลืมตั้งค่า Redirect URI (ที่อยู่เว็บที่อนุญาตให้ส่งข้อมูลกลับ) ในหน้าจัดการของผู้ให้บริการล็อกอิน หากคุณระบุในโค้ดว่าเป็น /callback แต่ในหน้าเว็บ Google หรือ Keycloak ไม่ได้ระบุไว้ ระบบจะปฏิเสธการเชื่อมต่อและแสดงข้อผิดพลาดทันที ซึ่งเป็นเรื่องที่น่าหงุดหงิดแต่แก้ไขง่ายมากครับ
อีกเรื่องคือการเลือกใช้ระหว่าง jakartaee-pac4j กับ javaee-pac4j หากคุณใช้ Java รุ่นใหม่ๆ ให้ยึดตามมาตรฐาน Jakarta EE (ใช้ชื่อแพ็กเกจขึ้นต้นด้วย jakarta.) เสมอ อย่าเผลอไปใช้ชื่อแพ็กเกจเก่าที่เป็น javax. เพราะจะทำให้โปรแกรมทำงานร่วมกันไม่ได้และเกิดข้อผิดพลาดตอนรัน (Runtime Error) ได้ง่าย
สุดท้ายคือเรื่องการจัดการ Session (ข้อมูลการใช้งานของผู้ใช้ที่เก็บไว้ชั่วคราว) อย่าลืมว่าเมื่อเราใช้ LogoutFilter แล้ว ระบบจะล้างข้อมูลในเซสชันให้เราโดยอัตโนมัติ หากคุณมีข้อมูลสำคัญที่ต้องเก็บไว้ในเซสชัน ให้มั่นใจว่าข้อมูลเหล่านั้นไม่จำเป็นต้องใช้อีกหลังจากผู้ใช้กดออกจากระบบแล้ว เพราะมันจะถูกทำลายทิ้งเพื่อความปลอดภัยครับ
สรุป: เริ่มต้นเส้นทางโปรแกรมเมอร์สายความปลอดภัย
การทำระบบล็อกอินด้วย OIDC ผ่าน pac4j ไม่ใช่เรื่องยากอย่างที่คิดครับ เพียงแค่เราเข้าใจว่ามันคือการส่งต่อความรับผิดชอบเรื่องการยืนยันตัวตนให้กับผู้ให้บริการที่น่าเชื่อถือ แล้วเราก็แค่รอรับผลลัพธ์กลับมาจัดการในแอปพลิเคชันของเรา การทำแบบนี้ช่วยให้แอปของคุณปลอดภัยขึ้นมาก และลดภาระในการดูแลรหัสผ่านของผู้ใช้งานลงได้มหาศาล
สำหรับคนที่กำลังฝึกเป็นโปรแกรมเมอร์ ผมแนะนำให้ลองทำโปรเจกต์เล็กๆ โดยเชื่อมต่อกับ Google หรือ Okta ดูครับ การได้ลองผิดลองถูกในขั้นตอนการตั้งค่า Redirect URI หรือการไล่ดู Logs (บันทึกการทำงานของระบบ) จะทำให้คุณเห็นภาพชัดเจนว่าข้อมูลวิ่งไปมาอย่างไร ซึ่งประสบการณ์นี้จะหาไม่ได้จากการนั่งอ่านตำราเพียงอย่างเดียวครับ
เมื่อคุณทำระบบล็อกอินนี้สำเร็จแล้ว ลองต่อยอดด้วยการเขียนโค้ดเพื่อดึงชื่อผู้ใช้มาแสดงผลบนหน้าเว็บดูครับ การนำข้อมูลจาก Profile (ข้อมูลส่วนตัวของผู้ใช้ที่ได้จาก OIDC) มาใช้ จะทำให้แอปของคุณดูเป็นมืออาชีพและพร้อมสำหรับการพัฒนาฟีเจอร์อื่นๆ ต่อไป ขอให้สนุกกับการเขียนโค้ดและพัฒนาตัวเองให้เก่งขึ้นทุกวันนะครับ
ที่มา: How to secure a Jakarta EE client application with OIDC (using pac4j) — DEV Community