spring-boot-jwt/
│
├── src/main/java/
│ └── murraco
│ ├── configuration
│ │ └── SwaggerConfig.java
│ │
│ ├── controller
│ │ └── UserController.java
│ │
│ ├── dto
│ │ ├── UserDataDTO.java
│ │ └── UserResponseDTO.java
│ │
│ ├── exception
│ │ ├── CustomException.java
│ │ └── GlobalExceptionController.java
│ │
│ ├── model
│ │ ├── AppUserRole.java
│ │ └── AppUser.java
│ │
│ ├── repository
│ │ └── UserRepository.java
│ │
│ ├── security
│ │ ├── JwtTokenFilter.java
│ │ ├── JwtTokenFilterConfigurer.java
│ │ ├── JwtTokenProvider.java
│ │ ├── MyUserDetails.java
│ │ └── WebSecurityConfig.java
│ │
│ ├── service
│ │ └── UserService.java
│ │
│ └── JwtAuthServiceApp.java
│
├── src/main/resources/
│ └── application.yml
│
├── .gitignore
├── LICENSE
├── mvnw/mvnw.cmd
├── README.md
└── pom.xml
เพื่อเป็นการแนะนำพื้นหลัง เรามีการแนะนำที่ยอดเยี่ยม โดยได้รับความอนุเคราะห์จาก jwt.io ! ลองมาดูกัน:
JSON Web Token (JWT) เป็นมาตรฐานแบบเปิด (RFC 7519) ที่กำหนดวิธีที่กะทัดรัดและครบถ้วนในตัวเองสำหรับการส่งข้อมูลอย่างปลอดภัยระหว่างฝ่ายต่างๆ ในฐานะออบเจ็กต์ JSON ข้อมูลนี้สามารถตรวจสอบและเชื่อถือได้เนื่องจากมีการเซ็นชื่อแบบดิจิทัล JWT สามารถลงนามโดยใช้ความลับ (ด้วยอัลกอริธึม HMAC) หรือคู่คีย์สาธารณะ/ส่วนตัวโดยใช้ RSA
มาอธิบายแนวคิดบางประการของคำจำกัดความนี้เพิ่มเติม
กะทัดรัด : เนื่องจากมีขนาดเล็ก JWT จึงสามารถส่งผ่าน URL, พารามิเตอร์ POST หรือภายในส่วนหัว HTTP นอกจากนี้ขนาดที่เล็กลงยังหมายถึงการส่งข้อมูลที่รวดเร็ว
มีอยู่ในตัวเอง : เพย์โหลดมีข้อมูลที่จำเป็นทั้งหมดเกี่ยวกับผู้ใช้ หลีกเลี่ยงความจำเป็นในการสืบค้นฐานข้อมูลมากกว่าหนึ่งครั้ง
ต่อไปนี้เป็นสถานการณ์บางส่วนที่ JSON Web Tokens มีประโยชน์:
การรับรองความถูกต้อง : นี่เป็นสถานการณ์ทั่วไปที่สุดสำหรับการใช้ JWT เมื่อผู้ใช้เข้าสู่ระบบ แต่ละคำขอที่ตามมาจะรวม JWT ไว้ด้วย ซึ่งอนุญาตให้ผู้ใช้เข้าถึงเส้นทาง บริการ และทรัพยากรที่ได้รับอนุญาตด้วยโทเค็นนั้น Single Sign On เป็นคุณลักษณะที่ใช้ JWT กันอย่างแพร่หลายในปัจจุบัน เนื่องจากมีค่าใช้จ่ายเพียงเล็กน้อยและความสามารถในการใช้งานข้ามโดเมนต่างๆ ได้อย่างง่ายดาย
การแลกเปลี่ยนข้อมูล : JSON Web Tokens เป็นวิธีที่ดีในการส่งข้อมูลอย่างปลอดภัยระหว่างฝ่ายต่างๆ เนื่องจากสามารถลงนาม JWT ได้ เช่น การใช้คู่คีย์สาธารณะ/ส่วนตัว คุณจึงมั่นใจได้ว่าผู้ส่งคือสิ่งที่พวกเขาพูด นอกจากนี้ เนื่องจากลายเซ็นได้รับการคำนวณโดยใช้ส่วนหัวและเพย์โหลด คุณจึงสามารถตรวจสอบได้ว่าเนื้อหาไม่ได้ถูกดัดแปลง
JSON Web Tokens ประกอบด้วยสามส่วนที่คั่นด้วยจุด (.) ซึ่งได้แก่:
ดังนั้น โดยทั่วไปแล้ว JWT จะมีลักษณะดังนี้
xxxxx
. yyyyy
zzzzz
มาแยกส่วนต่างๆ กัน
ส่วนหัว
โดยทั่วไปส่วนหัวจะประกอบด้วยสองส่วน: ประเภทของโทเค็นซึ่งก็คือ JWT และอัลกอริทึมการแฮชที่ใช้ เช่น HMAC SHA256 หรือ RSA
ตัวอย่างเช่น:
{
"alg" : " HS256 " ,
"typ" : " JWT "
}
จากนั้น JSON นี้จะถูกเข้ารหัส Base64Url เพื่อสร้างส่วนแรกของ JWT
เพย์โหลด
ส่วนที่สองของโทเค็นคือเพย์โหลด ซึ่งมีการอ้างสิทธิ์ การอ้างสิทธิ์คือข้อความเกี่ยวกับเอนทิตี (โดยทั่วไปคือผู้ใช้) และข้อมูลเมตาเพิ่มเติม การเรียกร้องมีสามประเภท: การเรียกร้องที่สงวนไว้ การเรียกร้องสาธารณะ และการเรียกร้องส่วนตัว
โปรดสังเกตว่าชื่อการอ้างสิทธิ์มีความยาวเพียงสามอักขระเนื่องจาก JWT มีขนาดกะทัดรัด
การเรียกร้องสาธารณะ : สามารถกำหนดสิ่งเหล่านี้ได้ตามต้องการโดยผู้ที่ใช้ JWT แต่เพื่อหลีกเลี่ยงการชนกัน ควรกำหนดไว้ใน IANA JSON Web Token Registry หรือกำหนดเป็น URI ที่มีเนมสเปซป้องกันการชนกัน
การเรียกร้องส่วนตัว : เป็นการเรียกร้องแบบกำหนดเองที่สร้างขึ้นเพื่อแบ่งปันข้อมูลระหว่างฝ่ายต่างๆ ที่ตกลงที่จะใช้การเรียกร้องเหล่านี้
ตัวอย่างของเพย์โหลดอาจเป็น:
{
"sub" : " 1234567890 " ,
"name" : " John Doe " ,
"admin" : true
}
เพย์โหลดจะถูกเข้ารหัส Base64Url เพื่อสร้างส่วนที่สองของ JSON Web Token
ลายเซ็น
ในการสร้างส่วนลายเซ็น คุณต้องนำส่วนหัวที่เข้ารหัส เพย์โหลดที่เข้ารหัส ข้อมูลลับ อัลกอริธึมที่ระบุในส่วนหัว และลงนามนั้น
ตัวอย่างเช่น หากคุณต้องการใช้อัลกอริทึม HMAC SHA256 ลายเซ็นจะถูกสร้างขึ้นในลักษณะต่อไปนี้:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
ลายเซ็นนี้ใช้เพื่อตรวจสอบว่าผู้ส่ง JWT เป็นใคร และเพื่อให้แน่ใจว่าข้อความจะไม่มีการเปลี่ยนแปลงไปพร้อมกัน รวบรวมทั้งหมดเข้าด้วยกัน
เอาต์พุตคือสตริง Base64 สามสตริงคั่นด้วยจุดที่สามารถส่งผ่านได้อย่างง่ายดายในสภาพแวดล้อม HTML และ HTTP ในขณะที่มีขนาดเล็กกว่าเมื่อเปรียบเทียบกับมาตรฐานที่ใช้ XML เช่น SAML
ข้อมูลต่อไปนี้แสดง JWT ที่มีส่วนหัวและเพย์โหลดที่เข้ารหัสก่อนหน้านี้ และมีการเซ็นชื่อด้วยความลับ JWT ที่เข้ารหัส
ในการตรวจสอบสิทธิ์ เมื่อผู้ใช้เข้าสู่ระบบโดยใช้ข้อมูลประจำตัวได้สำเร็จ JSON Web Token จะถูกส่งคืนและจะต้องบันทึกไว้ในเครื่อง (โดยทั่วไปจะอยู่ในที่จัดเก็บในตัวเครื่อง แต่สามารถใช้คุกกี้ได้เช่นกัน) แทนที่จะเป็นวิธีดั้งเดิมในการสร้างเซสชันใน เซิร์ฟเวอร์และส่งคืนคุกกี้
เมื่อใดก็ตามที่ผู้ใช้ต้องการเข้าถึงเส้นทางหรือทรัพยากรที่ได้รับการป้องกัน ตัวแทนผู้ใช้ควรส่ง JWT ซึ่งโดยทั่วไปจะอยู่ในส่วนหัวการอนุญาตโดยใช้สคีมา Bearer เนื้อหาของส่วนหัวควรมีลักษณะดังนี้:
Authorization: Bearer <token>
นี่เป็นกลไกการตรวจสอบสิทธิ์แบบไร้สถานะ เนื่องจากสถานะผู้ใช้จะไม่ถูกบันทึกลงในหน่วยความจำเซิร์ฟเวอร์ เส้นทางที่ได้รับการป้องกันของเซิร์ฟเวอร์จะตรวจสอบ JWT ที่ถูกต้องในส่วนหัวการอนุญาต และหากมีอยู่ ผู้ใช้จะได้รับอนุญาตให้เข้าถึงทรัพยากรที่ได้รับการป้องกัน เนื่องจาก JWT มีอยู่ในตัวเอง ข้อมูลที่จำเป็นทั้งหมดจึงอยู่ที่นั่น ช่วยลดความจำเป็นในการสืบค้นฐานข้อมูลหลายครั้ง
วิธีนี้ช่วยให้คุณพึ่งพา API ข้อมูลที่ไม่เก็บสถานะได้อย่างเต็มที่ และยังส่งคำขอไปยังบริการดาวน์สตรีมได้ด้วย ไม่ว่าโดเมนใดจะให้บริการ API ของคุณ ดังนั้น Cross-Origin Resource Sharing (CORS) จะไม่เป็นปัญหาเนื่องจากไม่ได้ใช้คุกกี้
แผนภาพต่อไปนี้แสดงกระบวนการนี้:
สคีมาการรับรองความถูกต้องที่ใช้โทเค็นได้รับความนิยมอย่างมากในช่วงไม่กี่ครั้งที่ผ่านมา เนื่องจากให้ประโยชน์ที่สำคัญเมื่อเปรียบเทียบกับเซสชัน/คุกกี้:
ต้องมีการแลกเปลี่ยนบางอย่างด้วยวิธีนี้:
ขั้นตอนการตรวจสอบสิทธิ์ JWT นั้นง่ายมาก
สิ่งสำคัญคือต้องทราบว่าการอ้างสิทธิ์จะรวมอยู่ในโทเค็นการเข้าถึง ทำไมสิ่งนี้ถึงสำคัญ? สมมติว่าการอ้างสิทธิ์การอนุญาต (เช่น สิทธิ์ของผู้ใช้ในฐานข้อมูล) มีการเปลี่ยนแปลงตลอดช่วงชีวิตของโทเค็นการเข้าถึง การเปลี่ยนแปลงเหล่านั้นจะไม่มีผลจนกว่าจะมีการออกโทเค็นการเข้าถึงใหม่ ในกรณีส่วนใหญ่ นี่ไม่ใช่ปัญหาใหญ่ เนื่องจากโทเค็นการเข้าถึงมีอายุสั้น มิฉะนั้นให้ใช้รูปแบบโทเค็นทึบแสง
มาดูกันว่าเราจะใช้การรับรองความถูกต้องตามโทเค็น JWT โดยใช้ Java และ Spring ได้อย่างไร ในขณะที่พยายามนำลักษณะการทำงานเริ่มต้นความปลอดภัยของ Spring มาใช้ซ้ำในที่ที่เราสามารถทำได้ เฟรมเวิร์ก Spring Security มาพร้อมกับคลาสปลั๊กอินที่จัดการกับกลไกการอนุญาตอยู่แล้ว เช่น คุกกี้เซสชัน, HTTP Basic และ HTTP Digest อย่างไรก็ตาม ยังขาดการสนับสนุนจาก JWT และเราจำเป็นต้องทำให้มือของเราสกปรกเพื่อให้มันใช้งานได้
ขณะนี้การสาธิตนี้ใช้ฐานข้อมูล H2 ชื่อ test_db เพื่อให้คุณสามารถรันได้อย่างรวดเร็วและพร้อมใช้งานได้ทันทีโดยไม่ต้องกำหนดค่าอะไรมากมาย หากคุณต้องการเชื่อมต่อกับฐานข้อมูลอื่น คุณต้องระบุการเชื่อมต่อในไฟล์ application.yml
ภายในไดเร็กทอรีทรัพยากร โปรดทราบว่า hibernate.hbm2ddl.auto=create-drop
จะดรอปและสร้างฐานข้อมูลใหม่ทุกครั้งที่เราปรับใช้ (คุณอาจต้องการเปลี่ยนหากคุณใช้สิ่งนี้ในโปรเจ็กต์จริง) นี่คือตัวอย่างจากโปรเจ็กต์ ดูว่าคุณสามารถสลับความคิดเห็นเกี่ยวกับคุณสมบัติ url
และ dialect
เพื่อใช้ฐานข้อมูล MySQL ของคุณได้อย่างง่ายดายเพียงใด:
spring :
datasource :
url : jdbc:h2:mem:test_db;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
# url: jdbc:mysql://localhost:3306/user_db
username : root
password : root
tomcat :
max-wait : 20000
max-active : 50
max-idle : 20
min-idle : 15
jpa :
hibernate :
ddl-auto : create-drop
properties :
hibernate :
dialect : org.hibernate.dialect.H2Dialect
# dialect: org.hibernate.dialect.MySQL8Dialect
format_sql : true
id :
new_generator_mappings : false
JwtTokenFilter
JwtTokenFilterConfigurer
JwtTokenProvider
MyUserDetails
WebSecurityConfig
JwtTokenFilter
ตัวกรอง JwtTokenFilter
ถูกนำไปใช้กับแต่ละ API ( /**
) ยกเว้นจุดสิ้นสุดโทเค็นการลงชื่อเข้าใช้ ( /users/signin
) และจุดสิ้นสุด singup ( /users/signup
)
ตัวกรองนี้มีหน้าที่ดังต่อไปนี้:
JwtTokenProvider
มิฉะนั้นจะส่งข้อยกเว้นการรับรองความถูกต้อง โปรดตรวจสอบให้แน่ใจว่า chain.doFilter(request, response)
ถูกเรียกใช้เมื่อการตรวจสอบสิทธิ์สำเร็จ คุณต้องการประมวลผลคำขอเพื่อเลื่อนไปยังตัวกรองถัดไป เนื่องจากตัวกรองตัวสุดท้าย FilterSecurityInterceptor#doFilter มีหน้าที่รับผิดชอบในการเรียกใช้เมธอดในคอนโทรลเลอร์ของคุณที่จัดการทรัพยากร API ที่ร้องขอ
String token = jwtTokenProvider . resolveToken (( HttpServletRequest ) req );
if ( token != null && jwtTokenProvider . validateToken ( token )) {
Authentication auth = jwtTokenProvider . getAuthentication ( token );
SecurityContextHolder . getContext (). setAuthentication ( auth );
}
filterChain . doFilter ( req , res );
JwtTokenFilterConfigurer
เพิ่ม JwtTokenFilter
ให้กับ DefaultSecurityFilterChain
ของการรักษาความปลอดภัยการบูตสปริง
JwtTokenFilter customFilter = new JwtTokenFilter ( jwtTokenProvider );
http . addFilterBefore ( customFilter , UsernamePasswordAuthenticationFilter . class );
JwtTokenProvider
JwtTokenProvider
มีหน้าที่ดังต่อไปนี้:
รายละเอียดผู้ใช้ของฉัน
ใช้ UserDetailsService
เพื่อกำหนดฟังก์ชัน loadUserbyUsername ที่เรากำหนดเอง อินเทอร์เฟซ UserDetailsService
ใช้เพื่อดึงข้อมูลที่เกี่ยวข้องกับผู้ใช้ มีวิธีการหนึ่งชื่อ loadUserByUsername ซึ่งค้นหาเอนทิตีผู้ใช้ตามชื่อผู้ใช้และสามารถเขียนทับได้เพื่อปรับแต่งกระบวนการค้นหาผู้ใช้
DaoAuthenticationProvider
ถูกใช้เพื่อโหลดรายละเอียดเกี่ยวกับผู้ใช้ระหว่างการตรวจสอบสิทธิ์
การกำหนดค่าความปลอดภัยบนเว็บ
คลาส WebSecurityConfig
ขยาย WebSecurityConfigurerAdapter
เพื่อจัดเตรียมการกำหนดค่าความปลอดภัยแบบกำหนดเอง
bean ต่อไปนี้ได้รับการกำหนดค่าและสร้างอินสแตนซ์ในคลาสนี้:
JwtTokenFilter
PasswordEncoder
นอกจากนี้ ภายในเมธอด WebSecurityConfig#configure(HttpSecurity http)
เราจะกำหนดค่ารูปแบบเพื่อกำหนดจุดสิ้นสุด API ที่มีการป้องกัน/ไม่มีการป้องกัน โปรดทราบว่าเราได้ปิดการใช้งานการป้องกัน CSRF เนื่องจากเราไม่ได้ใช้คุกกี้
// Disable CSRF (cross site request forgery)
http . csrf (). disable ();
// No session will be created or used by spring security
http . sessionManagement (). sessionCreationPolicy ( SessionCreationPolicy . STATELESS );
// Entry points
http . authorizeRequests () //
. antMatchers ( "/users/signin" ). permitAll () //
. antMatchers ( "/users/signup" ). permitAll () //
// Disallow everything else..
. anyRequest (). authenticated ();
// If a user try to access a resource without having enough permissions
http . exceptionHandling (). accessDeniedPage ( "/login" );
// Apply JWT
http . apply ( new JwtTokenFilterConfigurer ( jwtTokenProvider ));
// Optional, if you want to test the API from a browser
// http.httpBasic();
ตรวจสอบให้แน่ใจว่าคุณได้ติดตั้ง Java 8 และ Maven แล้ว
แยกที่เก็บนี้และโคลนมัน
$ git clone https://github.com/<your-user>/spring-boot-jwt
$ cd spring-boot-jwt
$ mvn install
$ mvn spring-boot:run
http://localhost:8080/swagger-ui.html
ในเบราว์เซอร์ของคุณเพื่อตรวจสอบว่าทุกอย่างทำงานอย่างถูกต้อง คุณสามารถเปลี่ยนพอร์ตเริ่มต้นในไฟล์ application.yml
server :
port : 8080
/users/me
เพื่อตรวจสอบว่าคุณไม่ได้รับการรับรองความถูกต้อง คุณควรได้รับการตอบกลับด้วย 403
พร้อมข้อความ Access Denied
เนื่องจากคุณยังไม่ได้ตั้งค่าโทเค็น JWT ที่ถูกต้อง $ curl -X GET http://localhost:8080/users/me
/users/signin
ด้วยผู้ดูแลระบบเริ่มต้นที่เราสร้างขึ้นโดยทางโปรแกรมเพื่อรับโทเค็น JWT ที่ถูกต้อง $ curl -X POST 'http://localhost:8080/users/signin?username=admin&password=admin'
/users/me
อีกครั้ง $ curl -X GET http://localhost:8080/users/me -H 'Authorization: Bearer <JWT_TOKEN>'
{
"id" : 1 ,
"username" : "admin" ,
"email" : "[email protected]" ,
"roles" : [
"ROLE_ADMIN"
]
}