portaldacalheta.pt
  • หลัก
  • การจัดการโครงการ
  • การเพิ่มขึ้นของระยะไกล
  • การบริหารโครงการ
  • เครื่องมือและบทช่วยสอน
ส่วนหลัง

Spring Security พร้อม JWT สำหรับ REST API



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

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



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



การกำหนดคำศัพท์

ก่อนที่จะเจาะลึกรายละเอียดทางเทคนิคฉันต้องการกำหนดคำศัพท์ที่ใช้ในบริบท Spring Security อย่างชัดเจนเพื่อให้แน่ใจว่าเราทุกคนพูดภาษาเดียวกัน



นี่คือข้อกำหนดที่เราต้องระบุ:

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

การสร้างแอปพลิเคชัน Spring พื้นฐาน

ก่อนที่จะย้ายไปที่การกำหนดค่า Spring Security framework มาสร้างเว็บแอปพลิเคชัน Spring พื้นฐานกัน สำหรับสิ่งนี้เราสามารถใช้ไฟล์ Spring Initializr และสร้างโครงการแม่แบบ สำหรับเว็บแอปพลิเคชันอย่างง่ายการพึ่งพา Spring web framework ก็เพียงพอแล้ว:



org.springframework.boot spring-boot-starter-web

เมื่อเราสร้างโครงการแล้วเราสามารถเพิ่มตัวควบคุม REST แบบธรรมดาได้ดังนี้:

@RestController @RequestMapping('hello') public class HelloRestController { @GetMapping('user') public String helloUser() { return 'Hello User'; } @GetMapping('admin') public String helloAdmin() { return 'Hello Admin'; } }

หลังจากนี้หากเราสร้างและดำเนินโครงการเราสามารถเข้าถึง URL ต่อไปนี้ในเว็บเบราว์เซอร์:



  • http://localhost:8080/hello/user จะส่งคืนสตริง Hello User.
  • http://localhost:8080/hello/admin จะส่งคืนสตริง Hello Admin.

ตอนนี้เราสามารถเพิ่ม Spring Security framework ให้กับโปรเจ็กต์ของเราได้แล้วและเราสามารถทำได้โดยเพิ่มการอ้างอิงต่อไปนี้ให้กับ pom.xml ของเรา ไฟล์:

org.springframework.boot spring-boot-starter-security

การเพิ่มการอ้างอิง Spring framework อื่น ๆ โดยปกติจะไม่ส่งผลทันทีกับแอปพลิเคชันจนกว่าเราจะจัดเตรียมการกำหนดค่าที่เกี่ยวข้อง แต่ Spring Security แตกต่างกันตรงที่จะมีผลทันทีและโดยปกติจะทำให้ผู้ใช้ใหม่สับสน หลังจากเพิ่มแล้วหากเราสร้างและเรียกใช้โปรเจ็กต์ใหม่จากนั้นพยายามเข้าถึงหนึ่งใน URL ดังกล่าวข้างต้นแทนที่จะดูผลลัพธ์เราจะเปลี่ยนเส้นทางไปที่ http://localhost:8080/login นี่เป็นลักษณะการทำงานเริ่มต้นเนื่องจากกรอบงาน Spring Security ต้องการการรับรองความถูกต้องนอกกรอบสำหรับ URL ทั้งหมด



ในการผ่านการรับรองความถูกต้องเราสามารถใช้ชื่อผู้ใช้เริ่มต้น user และค้นหารหัสผ่านที่สร้างขึ้นโดยอัตโนมัติในคอนโซลของเรา:

Using generated security password: 1fc15145-dfee-4bec-a009-e32ca21c77ce

โปรดจำไว้ว่ารหัสผ่านจะเปลี่ยนทุกครั้งที่เราเรียกใช้แอปพลิเคชันอีกครั้ง หากเราต้องการเปลี่ยนพฤติกรรมนี้และทำให้รหัสผ่านคงที่เราสามารถเพิ่มการกำหนดค่าต่อไปนี้ใน application.properties ของเราได้ ไฟล์:



spring.security.user.password=Test12345_

ตอนนี้ถ้าเราป้อนข้อมูลรับรองในแบบฟอร์มการเข้าสู่ระบบเราจะถูกเปลี่ยนเส้นทางกลับไปที่ URL ของเราและเราจะเห็นผลลัพธ์ที่ถูกต้อง โปรดทราบว่ากระบวนการรับรองความถูกต้องแบบสำเร็จรูปเป็นแบบเซสชันและหากเราต้องการออกจากระบบเราสามารถเข้าถึง URL ต่อไปนี้: http://localhost:8080/logout

การออกแบบกราฟิก vs วิจิตรศิลป์

ลักษณะการทำงานนอกกรอบนี้อาจมีประโยชน์สำหรับเว็บแอปพลิเคชัน MVC แบบคลาสสิกที่เรามีการรับรองความถูกต้องตามเซสชัน แต่ในกรณีของแอปพลิเคชันแบบหน้าเดียวมักจะไม่มีประโยชน์เนื่องจากในกรณีการใช้งานส่วนใหญ่เรามีฝั่งไคลเอ็นต์ การแสดงผลและการพิสูจน์ตัวตนแบบไม่ระบุสถานะโดยใช้ JWT ในกรณีนี้เราจะต้องปรับแต่ง Spring Security framework อย่างมากซึ่งเราจะทำในส่วนที่เหลือของบทความ



ตัวอย่างเช่นเราจะใช้แบบคลาสสิก แอปพลิเคชันเว็บร้านหนังสือ และสร้างส่วนหลังที่จะให้ CRUD API เพื่อสร้างผู้แต่งและหนังสือรวมถึง API สำหรับการจัดการผู้ใช้และการตรวจสอบสิทธิ์

ภาพรวมสถาปัตยกรรมความปลอดภัยในฤดูใบไม้ผลิ

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

แผนภาพต่อไปนี้แสดงโฟลว์และแสดงวิธีการประมวลผลคำร้องขอการพิสูจน์ตัวตน:

สถาปัตยกรรมความปลอดภัยในฤดูใบไม้ผลิ

สถาปัตยกรรมความปลอดภัยในฤดูใบไม้ผลิ

ตอนนี้เรามาแบ่งแผนภาพนี้ออกเป็นส่วนประกอบและอภิปรายแต่ละส่วนแยกกัน

โซ่กรองความปลอดภัยสปริง

เมื่อคุณเพิ่มเฟรมเวิร์ก Spring Security ลงในแอปพลิเคชันของคุณระบบจะลงทะเบียนเครือข่ายตัวกรองที่สกัดกั้นคำขอที่เข้ามาทั้งหมดโดยอัตโนมัติ ห่วงโซ่นี้ประกอบด้วยตัวกรองต่างๆและแต่ละตัวจัดการกรณีการใช้งานเฉพาะ

ตัวอย่างเช่น:

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

รายละเอียดที่สำคัญอย่างหนึ่งที่ฉันต้องการกล่าวถึงคือตัวกรอง Spring Security ได้รับการลงทะเบียนด้วยลำดับต่ำสุดและเป็นตัวกรองแรกที่เรียกใช้ สำหรับการใช้งานบางกรณีหากคุณต้องการวางฟิลเตอร์ที่กำหนดเองไว้ข้างหน้าคุณจะต้องเพิ่มช่องว่างภายในคำสั่งซื้อ สามารถทำได้ด้วยการกำหนดค่าต่อไปนี้:

spring.security.filter.order=10

เมื่อเราเพิ่มการกำหนดค่านี้ใน application.properties ของเรา เราจะมีที่ว่างสำหรับตัวกรองแบบกำหนดเอง 10 รายการที่ด้านหน้าตัวกรอง Spring Security

AuthenticationManager

คุณสามารถนึกถึง AuthenticationManager ในฐานะผู้ประสานงานที่คุณสามารถลงทะเบียนผู้ให้บริการหลายรายและขึ้นอยู่กับประเภทคำขอจะส่งคำขอการตรวจสอบสิทธิ์ไปยังผู้ให้บริการที่ถูกต้อง

AuthenticationProvider

AuthenticationProvider ประมวลผลการพิสูจน์ตัวตนประเภทเฉพาะ อินเทอร์เฟซมีเพียงสองฟังก์ชั่น:

  • authenticate ดำเนินการตรวจสอบสิทธิ์ตามคำขอ
  • supports ตรวจสอบว่าผู้ให้บริการรายนี้รองรับประเภทการรับรองความถูกต้องที่ระบุหรือไม่

การใช้งานอินเทอร์เฟซที่สำคัญอย่างหนึ่งที่เราใช้ในโครงการตัวอย่างคือ DaoAuthenticationProvider ซึ่งดึงรายละเอียดผู้ใช้จาก UserDetailsService

UserDetailsService

UserDetailsService อธิบายว่าเป็นอินเทอร์เฟซหลักที่โหลดข้อมูลเฉพาะผู้ใช้ในเอกสาร Spring

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

  • loadUserByUsername ยอมรับชื่อผู้ใช้เป็นพารามิเตอร์และส่งคืนอ็อบเจ็กต์ข้อมูลประจำตัวของผู้ใช้

การรับรองความถูกต้องโดยใช้ JWT กับ Spring Security

หลังจากพูดคุยเรื่องภายในของ Spring Security framework แล้วเรามากำหนดค่าสำหรับการตรวจสอบสิทธิ์แบบไร้สัญชาติด้วยไฟล์ โทเค็น JWT .

ในการปรับแต่ง Spring Security เราจำเป็นต้องมีคลาสการกำหนดค่าที่มีคำอธิบายประกอบ @EnableWebSecurity คำอธิบายประกอบใน classpath ของเรา นอกจากนี้เพื่อลดความซับซ้อนของกระบวนการปรับแต่งเฟรมเวิร์กจะแสดง WebSecurityConfigurerAdapter ชั้นเรียน. เราจะขยายอะแด็ปเตอร์นี้และแทนที่ทั้งสองฟังก์ชันเพื่อ:

  1. กำหนดค่าตัวจัดการการพิสูจน์ตัวตนกับผู้ให้บริการที่ถูกต้อง
  2. กำหนดค่าความปลอดภัยของเว็บ (URL สาธารณะ, URL ส่วนตัว, การอนุญาต ฯลฯ )
@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // TODO configure authentication manager } @Override protected void configure(HttpSecurity http) throws Exception { // TODO configure web security } }

ในแอปพลิเคชันตัวอย่างของเราเราจัดเก็บข้อมูลประจำตัวของผู้ใช้ในฐานข้อมูล MongoDB ใน users คอลเลกชัน ข้อมูลประจำตัวเหล่านี้ถูกแมปโดย User เอนทิตีและการดำเนินการ CRUD ถูกกำหนดโดย UserRepo ที่เก็บข้อมูล Spring

ตอนนี้เมื่อเรายอมรับคำขอรับรองความถูกต้องเราจำเป็นต้องดึงข้อมูลประจำตัวที่ถูกต้องจากฐานข้อมูลโดยใช้ข้อมูลประจำตัวที่ให้มาจากนั้นตรวจสอบ สำหรับสิ่งนี้เราจำเป็นต้องใช้ UserDetailsService อินเทอร์เฟซซึ่งกำหนดไว้ดังนี้:

public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }

ที่นี่เราจะเห็นว่าจำเป็นต้องส่งคืนวัตถุที่ใช้ UserDetails อินเทอร์เฟซและ User ของเรา เอนทิตีดำเนินการ (สำหรับรายละเอียดการนำไปใช้โปรดดูที่เก็บข้อมูลของโครงการตัวอย่าง) เมื่อพิจารณาจากข้อเท็จจริงที่ว่ามันแสดงเฉพาะต้นแบบฟังก์ชันเดียวเราสามารถถือว่ามันเป็นอินเทอร์เฟซที่ใช้งานได้และจัดเตรียมการนำไปใช้เป็นนิพจน์แลมบ์ดา

@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { private final UserRepo userRepo; public SecurityConfig(UserRepo userRepo) { this.userRepo = userRepo; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(username -> userRepo .findByUsername(username) .orElseThrow( () -> new UsernameNotFoundException( format('User: %s, not found', username) ) )); } // Details omitted for brevity }

นี่คือ auth.userDetailsService การเรียกใช้ฟังก์ชันจะเริ่มต้น DaoAuthenticationProvider อินสแตนซ์โดยใช้การติดตั้ง UserDetailsService และลงทะเบียนในตัวจัดการการพิสูจน์ตัวตน

เราจำเป็นต้องกำหนดค่าตัวจัดการการพิสูจน์ตัวตนด้วยสคีมาการเข้ารหัสรหัสผ่านที่ถูกต้องซึ่งจะใช้สำหรับการยืนยันข้อมูลรับรองด้วย สำหรับสิ่งนี้เราจำเป็นต้องเปิดเผยการใช้งาน PasswordEncoder ที่ต้องการ อินเทอร์เฟซเป็นถั่ว

ในโครงการตัวอย่างของเราเราจะใช้ไฟล์ bcrypt อัลกอริทึมการแฮชรหัสผ่าน

@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { private final UserRepo userRepo; public SecurityConfig(UserRepo userRepo) { this.userRepo = userRepo; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(username -> userRepo .findByUsername(username) .orElseThrow( () -> new UsernameNotFoundException( format('User: %s, not found', username) ) )); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } // Details omitted for brevity }

เมื่อกำหนดค่าตัวจัดการการตรวจสอบสิทธิ์แล้วตอนนี้เราจำเป็นต้องกำหนดค่าความปลอดภัยของเว็บ เรากำลังใช้งาน REST API และต้องการการพิสูจน์ตัวตนแบบไร้สถานะด้วยโทเค็น JWT ดังนั้นเราจำเป็นต้องตั้งค่าตัวเลือกต่อไปนี้:

  • เปิดใช้งาน CORS และปิดการใช้งาน CSRF .
  • ตั้งค่าการจัดการเซสชันเป็นแบบไร้สัญชาติ
  • ตั้งค่าตัวจัดการข้อยกเว้นคำขอที่ไม่ได้รับอนุญาต
  • ตั้งค่าสิทธิ์ในจุดสิ้นสุด
  • เพิ่มตัวกรองโทเค็น JWT

การกำหนดค่านี้ใช้งานได้ดังนี้:

@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { private final UserRepo userRepo; private final JwtTokenFilter jwtTokenFilter; public SecurityConfig(UserRepo userRepo, JwtTokenFilter jwtTokenFilter) { this.userRepo = userRepo; this.jwtTokenFilter = jwtTokenFilter; } // Details omitted for brevity @Override protected void configure(HttpSecurity http) throws Exception { // Enable CORS and disable CSRF http = http.cors().and().csrf().disable(); // Set session management to stateless http = http .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and(); // Set unauthorized requests exception handler http = http .exceptionHandling() .authenticationEntryPoint( (request, response, ex) -> { response.sendError( HttpServletResponse.SC_UNAUTHORIZED, ex.getMessage() ); } ) .and(); // Set permissions on endpoints http.authorizeRequests() // Our public endpoints .antMatchers('/api/public/**').permitAll() .antMatchers(HttpMethod.GET, '/api/author/**').permitAll() .antMatchers(HttpMethod.POST, '/api/author/search').permitAll() .antMatchers(HttpMethod.GET, '/api/book/**').permitAll() .antMatchers(HttpMethod.POST, '/api/book/search').permitAll() // Our private endpoints .anyRequest().authenticated(); // Add JWT token filter http.addFilterBefore( jwtTokenFilter, UsernamePasswordAuthenticationFilter.class ); } // Used by spring security if CORS is enabled. @Bean public CorsFilter corsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); config.addAllowedOrigin('*'); config.addAllowedHeader('*'); config.addAllowedMethod('*'); source.registerCorsConfiguration('/**', config); return new CorsFilter(source); } }

โปรดทราบว่าเราได้เพิ่ม JwtTokenFilter ก่อน Spring Security internal UsernamePasswordAuthenticationFilter. เรากำลังดำเนินการดังกล่าวเนื่องจากเราต้องการเข้าถึงข้อมูลประจำตัวของผู้ใช้ ณ จุดนี้เพื่อดำเนินการตรวจสอบสิทธิ์ / การอนุญาตและการแยกจะเกิดขึ้นภายในตัวกรองโทเค็น JWT ตามโทเค็น JWT ที่ให้มา ดำเนินการดังนี้:

@Component public class JwtTokenFilter extends OncePerRequestFilter { private final JwtTokenUtil jwtTokenUtil; private final UserRepo userRepo; public JwtTokenFilter(JwtTokenUtil jwtTokenUtil, UserRepo userRepo) { this.jwtTokenUtil = jwtTokenUtil; this.userRepo = userRepo; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { // Get authorization header and validate final String header = request.getHeader(HttpHeaders.AUTHORIZATION); if (isEmpty(header) || !header.startsWith('Bearer ')) { chain.doFilter(request, response); return; } // Get jwt token and validate final String token = header.split(' ')[1].trim(); if (!jwtTokenUtil.validate(token)) { chain.doFilter(request, response); return; } // Get user identity and set it on the spring security context UserDetails userDetails = userRepo .findByUsername(jwtTokenUtil.getUsername(token)) .orElse(null); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails == null ? List.of() : userDetails.getAuthorities() ); authentication.setDetails( new WebAuthenticationDetailsSource().buildDetails(request) ); SecurityContextHolder.getContext().setAuthentication(authentication); chain.doFilter(request, response); } }

ก่อนที่จะใช้งานฟังก์ชัน API การเข้าสู่ระบบของเราเราต้องดูแลอีกขั้นตอนหนึ่ง - เราจำเป็นต้องเข้าถึงตัวจัดการการตรวจสอบสิทธิ์ โดยค่าเริ่มต้นจะไม่สามารถเข้าถึงได้แบบสาธารณะและเราจำเป็นต้องเปิดเผยอย่างชัดเจนว่าเป็นถั่วในคลาสการกำหนดค่าของเรา

สามารถทำได้ดังนี้:

ภาษา c เขียนใน
@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { // Details omitted for brevity @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }

และตอนนี้เราพร้อมที่จะใช้งานฟังก์ชันล็อกอิน API ของเราแล้ว:

@Api(tags = 'Authentication') @RestController @RequestMapping(path = 'api/public') public class AuthApi { private final AuthenticationManager authenticationManager; private final JwtTokenUtil jwtTokenUtil; private final UserViewMapper userViewMapper; public AuthApi(AuthenticationManager authenticationManager, JwtTokenUtil jwtTokenUtil, UserViewMapper userViewMapper) { this.authenticationManager = authenticationManager; this.jwtTokenUtil = jwtTokenUtil; this.userViewMapper = userViewMapper; } @PostMapping('login') public ResponseEntity login(@RequestBody @Valid AuthRequest request) { try { Authentication authenticate = authenticationManager .authenticate( new UsernamePasswordAuthenticationToken( request.getUsername(), request.getPassword() ) ); User user = (User) authenticate.getPrincipal(); return ResponseEntity.ok() .header( HttpHeaders.AUTHORIZATION, jwtTokenUtil.generateAccessToken(user) ) .body(userViewMapper.toUserView(user)); } catch (BadCredentialsException ex) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } } }

ที่นี่เราตรวจสอบข้อมูลรับรองที่ให้มาโดยใช้ตัวจัดการการพิสูจน์ตัวตนและในกรณีที่ประสบความสำเร็จเราจะสร้างโทเค็น JWT และส่งคืนเป็นส่วนหัวการตอบกลับพร้อมกับข้อมูลประจำตัวของผู้ใช้ในเนื้อหาการตอบกลับ

การอนุญาตด้วย Spring Security

ในส่วนก่อนหน้านี้เราได้ตั้งค่ากระบวนการตรวจสอบสิทธิ์และกำหนดค่า URL สาธารณะ / ส่วนตัว สิ่งนี้อาจเพียงพอสำหรับการใช้งานทั่วไป แต่สำหรับกรณีการใช้งานจริงส่วนใหญ่เราจำเป็นต้องมีนโยบายการเข้าถึงตามบทบาทสำหรับผู้ใช้ของเราเสมอ ในบทนี้เราจะแก้ไขปัญหานี้และตั้งค่าสคีมาการให้สิทธิ์ตามบทบาทโดยใช้ Spring Security framework

ในแอปพลิเคชันตัวอย่างของเราเราได้กำหนดบทบาทสามประการดังต่อไปนี้:

  • USER_ADMIN ช่วยให้เราจัดการผู้ใช้แอปพลิเคชันได้
  • AUTHOR_ADMIN ช่วยให้เราจัดการผู้เขียนได้
  • BOOK_ADMIN ช่วยให้เราจัดการหนังสือได้

ตอนนี้เราจำเป็นต้องใช้กับ URL ที่เกี่ยวข้อง:

  • api/public สามารถเข้าถึงได้โดยสาธารณะ
  • api/admin/user สามารถเข้าถึงผู้ใช้ด้วย USER_ADMIN บทบาท.
  • api/author สามารถเข้าถึงผู้ใช้ด้วย AUTHOR_ADMIN บทบาท.
  • api/book สามารถเข้าถึงผู้ใช้ด้วย BOOK_ADMIN บทบาท.

Spring Security framework ให้เรามีสองตัวเลือกในการตั้งค่า schema การอนุญาต:

  • การกำหนดค่าตาม URL
  • การกำหนดค่าตามคำอธิบายประกอบ

ขั้นแรกมาดูกันว่าการกำหนดค่าตาม URL ทำงานอย่างไร สามารถนำไปใช้กับการกำหนดค่าความปลอดภัยของเว็บได้ดังนี้:

@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { // Details omitted for brevity @Override protected void configure(HttpSecurity http) throws Exception { // Enable CORS and disable CSRF http = http.cors().and().csrf().disable(); // Set session management to stateless http = http .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and(); // Set unauthorized requests exception handler http = http .exceptionHandling() .authenticationEntryPoint( (request, response, ex) -> { response.sendError( HttpServletResponse.SC_UNAUTHORIZED, ex.getMessage() ); } ) .and(); // Set permissions on endpoints http.authorizeRequests() // Our public endpoints .antMatchers('/api/public/**').permitAll() .antMatchers(HttpMethod.GET, '/api/author/**').permitAll() .antMatchers(HttpMethod.POST, '/api/author/search').permitAll() .antMatchers(HttpMethod.GET, '/api/book/**').permitAll() .antMatchers(HttpMethod.POST, '/api/book/search').permitAll() // Our private endpoints .antMatchers('/api/admin/user/**').hasRole(Role.USER_ADMIN) .antMatchers('/api/author/**').hasRole(Role.AUTHOR_ADMIN) .antMatchers('/api/book/**').hasRole(Role.BOOK_ADMIN) .anyRequest().authenticated(); // Add JWT token filter http.addFilterBefore( jwtTokenFilter, UsernamePasswordAuthenticationFilter.class ); } // Details omitted for brevity }

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

Spring Security framework กำหนดคำอธิบายประกอบต่อไปนี้สำหรับการรักษาความปลอดภัยบนเว็บ:

  • @PreAuthorize รองรับ ภาษานิพจน์สปริง และใช้เพื่อจัดเตรียมการควบคุมการเข้าถึงตามนิพจน์ ก่อน ดำเนินการวิธีการ
  • @PostAuthorize รองรับ ภาษานิพจน์สปริง และใช้เพื่อจัดเตรียมการควบคุมการเข้าถึงตามนิพจน์ หลังจาก ดำเนินการเมธอด (ให้ความสามารถในการเข้าถึงผลลัพธ์ของวิธีการ)
  • @PreFilter รองรับ ภาษานิพจน์สปริง และใช้เพื่อกรองคอลเลกชันหรืออาร์เรย์ ก่อน ดำเนินการตามวิธีการตามกฎความปลอดภัยที่กำหนดเองที่เรากำหนด
  • @PostFilter รองรับ ภาษานิพจน์สปริง และใช้เพื่อกรองคอลเลคชันหรืออาร์เรย์ที่ส่งคืน หลังจาก ดำเนินการวิธีการตามกฎความปลอดภัยที่กำหนดเองที่เรากำหนด (ให้ความสามารถในการเข้าถึงผลลัพธ์ของวิธีการ)
  • @Secured ไม่รองรับ ภาษานิพจน์สปริง และใช้เพื่อระบุรายการของบทบาทในวิธีการ
  • @RolesAllowed ไม่รองรับ ภาษานิพจน์สปริง และเป็นไฟล์ JSR 250 คำอธิบายประกอบที่เทียบเท่าของ @Secured คำอธิบายประกอบ

คำอธิบายประกอบเหล่านี้ถูกปิดใช้งานโดยค่าเริ่มต้นและสามารถเปิดใช้งานได้ในแอปพลิเคชันของเราดังนี้:

@EnableWebSecurity @EnableGlobalMethodSecurity( securedEnabled = true, jsr250Enabled = true, prePostEnabled = true ) public class SecurityConfig extends WebSecurityConfigurerAdapter { // Details omitted for brevity }


securedEnabled = true เปิดใช้งาน @Secured คำอธิบายประกอบ
jsr250Enabled = true เปิดใช้งาน @RolesAllowed คำอธิบายประกอบ
prePostEnabled = true เปิดใช้งาน @PreAuthorize, @PostAuthorize, @PreFilter, @PostFilter คำอธิบายประกอบ

หลังจากเปิดใช้งานแล้วเราสามารถบังคับใช้นโยบายการเข้าถึงตามบทบาทกับปลายทาง API ของเราได้ดังนี้:

@Api(tags = 'UserAdmin') @RestController @RequestMapping(path = 'api/admin/user') @RolesAllowed(Role.USER_ADMIN) public class UserAdminApi { // Details omitted for brevity } @Api(tags = 'Author') @RestController @RequestMapping(path = 'api/author') public class AuthorApi { // Details omitted for brevity @RolesAllowed(Role.AUTHOR_ADMIN) @PostMapping public void create() { } @RolesAllowed(Role.AUTHOR_ADMIN) @PutMapping('{id}') public void edit() { } @RolesAllowed(Role.AUTHOR_ADMIN) @DeleteMapping('{id}') public void delete() { } @GetMapping('{id}') public void get() { } @GetMapping('{id}/book') public void getBooks() { } @PostMapping('search') public void search() { } } @Api(tags = 'Book') @RestController @RequestMapping(path = 'api/book') public class BookApi { // Details omitted for brevity @RolesAllowed(Role.BOOK_ADMIN) @PostMapping public BookView create() { } @RolesAllowed(Role.BOOK_ADMIN) @PutMapping('{id}') public void edit() { } @RolesAllowed(Role.BOOK_ADMIN) @DeleteMapping('{id}') public void delete() { } @GetMapping('{id}') public void get() { } @GetMapping('{id}/author') public void getAuthors() { } @PostMapping('search') public void search() { } }

โปรดทราบว่าคำอธิบายประกอบการรักษาความปลอดภัยสามารถให้ได้ทั้งในระดับชั้นเรียนและระดับวิธีการ

ตัวอย่างที่แสดงให้เห็นนั้นเรียบง่ายและไม่ได้แสดงถึงสถานการณ์ในโลกแห่งความเป็นจริง แต่ Spring Security มีชุดคำอธิบายประกอบมากมายและคุณสามารถจัดการกับสคีมาการให้สิทธิ์ที่ซับซ้อนได้หากคุณเลือกที่จะใช้

อัลกอริธึมการเรียนรู้ของเครื่องใน python

คำนำหน้าชื่อบทบาทเริ่มต้น

ในส่วนย่อยที่แยกจากกันนี้ฉันต้องการเน้นรายละเอียดที่ละเอียดอ่อนอีกอย่างหนึ่งซึ่งทำให้ผู้ใช้ใหม่จำนวนมากสับสน

กรอบงาน Spring Security สร้างความแตกต่างสองคำ:

  • Authority แสดงถึงการอนุญาตของแต่ละบุคคล
  • Role แสดงถึงกลุ่มสิทธิ์

ทั้งสองสามารถแสดงด้วยอินเทอร์เฟซเดียวที่เรียกว่า GrantedAuthority และตรวจสอบภายหลังด้วย Spring Expression Language ภายในคำอธิบายประกอบ Spring Security ดังนี้:

  • Authority: @PreAuthorize (“ hasAuthority (‘EDIT_BOOK’)”)
  • Role: @PreAuthorize (“ hasRole (‘BOOK_ADMIN’)”)

เพื่อสร้างความแตกต่างระหว่างสองคำนี้ให้ชัดเจนยิ่งขึ้น Spring Security framework ได้เพิ่ม ROLE_ คำนำหน้าชื่อบทบาทตามค่าเริ่มต้น ดังนั้นแทนที่จะตรวจสอบบทบาทที่ชื่อ BOOK_ADMIN มันจะตรวจหา ROLE_BOOK_ADMIN

โดยส่วนตัวแล้วฉันพบว่าพฤติกรรมนี้สับสนและต้องการปิดใช้งานในแอปพลิเคชันของฉัน สามารถปิดใช้งานได้ภายในการกำหนดค่า Spring Security ดังนี้:

@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { // Details omitted for brevity @Bean GrantedAuthorityDefaults grantedAuthorityDefaults() { return new GrantedAuthorityDefaults(''); // Remove the ROLE_ prefix } }

การทดสอบด้วย Spring Security

ในการทดสอบจุดสิ้นสุดของเราด้วยการทดสอบหน่วยหรือการรวมเมื่อใช้ Spring Security framework เราจำเป็นต้องเพิ่ม spring-security-test การพึ่งพาพร้อมกับ spring-boot-starter-test ของเรา pom.xml สร้างไฟล์จะมีลักษณะดังนี้:

org.springframework.boot spring-boot-starter-test test org.junit.vintage junit-vintage-engine org.springframework.security spring-security-test test

การพึ่งพานี้ทำให้เราสามารถเข้าถึงคำอธิบายประกอบบางส่วนที่สามารถใช้เพื่อเพิ่มบริบทความปลอดภัยให้กับฟังก์ชันการทดสอบของเรา

คำอธิบายประกอบเหล่านี้ ได้แก่ :

  • @WithMockUser สามารถเพิ่มลงในวิธีการทดสอบเพื่อจำลองการทำงานกับผู้ใช้ที่ถูกล้อเลียน
  • @WithUserDetails สามารถเพิ่มลงในวิธีการทดสอบเพื่อจำลองการทำงานด้วย UserDetails กลับมาจาก UserDetailsService.
  • @WithAnonymousUser สามารถเพิ่มลงในวิธีการทดสอบเพื่อจำลองการทำงานกับผู้ใช้ที่ไม่ระบุชื่อ สิ่งนี้มีประโยชน์เมื่อผู้ใช้ต้องการเรียกใช้การทดสอบส่วนใหญ่ในฐานะผู้ใช้ที่เจาะจงและลบล้างวิธีการบางอย่างเพื่อไม่ระบุตัวตน
  • @WithSecurityContext กำหนดอะไร SecurityContext ที่จะใช้และคำอธิบายประกอบทั้งสามที่อธิบายไว้ข้างต้นเป็นไปตามนั้น หากเรามีกรณีการใช้งานที่เฉพาะเจาะจงเราสามารถสร้างคำอธิบายประกอบของเราเองที่ใช้ @WithSecurityContext เพื่อสร้าง SecurityContext พวกเราต้องการ. การสนทนานี้อยู่นอกขอบเขตของบทความของเราและโปรดดูเอกสาร Spring Security สำหรับรายละเอียดเพิ่มเติม

วิธีที่ง่ายที่สุดในการเรียกใช้การทดสอบกับผู้ใช้เฉพาะคือการใช้ @WithMockUser คำอธิบายประกอบ เราสามารถสร้างผู้ใช้จำลองขึ้นมาและทำการทดสอบได้ดังนี้:

@Test @WithMockUser(username=' [email protected] ', roles={'USER_ADMIN'}) public void test() { // Details omitted for brevity }

แนวทางนี้มีข้อเสียอยู่สองประการ ขั้นแรกไม่มีผู้ใช้จำลองและหากคุณเรียกใช้การทดสอบการรวมซึ่งค้นหาข้อมูลผู้ใช้จากฐานข้อมูลในภายหลังการทดสอบจะล้มเหลว ประการที่สองผู้ใช้จำลองเป็นตัวอย่างของ org.springframework.security.core.userdetails.User คลาสซึ่งเป็นการใช้งาน UserDetails ภายในของ Spring framework อินเทอร์เฟซและหากเรามีการใช้งานของเราเองสิ่งนี้อาจทำให้เกิดความขัดแย้งในภายหลังระหว่างการดำเนินการทดสอบ

หากข้อเสียก่อนหน้านี้เป็นตัวบล็อกสำหรับแอปพลิเคชันของเราดังนั้น @WithUserDetails คำอธิบายประกอบเป็นวิธีที่จะไป ใช้เมื่อเรากำหนดเอง UserDetails และ UserDetailsService การใช้งาน ถือว่ามีผู้ใช้อยู่ดังนั้นเราจึงต้องสร้างแถวจริงในฐานข้อมูลหรือระบุ UserDetailsService ตัวอย่างก่อนทำการทดสอบ

นี่คือวิธีที่เราสามารถใช้คำอธิบายประกอบนี้:

@Test @WithUserDetails(' [email protected] ') public void test() { // Details omitted for brevity }

นี่เป็นคำอธิบายประกอบที่ต้องการในการทดสอบการรวมโครงการตัวอย่างของเราเนื่องจากเรามีการใช้งานอินเทอร์เฟซดังกล่าวที่กำหนดเอง

ใช้ @WithAnonymousUser อนุญาตให้ทำงานในฐานะผู้ใช้ที่ไม่ระบุชื่อ สิ่งนี้สะดวกเป็นพิเศษเมื่อคุณต้องการเรียกใช้การทดสอบส่วนใหญ่กับผู้ใช้เฉพาะ แต่การทดสอบบางส่วนในฐานะผู้ใช้ที่ไม่ระบุตัวตน ตัวอย่างเช่นต่อไปนี้จะทำงาน ทดสอบ 1 และ ทดสอบ 2 กรณีทดสอบกับผู้ใช้จำลองและ ทดสอบ 3 กับผู้ใช้ที่ไม่ระบุชื่อ:

@SpringBootTest @AutoConfigureMockMvc @WithMockUser public class WithUserClassLevelAuthenticationTests { @Test public void test1() { // Details omitted for brevity } @Test public void test2() { // Details omitted for brevity } @Test @WithAnonymousUser public void test3() throws Exception { // Details omitted for brevity } }

ห่อ

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

ในบทความนี้ฉันพยายามสาธิตรายละเอียดปลีกย่อยของการกำหนดค่าทั้งหมดและหวังว่าคุณจะพบว่าตัวอย่างมีประโยชน์ สำหรับตัวอย่างโค้ดทั้งหมดโปรดดูที่เก็บ Git ของ my ตัวอย่างโครงการ Spring Security .

ทำความเข้าใจพื้นฐาน

Spring Security คืออะไร?

Spring Security เป็นกรอบการพิสูจน์ตัวตนและการอนุญาตที่มีประสิทธิภาพและปรับแต่งได้สูง เป็นมาตรฐานโดยพฤตินัยสำหรับการรักษาความปลอดภัยแอปพลิเคชันที่ใช้สปริง

ฉันจะใช้ Spring Security กับ REST API ได้อย่างไร

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

Spring Security ปลอดภัยแค่ไหน?

Spring Security ค่อนข้างปลอดภัย มันรวมเข้ากับแอพพลิเคชั่นที่ใช้ Spring ได้อย่างง่ายดายรองรับการรับรองความถูกต้องหลายประเภทนอกกรอบและมีความสามารถในการตั้งโปรแกรมการรักษาความปลอดภัยแบบเปิดเผย

เหตุใดจึงใช้ Spring Security

เนื่องจากมันรวมเข้ากับระบบนิเวศอื่น ๆ ของ Spring ได้อย่างราบรื่นและนักพัฒนาหลายคนชอบที่จะนำโซลูชันที่มีอยู่กลับมาใช้ใหม่แทนที่จะสร้างวงล้อใหม่

JWT คืออะไร?

JSON Web Token (JWT) เป็นมาตรฐานสำหรับการเข้ารหัสข้อมูลที่อาจถูกส่งอย่างปลอดภัยเป็นออบเจ็กต์ JSON

JWT ทำงานกับ Spring Security อย่างไร

เราเปิดเผย POST API สาธารณะสำหรับการตรวจสอบความถูกต้องและเมื่อส่งผ่านข้อมูลรับรองที่ถูกต้องมันจะสร้าง JWT หากผู้ใช้พยายามเข้าถึง API ที่มีการป้องกันผู้ใช้จะอนุญาตให้เข้าถึงก็ต่อเมื่อคำขอมี JWT ที่ถูกต้อง การตรวจสอบจะเกิดขึ้นในตัวกรองที่ลงทะเบียนในห่วงโซ่ตัวกรอง Spring Security

อัปเกรดการวิเคราะห์ของคุณด้วยแรงบันดาลใจในการออกแบบแดชบอร์ดเหล่านี้

การออกแบบ Ui

อัปเกรดการวิเคราะห์ของคุณด้วยแรงบันดาลใจในการออกแบบแดชบอร์ดเหล่านี้
ทำให้ Web Front-end เชื่อถือได้ด้วย Elm

ทำให้ Web Front-end เชื่อถือได้ด้วย Elm

เทคโนโลยี

โพสต์ยอดนิยม
ตลาด Crowdfunding Equity ของสหรัฐมีการเติบโตขึ้นตามความคาดหวังหรือไม่?
ตลาด Crowdfunding Equity ของสหรัฐมีการเติบโตขึ้นตามความคาดหวังหรือไม่?
คู่มือสำคัญสำหรับ Qmake
คู่มือสำคัญสำหรับ Qmake
หลักการออกแบบ Mobile UX
หลักการออกแบบ Mobile UX
MIDI Tutorial: การสร้างแอปพลิเคชั่นเสียงบนเบราว์เซอร์ที่ควบคุมโดยฮาร์ดแวร์ MIDI
MIDI Tutorial: การสร้างแอปพลิเคชั่นเสียงบนเบราว์เซอร์ที่ควบคุมโดยฮาร์ดแวร์ MIDI
Init.js: คำแนะนำเกี่ยวกับสาเหตุและวิธีการใช้ JavaScript แบบ Full-Stack
Init.js: คำแนะนำเกี่ยวกับสาเหตุและวิธีการใช้ JavaScript แบบ Full-Stack
 
Splash of EarlGrey - UI การทดสอบแอพ ApeeScape Talent
Splash of EarlGrey - UI การทดสอบแอพ ApeeScape Talent
จาก Node.js ไปจนถึงการจ่ายภาษีอิสระของคุณ: บทสัมภาษณ์กับ Developer ที่ประสบความสำเร็จ
จาก Node.js ไปจนถึงการจ่ายภาษีอิสระของคุณ: บทสัมภาษณ์กับ Developer ที่ประสบความสำเร็จ
ขายธุรกิจของคุณ? หยุดทิ้งเงินไว้บนโต๊ะ
ขายธุรกิจของคุณ? หยุดทิ้งเงินไว้บนโต๊ะ
บทช่วยสอนเกี่ยวกับส่วนขยายแอป iOS 8
บทช่วยสอนเกี่ยวกับส่วนขยายแอป iOS 8
ผู้จัดการการเติบโต
ผู้จัดการการเติบโต
โพสต์ยอดนิยม
  • องค์ประกอบและหลักการนิยามการออกแบบ
  • นิติบุคคลประเภทใดเป็น llc
  • ข้อยกเว้นจาวาสคริปต์ในการจัดการแนวปฏิบัติที่ดีที่สุด
  • สร้าง Google glass ของคุณเอง
  • เคล็ดลับการปรับแต่งประสิทธิภาพของเซิร์ฟเวอร์ sql
  • อินเทอร์เน็ตของสิ่งต่าง ๆ ที่บ้านอัตโนมัติ
  • เว็บไซต์ที่ดีที่สุดในการเรียนรู้ c++
หมวดหมู่
  • การจัดการโครงการ
  • การเพิ่มขึ้นของระยะไกล
  • การบริหารโครงการ
  • เครื่องมือและบทช่วยสอน
  • © 2022 | สงวนลิขสิทธิ์

    portaldacalheta.pt