ฤดูใบไม้ผลิ ถือเป็นกรอบงานที่เชื่อถือได้ในระบบนิเวศของ Java และมีการใช้กันอย่างแพร่หลาย การอ้างถึง Spring เป็นเฟรมเวิร์กไม่ถูกต้องอีกต่อไปเนื่องจากเป็นคำศัพท์ที่ครอบคลุมกรอบต่างๆมากกว่า หนึ่งในกรอบนี้คือ ความปลอดภัยในฤดูใบไม้ผลิ ซึ่งเป็นกรอบการพิสูจน์ตัวตนและการอนุญาตที่มีประสิทธิภาพและปรับแต่งได้ ถือเป็นมาตรฐานโดยพฤตินัยสำหรับการรักษาความปลอดภัยแอปพลิเคชันที่ใช้สปริง
แม้จะเป็นที่นิยม แต่ก็ต้องยอมรับว่าเมื่อพูดถึง แอปพลิเคชันหน้าเดียว การกำหนดค่าไม่ง่ายและตรงไปตรงมา ฉันสงสัยว่าเหตุผลคือมันเริ่มมากขึ้นในฐานะไฟล์ แอปพลิเคชัน MVC เฟรมเวิร์กที่ได้รับการสนับสนุนโดยที่การแสดงผลหน้าเว็บเกิดขึ้นบนฝั่งเซิร์ฟเวอร์และการสื่อสารเป็นแบบเซสชัน
หากแบ็คเอนด์เป็นไปตาม Java และ Spring ควรใช้ Spring Security สำหรับการพิสูจน์ตัวตน / การอนุญาตและกำหนดค่าสำหรับการสื่อสารแบบไม่ระบุสถานะ แม้ว่าจะมีบทความมากมายที่อธิบายถึงวิธีการดำเนินการนี้ แต่สำหรับฉันแล้วการตั้งค่าเป็นครั้งแรกยังคงน่าหงุดหงิดและฉันต้องอ่านและสรุปข้อมูลจากหลายแหล่ง นั่นคือเหตุผลที่ฉันตัดสินใจเขียนบทความนี้โดยที่ฉันจะพยายามสรุปและครอบคลุมรายละเอียดและข้อบกพร่องที่จำเป็นทั้งหมดที่คุณอาจพบในระหว่างขั้นตอนการกำหนดค่า
ก่อนที่จะเจาะลึกรายละเอียดทางเทคนิคฉันต้องการกำหนดคำศัพท์ที่ใช้ในบริบท Spring Security อย่างชัดเจนเพื่อให้แน่ใจว่าเราทุกคนพูดภาษาเดียวกัน
นี่คือข้อกำหนดที่เราต้องระบุ:
ก่อนที่จะย้ายไปที่การกำหนดค่า 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 ลงในแอปพลิเคชันของคุณระบบจะลงทะเบียนเครือข่ายตัวกรองที่สกัดกั้นคำขอที่เข้ามาทั้งหมดโดยอัตโนมัติ ห่วงโซ่นี้ประกอบด้วยตัวกรองต่างๆและแต่ละตัวจัดการกรณีการใช้งานเฉพาะ
ตัวอย่างเช่น:
รายละเอียดที่สำคัญอย่างหนึ่งที่ฉันต้องการกล่าวถึงคือตัวกรอง Spring Security ได้รับการลงทะเบียนด้วยลำดับต่ำสุดและเป็นตัวกรองแรกที่เรียกใช้ สำหรับการใช้งานบางกรณีหากคุณต้องการวางฟิลเตอร์ที่กำหนดเองไว้ข้างหน้าคุณจะต้องเพิ่มช่องว่างภายในคำสั่งซื้อ สามารถทำได้ด้วยการกำหนดค่าต่อไปนี้:
spring.security.filter.order=10
เมื่อเราเพิ่มการกำหนดค่านี้ใน application.properties
ของเรา เราจะมีที่ว่างสำหรับตัวกรองแบบกำหนดเอง 10 รายการที่ด้านหน้าตัวกรอง Spring Security
คุณสามารถนึกถึง AuthenticationManager
ในฐานะผู้ประสานงานที่คุณสามารถลงทะเบียนผู้ให้บริการหลายรายและขึ้นอยู่กับประเภทคำขอจะส่งคำขอการตรวจสอบสิทธิ์ไปยังผู้ให้บริการที่ถูกต้อง
AuthenticationProvider
ประมวลผลการพิสูจน์ตัวตนประเภทเฉพาะ อินเทอร์เฟซมีเพียงสองฟังก์ชั่น:
authenticate
ดำเนินการตรวจสอบสิทธิ์ตามคำขอsupports
ตรวจสอบว่าผู้ให้บริการรายนี้รองรับประเภทการรับรองความถูกต้องที่ระบุหรือไม่การใช้งานอินเทอร์เฟซที่สำคัญอย่างหนึ่งที่เราใช้ในโครงการตัวอย่างคือ DaoAuthenticationProvider
ซึ่งดึงรายละเอียดผู้ใช้จาก UserDetailsService
UserDetailsService
อธิบายว่าเป็นอินเทอร์เฟซหลักที่โหลดข้อมูลเฉพาะผู้ใช้ในเอกสาร Spring
ในกรณีการใช้งานส่วนใหญ่ผู้ให้บริการการพิสูจน์ตัวตนจะดึงข้อมูลประจำตัวของผู้ใช้ตามข้อมูลประจำตัวจากฐานข้อมูลจากนั้นทำการตรวจสอบความถูกต้อง เนื่องจากกรณีการใช้งานนี้เป็นเรื่องธรรมดานักพัฒนา Spring จึงตัดสินใจแยกเป็นอินเทอร์เฟซแยกต่างหากซึ่งจะแสดงฟังก์ชันเดียว:
loadUserByUsername
ยอมรับชื่อผู้ใช้เป็นพารามิเตอร์และส่งคืนอ็อบเจ็กต์ข้อมูลประจำตัวของผู้ใช้หลังจากพูดคุยเรื่องภายในของ Spring Security framework แล้วเรามากำหนดค่าสำหรับการตรวจสอบสิทธิ์แบบไร้สัญชาติด้วยไฟล์ โทเค็น JWT .
ในการปรับแต่ง Spring Security เราจำเป็นต้องมีคลาสการกำหนดค่าที่มีคำอธิบายประกอบ @EnableWebSecurity
คำอธิบายประกอบใน classpath ของเรา นอกจากนี้เพื่อลดความซับซ้อนของกระบวนการปรับแต่งเฟรมเวิร์กจะแสดง WebSecurityConfigurerAdapter
ชั้นเรียน. เราจะขยายอะแด็ปเตอร์นี้และแทนที่ทั้งสองฟังก์ชันเพื่อ:
@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 ดังนั้นเราจำเป็นต้องตั้งค่าตัวเลือกต่อไปนี้:
การกำหนดค่านี้ใช้งานได้ดังนี้:
@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 และส่งคืนเป็นส่วนหัวการตอบกลับพร้อมกับข้อมูลประจำตัวของผู้ใช้ในเนื้อหาการตอบกลับ
ในส่วนก่อนหน้านี้เราได้ตั้งค่ากระบวนการตรวจสอบสิทธิ์และกำหนดค่า 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 ทำงานอย่างไร สามารถนำไปใช้กับการกำหนดค่าความปลอดภัยของเว็บได้ดังนี้:
@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 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 มาพร้อมกับการรับรองความถูกต้องตามเซสชันซึ่งมีประโยชน์สำหรับเว็บแอปพลิเคชัน MVC แบบคลาสสิก แต่เราสามารถกำหนดค่าให้รองรับการตรวจสอบสิทธิ์แบบไร้สถานะแบบ JWT สำหรับ REST API
Spring Security ค่อนข้างปลอดภัย มันรวมเข้ากับแอพพลิเคชั่นที่ใช้ Spring ได้อย่างง่ายดายรองรับการรับรองความถูกต้องหลายประเภทนอกกรอบและมีความสามารถในการตั้งโปรแกรมการรักษาความปลอดภัยแบบเปิดเผย
เนื่องจากมันรวมเข้ากับระบบนิเวศอื่น ๆ ของ Spring ได้อย่างราบรื่นและนักพัฒนาหลายคนชอบที่จะนำโซลูชันที่มีอยู่กลับมาใช้ใหม่แทนที่จะสร้างวงล้อใหม่
JSON Web Token (JWT) เป็นมาตรฐานสำหรับการเข้ารหัสข้อมูลที่อาจถูกส่งอย่างปลอดภัยเป็นออบเจ็กต์ JSON
เราเปิดเผย POST API สาธารณะสำหรับการตรวจสอบความถูกต้องและเมื่อส่งผ่านข้อมูลรับรองที่ถูกต้องมันจะสร้าง JWT หากผู้ใช้พยายามเข้าถึง API ที่มีการป้องกันผู้ใช้จะอนุญาตให้เข้าถึงก็ต่อเมื่อคำขอมี JWT ที่ถูกต้อง การตรวจสอบจะเกิดขึ้นในตัวกรองที่ลงทะเบียนในห่วงโซ่ตัวกรอง Spring Security