REST Security ด้วย JWT โดยใช้ Java และ Spring Security

เผยแพร่แล้ว: 2022-03-11

ความปลอดภัย

ความปลอดภัยเป็นศัตรูของความสะดวกสบาย และในทางกลับกัน คำชี้แจงนี้เป็นจริงสำหรับระบบใดๆ ทั้งแบบเสมือนหรือจริง ตั้งแต่ทางเข้าบ้านจริงไปจนถึงแพลตฟอร์มการธนาคารทางเว็บ วิศวกรมักจะพยายามค้นหาจุดสมดุลที่เหมาะสมสำหรับกรณีการใช้งานที่กำหนด โดยเอนไปด้านใดด้านหนึ่ง โดยปกติ เมื่อมีภัยคุกคามใหม่ปรากฏขึ้น เราจะย้ายไปยังความปลอดภัยและหลีกหนีจากความสะดวก จากนั้นเราจะดูว่าเราสามารถกู้คืนความสะดวกที่สูญเสียไปโดยไม่ลดความปลอดภัยมากเกินไปหรือไม่ ยิ่งกว่านั้น วงจรอุบาทว์นี้จะดำเนินต่อไปตลอดกาล

กวดวิชาความปลอดภัยสปริง: ภาพประกอบความปลอดภัยเทียบกับความสะดวกสบาย

ความปลอดภัยเป็นศัตรูของความสะดวกสบาย และในทางกลับกัน
ทวีต

มาลองตรวจสอบสถานะของความปลอดภัย REST กันวันนี้ โดยใช้บทช่วยสอนความปลอดภัย Spring ที่ตรงไปตรงมาเพื่อสาธิตการใช้งานจริง

บริการ REST (ซึ่งย่อมาจาก Representational State Transfer) เริ่มต้นจากวิธีการที่ง่ายมากสำหรับ Web Services ที่มีข้อกำหนดขนาดใหญ่และรูปแบบที่ยุ่งยาก เช่น WSDL สำหรับการอธิบายบริการ หรือ SOAP สำหรับการระบุรูปแบบข้อความ ใน REST เราไม่มีสิ่งเหล่านี้ เราสามารถอธิบายบริการ REST ในไฟล์ข้อความธรรมดาและใช้รูปแบบข้อความใดก็ได้ที่เราต้องการ เช่น JSON, XML หรือแม้แต่ข้อความธรรมดาอีกครั้ง แนวทางแบบง่ายถูกนำไปใช้กับความปลอดภัยของบริการ REST เช่นกัน ไม่มีมาตรฐานที่กำหนดไว้กำหนดวิธีการเฉพาะในการตรวจสอบผู้ใช้

แม้ว่าบริการ REST จะไม่ได้ระบุไว้มากนัก แต่สิ่งสำคัญคือการขาดสถานะ หมายความว่าเซิร์ฟเวอร์ไม่เก็บสถานะไคลเอนต์ใด ๆ โดยมีเซสชันเป็นตัวอย่างที่ดี ดังนั้น เซิร์ฟเวอร์จะตอบกลับคำขอแต่ละรายการราวกับว่าเป็นคำขอแรกที่ไคลเอ็นต์สร้างขึ้น อย่างไรก็ตาม แม้กระทั่งตอนนี้ การใช้งานจำนวนมากยังคงใช้การรับรองความถูกต้องตามคุกกี้ ซึ่งสืบทอดมาจากการออกแบบสถาปัตยกรรมเว็บไซต์มาตรฐาน แนวทางการไร้สัญชาติของ REST ทำให้เซสชันคุกกี้ไม่เหมาะสมจากจุดยืนด้านความปลอดภัย แต่ถึงกระนั้นก็ยังมีการใช้กันอย่างแพร่หลาย นอกจากการเพิกเฉยต่อภาวะไร้สัญชาติที่จำเป็นแล้ว แนวทางที่ง่ายขึ้นยังเป็นการแลกเปลี่ยนด้านความปลอดภัยที่คาดไว้อีกด้วย เมื่อเทียบกับมาตรฐาน WS-Security ที่ใช้สำหรับ Web Services การสร้างและใช้บริการ REST ทำได้ง่ายกว่ามาก ความสะดวกจึงเข้ามาแทนที่ การแลกเปลี่ยนคือการรักษาความปลอดภัยที่บางเฉียบ การจี้เซสชันและการปลอมแปลงคำขอข้ามไซต์ (XSRF) เป็นปัญหาด้านความปลอดภัยที่พบบ่อยที่สุด

ในการพยายามกำจัดเซสชันไคลเอ็นต์จากเซิร์ฟเวอร์นั้น มีการใช้วิธีการอื่นๆ เป็นครั้งคราว เช่น การพิสูจน์ตัวตน HTTP พื้นฐานหรือไดเจสต์ ทั้งสองใช้ส่วนหัว Authorization เพื่อส่งข้อมูลรับรองผู้ใช้ โดยเพิ่มการเข้ารหัส (HTTP Basic) หรือการเข้ารหัส (HTTP Digest) แน่นอน พวกเขามีข้อบกพร่องเหมือนกันที่พบในเว็บไซต์: ต้องใช้ HTTP Basic ผ่าน HTTPS เนื่องจากชื่อผู้ใช้และรหัสผ่านถูกส่งไปในการเข้ารหัส base64 ที่ย้อนกลับได้ง่าย และ HTTP Digest บังคับให้ใช้การแฮช MD5 ที่ล้าสมัยซึ่งพิสูจน์แล้วว่าไม่ปลอดภัย

สุดท้าย การใช้งานบางอย่างใช้โทเค็นโดยพลการเพื่อรับรองความถูกต้องของไคลเอ็นต์ ตัวเลือกนี้ดูเหมือนจะดีที่สุดที่เรามีในตอนนี้ หากใช้งานอย่างถูกต้อง จะแก้ไขปัญหาด้านความปลอดภัยทั้งหมดของ HTTP Basic, HTTP Digest หรือคุกกี้เซสชัน ใช้งานง่าย และเป็นไปตามรูปแบบไร้สัญชาติ

อย่างไรก็ตาม ด้วยโทเค็นโดยพลการดังกล่าว จึงมีมาตรฐานเพียงเล็กน้อยที่เกี่ยวข้อง ผู้ให้บริการทุกรายมีความคิดของตนเองว่าจะใส่อะไรลงในโทเค็น และวิธีการเข้ารหัสหรือเข้ารหัส การใช้บริการจากผู้ให้บริการหลายรายต้องใช้เวลาในการตั้งค่าเพิ่มเติม เพียงเพื่อปรับให้เข้ากับรูปแบบโทเค็นเฉพาะที่ใช้ ในทางกลับกัน วิธีอื่นๆ (คุกกี้เซสชัน, HTTP Basic และ HTTP Digest) เป็นที่รู้จักกันดีสำหรับนักพัฒนา และเบราว์เซอร์เกือบทั้งหมดบนอุปกรณ์ทั้งหมดใช้งานได้ตั้งแต่แกะกล่อง กรอบงานและภาษาพร้อมสำหรับวิธีการเหล่านี้ โดยมีฟังก์ชันในตัวเพื่อจัดการกับแต่ละวิธีอย่างราบรื่น

การตรวจสอบ JWT

JWT (ย่อมาจาก JSON Web Token) เป็นมาตรฐานที่ขาดหายไปสำหรับการใช้โทเค็นเพื่อรับรองความถูกต้องบนเว็บโดยทั่วไป ไม่เพียงแต่สำหรับบริการ REST เท่านั้น ปัจจุบันอยู่ในสถานะร่างเป็น RFC 7519 มีความทนทานและสามารถบรรทุกข้อมูลได้มาก แต่ก็ยังใช้งานง่ายแม้ว่าขนาดจะค่อนข้างเล็ก เช่นเดียวกับโทเค็นอื่น ๆ JWT สามารถใช้เพื่อส่งต่อข้อมูลประจำตัวของผู้ใช้ที่ได้รับการรับรองความถูกต้องระหว่างผู้ให้บริการข้อมูลประจำตัวและผู้ให้บริการ (ซึ่งไม่จำเป็นต้องเป็นระบบเดียวกัน) นอกจากนี้ยังสามารถดำเนินการเรียกร้องทั้งหมดของผู้ใช้ เช่น ข้อมูลการอนุญาต ดังนั้นผู้ให้บริการจึงไม่จำเป็นต้องเข้าไปในฐานข้อมูลหรือระบบภายนอกเพื่อตรวจสอบบทบาทและสิทธิ์ของผู้ใช้สำหรับแต่ละคำขอ ข้อมูลที่ดึงมาจากโทเค็น

นี่คือวิธีที่การรักษาความปลอดภัย JWT ได้รับการออกแบบมาให้ทำงาน:

ภาพประกอบ JWT java flow

  • ลูกค้าเข้าสู่ระบบโดยส่งข้อมูลประจำตัวไปยังผู้ให้บริการข้อมูลประจำตัว
  • ผู้ให้บริการข้อมูลประจำตัวตรวจสอบข้อมูลประจำตัว หากทุกอย่างเรียบร้อย มันจะดึงข้อมูลผู้ใช้ สร้าง JWT ที่มีรายละเอียดผู้ใช้และการอนุญาตที่จะใช้ในการเข้าถึงบริการ และยังตั้งค่าการหมดอายุของ JWT (ซึ่งอาจไม่จำกัด)
  • ผู้ให้บริการข้อมูลประจำตัวจะลงนาม และหากจำเป็น จะเข้ารหัส JWT และส่งไปยังไคลเอ็นต์เพื่อตอบสนองต่อคำขอเริ่มต้นพร้อมข้อมูลประจำตัว
  • ลูกค้าเก็บ JWT ในระยะเวลาที่จำกัดหรือไม่จำกัด ขึ้นอยู่กับการหมดอายุที่กำหนดโดยผู้ให้บริการข้อมูลประจำตัว
  • ลูกค้าส่ง JWT ที่เก็บไว้ในส่วนหัวการอนุญาตสำหรับทุกคำขอไปยังผู้ให้บริการ
  • สำหรับแต่ละคำขอ ผู้ให้บริการจะนำ JWT จากส่วนหัว Authorization และถอดรหัส หากจำเป็น ตรวจสอบลายเซ็น และหากทุกอย่างเรียบร้อย จะดึงข้อมูลผู้ใช้และการอนุญาต จากข้อมูลนี้เพียงอย่างเดียวและอีกครั้งโดยไม่ต้องค้นหารายละเอียดเพิ่มเติมในฐานข้อมูลหรือติดต่อผู้ให้บริการข้อมูลประจำตัว มันสามารถยอมรับหรือปฏิเสธคำขอของลูกค้า ข้อกำหนดเพียงอย่างเดียวคือข้อมูลประจำตัวและผู้ให้บริการมีข้อตกลงในการเข้ารหัสเพื่อให้บริการสามารถตรวจสอบลายเซ็นหรือแม้กระทั่งถอดรหัสว่าข้อมูลประจำตัวใดถูกเข้ารหัส

โฟลว์นี้ช่วยให้มีความยืดหยุ่นสูงในขณะที่ยังคงรักษาสิ่งต่าง ๆ ให้ปลอดภัยและง่ายต่อการพัฒนา เมื่อใช้วิธีนี้ จะเป็นเรื่องง่ายที่จะเพิ่มโหนดเซิร์ฟเวอร์ใหม่ให้กับคลัสเตอร์ของผู้ให้บริการ โดยเริ่มต้นได้ด้วยความสามารถในการตรวจสอบลายเซ็นและถอดรหัสโทเค็นโดยการให้คีย์ลับที่ใช้ร่วมกันเท่านั้น ไม่จำเป็นต้องทำซ้ำเซสชัน การซิงโครไนซ์ฐานข้อมูล หรือการสื่อสารระหว่างโหนด พักผ่อนอย่างเต็มที่

ความแตกต่างที่สำคัญระหว่าง JWT และโทเค็นที่กำหนดเองอื่น ๆ คือการสร้างมาตรฐานของเนื้อหาของโทเค็น แนวทางที่แนะนำอีกวิธีหนึ่งคือการส่งโทเค็น JWT ในส่วนหัว Authorization โดยใช้รูปแบบ Bearer เนื้อหาของส่วนหัวควรมีลักษณะดังนี้:

 Authorization: Bearer <token>

การดำเนินการรักษาความปลอดภัย REST

เพื่อให้บริการ REST ทำงานได้ตามที่คาดไว้ เราจำเป็นต้องมีวิธีการอนุญาตที่แตกต่างกันเล็กน้อยเมื่อเทียบกับเว็บไซต์แบบคลาสสิกที่มีหลายหน้า

แทนที่จะทริกเกอร์กระบวนการตรวจสอบสิทธิ์โดยเปลี่ยนเส้นทางไปยังหน้าเข้าสู่ระบบเมื่อไคลเอนต์ร้องขอทรัพยากรที่ปลอดภัย เซิร์ฟเวอร์ REST จะตรวจสอบคำขอทั้งหมดโดยใช้ข้อมูลที่มีอยู่ในตัวคำขอเอง ซึ่งก็คือโทเค็น JWT ในกรณีนี้ หากการพิสูจน์ตัวตนล้มเหลว การเปลี่ยนเส้นทางก็ไม่สมเหตุสมผล REST API เพียงส่งการตอบกลับรหัส HTTP 401 (ไม่ได้รับอนุญาต) และลูกค้าควรรู้ว่าต้องทำอย่างไร ตัวอย่างเช่น เบราว์เซอร์จะแสดงไดนามิก div เพื่อให้ผู้ใช้สามารถระบุชื่อผู้ใช้และรหัสผ่านได้

ในทางกลับกัน หลังจากการตรวจสอบสิทธิ์สำเร็จในเว็บไซต์แบบคลาสสิกที่มีหลายหน้า ผู้ใช้จะถูกเปลี่ยนเส้นทางโดยใช้รหัส HTTP 301 (ย้ายอย่างถาวร) โดยปกติแล้วจะไปยังหน้าแรกหรือที่ดียิ่งกว่านั้น ไปยังหน้าที่ผู้ใช้ร้องขอในตอนแรกซึ่งถูกทริกเกอร์ กระบวนการรับรองความถูกต้อง ด้วย REST สิ่งนี้ไม่สมเหตุสมผลอีกครั้ง แต่เราจะดำเนินการตามคำร้องขอต่อไปราวกับว่าทรัพยากรไม่ปลอดภัยเลย ส่งคืนรหัส HTTP 200 (OK) และเนื้อหาการตอบสนองที่คาดไว้

ตัวอย่างความปลอดภัยของสปริง

Spring REST Security ด้วย JWT และ Java

ตอนนี้เรามาดูกันว่าเราจะใช้ REST API ที่ใช้โทเค็น JWT โดยใช้ Java และ Spring ได้อย่างไร ในขณะที่พยายามนำพฤติกรรมเริ่มต้นของ Spring Security กลับมาใช้ใหม่ที่เราสามารถทำได้

ตามที่คาดไว้ Spring Security framework มาพร้อมกับคลาสปลั๊กอินจำนวนมากที่จัดการกับกลไกการอนุญาต "เก่า": คุกกี้เซสชัน, HTTP Basic และ HTTP Digest อย่างไรก็ตาม มันขาดการสนับสนุนดั้งเดิมสำหรับ JWT และเราจำเป็นต้องทำให้มือสกปรกเพื่อให้มันใช้งานได้ สำหรับภาพรวมโดยละเอียดเพิ่มเติม คุณควรอ่านเอกสาร Spring Security อย่างเป็นทางการ

มาเริ่มกันที่ คำจำกัดความตัวกรอง Spring Security ปกติใน web.xml กัน:

 <filter> <filter-name>springSecurityFilterChain</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <filter-name>springSecurityFilterChain</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>

โปรดทราบว่าชื่อของตัวกรอง Spring Security จะต้องเป็น springSecurityFilterChain เพื่อให้การกำหนดค่า Spring ที่เหลือทำงานนอกกรอบ

ถัดมาคือการประกาศ XML ของ Spring beans ที่เกี่ยวข้องกับความปลอดภัย เพื่อให้ XML ง่ายขึ้น เราจะตั้งค่าเนมสเปซเริ่มต้นเป็นการ security โดยเพิ่ม xmlns="http://www.springframework.org/schema/security" ให้กับองค์ประกอบ XML รูท XML ที่เหลือมีลักษณะดังนี้:

 <global-method-security pre-post-annotations="enabled" /> (1) <http pattern="/api/login" security="none"/> (2) <http pattern="/api/signup" security="none"/> <http pattern="/api/**" entry-point-ref="restAuthenticationEntryPoint" create-session="stateless"> (3) <csrf disabled="true"/> (4) <custom-filter before="FORM_LOGIN_FILTER" ref="jwtAuthenticationFilter"/> (5) </http> <beans:bean class="com.toptal.travelplanner.security.JwtAuthenticationFilter"> (6) <beans:property name="authenticationManager" ref="authenticationManager" /> <beans:property name="authenticationSuccessHandler" ref="jwtAuthenticationSuccessHandler" /> (7) </beans:bean> <authentication-manager alias="authenticationManager"> <authentication-provider ref="jwtAuthenticationProvider" /> (8) </authentication-manager>
  • (1) ในบรรทัดนี้ เราเปิดใช้งานคำอธิบายประกอบ @PreFilter , @PreAuthorize , @PostFilter , @PostAuthorize บนสปริงบีนในบริบท
  • (2) เรากำหนดจุดสิ้นสุดการเข้าสู่ระบบและการลงทะเบียนเพื่อข้ามการรักษาความปลอดภัย แม้แต่ "นิรนาม" ก็ควรจะสามารถดำเนินการทั้งสองนี้ได้
  • (3) ต่อไป เรากำหนดสายการกรองที่ใช้กับคำขอทั้งหมดในขณะที่เพิ่มการกำหนดค่าที่สำคัญสองรายการ: การอ้างอิงจุดเริ่มต้นและการตั้งค่าการสร้างเซสชันเป็น stateless (เราไม่ต้องการให้เซสชันสร้างเพื่อความปลอดภัยในขณะที่เราใช้โทเค็นสำหรับแต่ละคำขอ) .
  • (4) เราไม่ต้องการการป้องกัน csrf เพราะโทเค็นของเรามีภูมิคุ้มกันต่อมัน
  • (5) ต่อไป เราเสียบตัวกรองการตรวจสอบสิทธิ์พิเศษของเราภายในห่วงโซ่ตัวกรองที่กำหนดไว้ล่วงหน้าของ Spring ก่อนตัวกรองการเข้าสู่ระบบแบบฟอร์ม
  • (6) ถั่วนี้เป็นการประกาศตัวกรองการตรวจสอบของเรา เนื่องจากกำลังขยาย AbstractAuthenticationProcessingFilter ของ Spring เราจึงต้องประกาศใน XML เพื่อเชื่อมโยงคุณสมบัติของมัน (การต่อสายอัตโนมัติไม่ทำงานที่นี่) เราจะอธิบายในภายหลังว่าตัวกรองทำอะไร
  • (7) ตัวจัดการความสำเร็จเริ่มต้นของ AbstractAuthenticationProcessingFilter ไม่ดีพอสำหรับวัตถุประสงค์ REST เนื่องจากจะเปลี่ยนเส้นทางผู้ใช้ไปยังหน้าความสำเร็จ นั่นคือเหตุผลที่เราตั้งของเราเองที่นี่
  • (8) ตัวกรองของเราใช้การประกาศของผู้ให้บริการที่สร้างขึ้นโดยการ authenticationManager ผู้จัดการเพื่อตรวจสอบสิทธิ์ผู้ใช้

ตอนนี้เรามาดูกันว่าเราปรับใช้คลาสเฉพาะที่ประกาศไว้ใน XML ด้านบนอย่างไร โปรดทราบว่าสปริงจะวางสายให้เรา เราเริ่มต้นด้วยสิ่งที่ง่ายที่สุด

RestAuthenticationEntryPoint.java

 public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { // This is invoked when user tries to access a secured REST resource without supplying any credentials // We should just send a 401 Unauthorized response because there is no 'login page' to redirect to response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"); } }

ตามที่อธิบายไว้ข้างต้น คลาสนี้เพิ่งส่งคืนรหัส HTTP 401 (ไม่ได้รับอนุญาต) เมื่อการรับรองความถูกต้องล้มเหลว แทนที่การเปลี่ยนเส้นทางเริ่มต้นของ Spring

JwtAuthenticationSuccessHandler.java

 public class JwtAuthenticationSuccessHandler implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { // We do not need to do anything extra on REST authentication success, because there is no page to redirect to } }

การแทนที่อย่างง่ายนี้จะลบการทำงานเริ่มต้นของการรับรองความถูกต้องที่สำเร็จ (เปลี่ยนเส้นทางไปยังหน้าแรกหรือหน้าอื่น ๆ ที่ผู้ใช้ร้องขอ) หากคุณสงสัยว่าเหตุใดเราจึงไม่จำเป็นต้องแทนที่ AuthenticationFailureHandler อาจเป็นเพราะการใช้งานเริ่มต้นจะไม่เปลี่ยนเส้นทางที่ใดก็ได้หากไม่ได้ตั้งค่า URL การเปลี่ยนเส้นทาง ดังนั้นเราจึงหลีกเลี่ยงการตั้งค่า URL ซึ่งดีพอ

JwtAuthenticationFilter.java

 public class JwtAuthenticationFilter extends AbstractAuthenticationProcessingFilter { public JwtAuthenticationFilter() { super("/**"); } @Override protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) { return true; } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { String header = request.getHeader("Authorization"); if (header == null || !header.startsWith("Bearer ")) { throw new JwtTokenMissingException("No JWT token found in request headers"); } String authToken = header.substring(7); JwtAuthenticationToken authRequest = new JwtAuthenticationToken(authToken); return getAuthenticationManager().authenticate(authRequest); } @Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { super.successfulAuthentication(request, response, chain, authResult); // As this authentication is in HTTP header, after success we need to continue the request normally // and return the response as if the resource was not secured at all chain.doFilter(request, response); } }

คลาสนี้เป็นจุดเริ่มต้นของกระบวนการตรวจสอบ JWT ของเรา ตัวกรองจะแยกโทเค็น JWT ออกจากส่วนหัวของคำขอและมอบหมายการพิสูจน์ตัวตนไปยัง AuthenticationManager ที่ฉีดเข้าไป หากไม่พบโทเค็น ข้อยกเว้นจะถูกส่งออกไปเพื่อหยุดการประมวลผลคำขอ เรายังจำเป็นต้องมีการแทนที่สำหรับการตรวจสอบสิทธิ์ที่สำเร็จ เนื่องจาก Spring flow เริ่มต้นจะหยุดการต่อสายของตัวกรองและดำเนินการเปลี่ยนเส้นทาง โปรดทราบว่าเราต้องการให้ chain ดำเนินการอย่างเต็มที่ รวมถึงสร้างการตอบสนองตามที่อธิบายไว้ข้างต้น

JwtAuthenticationProvider.java

 public class JwtAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { @Autowired private JwtUtil jwtUtil; @Override public boolean supports(Class<?> authentication) { return (JwtAuthenticationToken.class.isAssignableFrom(authentication)); } @Override protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { } @Override protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { JwtAuthenticationToken jwtAuthenticationToken = (JwtAuthenticationToken) authentication; String token = jwtAuthenticationToken.getToken(); User parsedUser = jwtUtil.parseToken(token); if (parsedUser == null) { throw new JwtTokenMalformedException("JWT token is not valid"); } List<GrantedAuthority> authorityList = AuthorityUtils.commaSeparatedStringToAuthorityList(parsedUser.getRole()); return new AuthenticatedUser(parsedUser.getId(), parsedUser.getUsername(), token, authorityList); } }

ในคลาสนี้ เราใช้ AuthenticationManager ที่เป็นค่าเริ่มต้นของ Spring แต่เราฉีดด้วย AuthenticationProvider ของเราเองซึ่งทำกระบวนการตรวจสอบสิทธิ์จริง ในการดำเนินการนี้ เราขยาย AbstractUserDetailsAuthenticationProvider ซึ่งต้องการให้เราส่งคืน UserDetails ตามคำขอการรับรองความถูกต้องเท่านั้น ในกรณีของเรา โทเค็น JWT ที่ห่อหุ้มในคลาส JwtAuthenticationToken หากโทเค็นไม่ถูกต้อง เราจะส่งข้อยกเว้น อย่างไรก็ตาม หากถูกต้องและถอดรหัสโดย JwtUtil ได้สำเร็จ เราจะดึงรายละเอียดผู้ใช้ (เราจะเห็นว่าเป็นอย่างไรในคลาส JwtUtil ) โดยไม่ต้องเข้าถึงฐานข้อมูลเลย ข้อมูลทั้งหมดเกี่ยวกับผู้ใช้ รวมถึงบทบาทของเขาหรือเธอ มีอยู่ในโทเค็นเอง

JwtUtil.java

 public class JwtUtil { @Value("${jwt.secret}") private String secret; /** * Tries to parse specified String as a JWT token. If successful, returns User object with username, id and role prefilled (extracted from token). * If unsuccessful (token is invalid or not containing all required user properties), simply returns null. * * @param token the JWT token to parse * @return the User object extracted from specified token or null if a token is invalid. */ public User parseToken(String token) { try { Claims body = Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); User u = new User(); u.setUsername(body.getSubject()); u.setId(Long.parseLong((String) body.get("userId"))); u.setRole((String) body.get("role")); return u; } catch (JwtException | ClassCastException e) { return null; } } /** * Generates a JWT token containing username as subject, and userId and role as additional claims. These properties are taken from the specified * User object. Tokens validity is infinite. * * @param u the user for which the token will be generated * @return the JWT token */ public String generateToken(User u) { Claims claims = Jwts.claims().setSubject(u.getUsername()); claims.put("userId", u.getId() + ""); claims.put("role", u.getRole()); return Jwts.builder() .setClaims(claims) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } }

ในที่สุด คลาส JwtUtil มีหน้าที่แยกวิเคราะห์โทเค็นเป็นวัตถุ User และสร้างโทเค็นจากวัตถุ User มันตรงไปตรงมาเพราะใช้ไลบรารี jjwt เพื่อทำงาน JWT ทั้งหมด ในตัวอย่างของเรา เราเพียงแค่เก็บชื่อผู้ใช้ ID ผู้ใช้ และบทบาทของผู้ใช้ในโทเค็น นอกจากนี้เรายังสามารถจัดเก็บสิ่งของตามอำเภอใจได้มากขึ้นและเพิ่มคุณสมบัติความปลอดภัยเพิ่มเติม เช่น การหมดอายุของโทเค็น การแยกวิเคราะห์โทเค็นใช้ใน AuthenticationProvider ดังที่แสดงด้านบน เมธอด generateToken() ถูกเรียกจากการเข้าสู่ระบบและลงชื่อสมัครใช้บริการ REST ซึ่งไม่มีความปลอดภัยและจะไม่ทริกเกอร์การตรวจสอบความปลอดภัยใดๆ หรือต้องการให้โทเค็นปรากฏในคำขอ ในท้ายที่สุด จะสร้างโทเค็นที่จะส่งคืนให้กับลูกค้าตามผู้ใช้

บทสรุป

แม้ว่าวิธีการรักษาความปลอดภัยแบบเก่าที่ได้มาตรฐาน (คุกกี้เซสชัน, HTTP Basic และ HTTP Digest) จะทำงานร่วมกับบริการ REST ได้เช่นกัน แต่ทั้งหมดก็มีปัญหาที่จะหลีกเลี่ยงได้โดยใช้มาตรฐานที่ดีกว่า JWT มาถึงทันเวลาพอดี และที่สำคัญที่สุดคือใกล้จะเป็นมาตรฐาน IETF แล้ว

จุดแข็งหลักของ JWT คือการจัดการการพิสูจน์ตัวตนผู้ใช้ในแบบไร้สัญชาติ ดังนั้นจึงสามารถปรับขนาดได้ ในขณะที่รักษาทุกอย่างให้ปลอดภัยด้วยมาตรฐานการเข้ารหัสที่ทันสมัย การจัดเก็บการอ้างสิทธิ์ (บทบาทผู้ใช้และการอนุญาต) ในโทเค็นนั้นสร้างประโยชน์มหาศาลในสถาปัตยกรรมระบบแบบกระจาย โดยที่เซิร์ฟเวอร์ที่ออกคำขอไม่มีสิทธิ์เข้าถึงแหล่งข้อมูลการตรวจสอบสิทธิ์