การใช้ Spring Boot สำหรับ OAuth2 และ JWT REST Protection

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

บทความนี้เป็นแนวทางเกี่ยวกับวิธีการตั้งค่าการใช้งานฝั่งเซิร์ฟเวอร์ของ JSON Web Token (JWT) - กรอบงานการอนุญาต OAuth2 โดยใช้ Spring Boot และ Maven

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

OAuth2 เป็นเฟรมเวิร์กการให้สิทธิ์แทนที่ OAuth เวอร์ชันแรกซึ่งสร้างขึ้นในปี 2549 โดยกำหนดโฟลว์การให้สิทธิ์ระหว่างไคลเอ็นต์และบริการ HTTP หนึ่งรายการขึ้นไปเพื่อเข้าถึงทรัพยากรที่มีการป้องกัน

OAuth2 กำหนดบทบาทฝั่งเซิร์ฟเวอร์ต่อไปนี้:

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

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

โครงสร้างที่มีสามารถเป็น JSON Web Signature (JWS) หรือ JSON Web Encryption (JWE)

สามารถเลือก JWT เป็นรูปแบบสำหรับการเข้าถึงและรีเฟรชโทเค็นที่ใช้ในโปรโตคอล OAuth2

OAuth2 และ JWT ได้รับความนิยมอย่างมากในช่วงหลายปีที่ผ่านมาเนื่องจากคุณสมบัติดังต่อไปนี้:

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

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

  • โปรโตคอลไร้สัญชาติไม่อนุญาตให้เพิกถอนการเข้าถึงทางฝั่งเซิร์ฟเวอร์
  • อายุการใช้งานคงที่ของโทเค็นเพิ่มความซับซ้อนเพิ่มเติมสำหรับการจัดการเซสชันที่ใช้เวลานานโดยไม่กระทบต่อความปลอดภัย (เช่น รีเฟรชโทเค็น)
  • ข้อกำหนดสำหรับร้านค้าที่ปลอดภัยสำหรับโทเค็นในฝั่งไคลเอ็นต์

กระแสโปรโตคอลที่คาดหวัง

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

การทำให้เข้าใจง่ายนี้ควรช่วยเน้นที่จุดมุ่งหมายของบทความ กล่าวคือ การตั้งค่าระบบดังกล่าวในสภาพแวดล้อมของ Spring Boot

โฟลว์แบบง่ายได้อธิบายไว้ด้านล่าง:

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

ไดอะแกรมการตรวจสอบความถูกต้อง

Spring Security และ Spring Boot

ก่อนอื่น แนะนำสั้นๆ เกี่ยวกับกลุ่มเทคโนโลยีที่เลือกสำหรับโครงการนี้

เครื่องมือการจัดการโครงการที่เลือกคือ Maven แต่เนื่องจากความเรียบง่ายของโครงการ การเปลี่ยนไปใช้เครื่องมืออื่นๆ เช่น Gradle ไม่ใช่เรื่องยาก

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

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

มาดูสถาปัตยกรรม Spring Security กันอย่างรวดเร็ว (ดูคำแนะนำโดยละเอียดเพิ่มเติมได้ที่นี่)

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

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

เกี่ยวกับการอนุญาตมีการระบุพื้นที่หลักสามประการ:

  1. เว็บขออนุญาติ
  2. การอนุญาตระดับวิธีการ
  3. การเข้าถึงการอนุญาตอินสแตนซ์อ็อบเจ็กต์โดเมน

การตรวจสอบสิทธิ์

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

การอนุญาต

อินเทอร์เฟซหลักคือ AccessDecisionManager ; ซึ่งการใช้งานสำหรับทั้งสามพื้นที่ที่ระบุไว้ข้างต้นจะมอบสิทธิ์ให้กับห่วงโซ่ของ AccessDecisionVoter แต่ละอินสแตนซ์ของอินเทอร์เฟซหลังแสดงถึงการเชื่อมโยงระหว่างการ Authentication (ตัวตนผู้ใช้ ชื่อตัวการ) ทรัพยากรและชุดของ ConfigAttribute ชุดของกฎที่อธิบายว่าเจ้าของทรัพยากรอนุญาตให้เข้าถึงทรัพยากรได้อย่างไร อาจผ่านทาง การใช้บทบาทของผู้ใช้

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

ความปลอดภัยของเมธอดจะเปิดใช้งานก่อนเมื่อมีคำอธิบายประกอบ @EnableGlobalMethodSecurity(securedEnabled = true) จากนั้นใช้ชุดคำอธิบายประกอบเฉพาะเพื่อนำไปใช้กับแต่ละวิธีที่จะได้รับการป้องกัน เช่น @Secured , @PreAuthorize และ @PostAuthorize

Spring Boot ได้เพิ่มคอลเลกชั่นของการกำหนดค่าแอปพลิเคชันที่ได้รับความเห็นชอบและไลบรารีของบุคคลที่สามมาไว้ในคอลเลกชั่นนี้ เพื่อช่วยให้การพัฒนาง่ายขึ้นในขณะที่ยังคงรักษามาตรฐานคุณภาพสูงไว้

JWT OAuth2 พร้อม Spring Boot

มาต่อกันที่ปัญหาเดิมในการตั้งค่าแอปพลิเคชันที่ใช้ OAuth2 และ JWT ด้วย Spring Boot

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

Maven จัดการการขึ้นต่อกันของไลบรารีที่เกี่ยวข้องกับความปลอดภัยทั้งหมดด้วยความช่วยเหลือของ Spring Boot ซึ่งเป็นส่วนประกอบเดียวที่ต้องการเวอร์ชันที่ชัดเจนภายในไฟล์การกำหนดค่าของ maven pom.xml (เช่น เวอร์ชันไลบรารีจะถูกอนุมานโดยอัตโนมัติโดย Maven โดยเลือกเวอร์ชันล่าสุด รุ่นที่เข้ากันได้กับรุ่น Spring Boot ที่ใส่ไว้)

ค้นหาด้านล่างที่ตัดตอนมาจากไฟล์กำหนดค่าของ maven pom.xml ที่มีการขึ้นต่อกันที่เกี่ยวข้องกับความปลอดภัยของ Spring Boot:

 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.security.oauth.boot</groupId> <artifactId>spring-security-oauth2-autoconfigure</artifactId> <version>2.1.0.RELEASE</version> </dependency>

แอปทำหน้าที่เป็นทั้งเซิร์ฟเวอร์การอนุญาต OAuth2 / เจ้าของทรัพยากรและเป็นเซิร์ฟเวอร์ทรัพยากร

ทรัพยากรที่ได้รับการป้องกัน (ในฐานะเซิร์ฟเวอร์ทรัพยากร) ถูกเผยแพร่ภายใต้เส้นทาง /api/ ในขณะที่เส้นทางการรับรองความถูกต้อง (ในฐานะเจ้าของทรัพยากร/เซิร์ฟเวอร์การให้สิทธิ์) ถูกแมปกับ /oauth/token ตามค่าเริ่มต้นที่เสนอ

โครงสร้างของแอพ:

  • แพ็คเกจ security ที่มีการกำหนดค่าความปลอดภัย
  • แพ็คเกจข้อผิด errors ที่มีการจัดการข้อผิดพลาด
  • users แพ็คเกจ glee สำหรับทรัพยากร REST รวมถึง model, repository และ controller

ย่อหน้าถัดไปครอบคลุมการกำหนดค่าสำหรับบทบาท OAuth2 แต่ละบทบาทที่กล่าวถึงข้างต้น คลาสที่เกี่ยวข้องอยู่ในแพ็คเกจ security :

  • OAuthConfiguration ขยาย AuthorizationServerConfigurerAdapter
  • ResourceServerConfiguration ขยาย ResourceServerConfigurerAdapter
  • ServerSecurityConfig ขยาย WebSecurityConfigurerAdapter
  • UserService การใช้งาน UserDetailsService

การตั้งค่าสำหรับ Resource Owner และ Authorization Server

ลักษณะการทำงานของเซิร์ฟเวอร์การให้สิทธิ์เปิดใช้งานโดยการมีคำอธิบายประกอบ @EnableAuthorizationServer การกำหนดค่าถูกรวมเข้ากับการกำหนดค่าที่เกี่ยวข้องกับพฤติกรรมของเจ้าของทรัพยากร และทั้งสองมีอยู่ในคลาส AuthorizationServerConfigurerAdapter

การกำหนดค่าที่ใช้ที่นี่เกี่ยวข้องกับ:

  • การเข้าถึงไคลเอ็นต์ (โดยใช้ ClientDetailsServiceConfigurer )
    • การเลือกใช้งานหน่วยความจำในหน่วยความจำหรือที่เก็บข้อมูลแบบ JDBC สำหรับรายละเอียดไคลเอ็นต์ด้วย inMemory หรือ jdbc
    • การรับรองความถูกต้องพื้นฐานของไคลเอ็นต์โดยใช้ clientId และ clientSecret (เข้ารหัสด้วย PasswordEncoder bean ที่เลือก)
    • เวลาที่ถูกต้องสำหรับการเข้าถึงและรีเฟรชโทเค็นโดยใช้แอตทริบิวต์ accessTokenValiditySeconds และ refreshTokenValiditySeconds
    • อนุญาตประเภทการให้ authorizedGrantTypes โดยใช้แอตทริบิวต์ที่ได้รับอนุญาตของ GrantTypes
    • กำหนดขอบเขตการเข้าถึงด้วยวิธี scopes
    • ระบุทรัพยากรที่ลูกค้าเข้าถึงได้
  • ปลายทางเซิร์ฟเวอร์การให้สิทธิ์ (โดยใช้ AuthorizationServerEndpointsConfigurer )
    • กำหนดการใช้โทเค็น JWT ด้วย accessTokenConverter
    • กำหนดการใช้อินเทอร์เฟซ UserDetailsService และ AuthenticationManager เพื่อดำเนินการรับรองความถูกต้อง (ในฐานะเจ้าของทรัพยากร)
 package net.reliqs.gleeometer.security; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; @Configuration @EnableAuthorizationServer public class OAuthConfiguration extends AuthorizationServerConfigurerAdapter { private final AuthenticationManager authenticationManager; private final PasswordEncoder passwordEncoder; private final UserDetailsService userService; @Value("${jwt.clientId:glee-o-meter}") private String clientId; @Value("${jwt.client-secret:secret}") private String clientSecret; @Value("${jwt.signing-key:123}") private String jwtSigningKey; @Value("${jwt.accessTokenValidititySeconds:43200}") // 12 hours private int accessTokenValiditySeconds; @Value("${jwt.authorizedGrantTypes:password,authorization_code,refresh_token}") private String[] authorizedGrantTypes; @Value("${jwt.refreshTokenValiditySeconds:2592000}") // 30 days private int refreshTokenValiditySeconds; public OAuthConfiguration(AuthenticationManager authenticationManager, PasswordEncoder passwordEncoder, UserDetailsService userService) { this.authenticationManager = authenticationManager; this.passwordEncoder = passwordEncoder; this.userService = userService; } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient(clientId) .secret(passwordEncoder.encode(clientSecret)) .accessTokenValiditySeconds(accessTokenValiditySeconds) .refreshTokenValiditySeconds(refreshTokenValiditySeconds) .authorizedGrantTypes(authorizedGrantTypes) .scopes("read", "write") .resourceIds("api"); } @Override public void configure(final AuthorizationServerEndpointsConfigurer endpoints) { endpoints .accessTokenConverter(accessTokenConverter()) .userDetailsService(userService) .authenticationManager(authenticationManager); } @Bean JwtAccessTokenConverter accessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); return converter; } }

ส่วนถัดไปจะอธิบายการกำหนดค่าเพื่อใช้กับเซิร์ฟเวอร์ทรัพยากร

การตั้งค่าสำหรับเซิร์ฟเวอร์ทรัพยากร

ลักษณะการทำงานของเซิร์ฟเวอร์ทรัพยากรเปิดใช้งานโดยใช้คำอธิบายประกอบ @EnableResourceServer และการกำหนดค่ามีอยู่ในคลาส ResourceServerConfiguration

การกำหนดค่าที่จำเป็นเพียงอย่างเดียวที่นี่คือคำจำกัดความของการระบุทรัพยากรเพื่อให้ตรงกับการเข้าถึงของไคลเอ็นต์ที่กำหนดไว้ในคลาสก่อนหน้า

 package net.reliqs.gleeometer.security; import org.springframework.context.annotation.Configuration; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; @Configuration @EnableResourceServer public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { @Override public void configure(ResourceServerSecurityConfigurer resources) { resources.resourceId("api"); } }

องค์ประกอบการกำหนดค่าล่าสุดเกี่ยวกับคำจำกัดความของความปลอดภัยของเว็บแอปพลิเคชัน

ตั้งค่าความปลอดภัยของเว็บ

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

มันกำหนดสิ่งต่อไปนี้:

  • ผู้ให้บริการตรวจสอบสิทธิ์ที่จะใช้ กำหนด bean authenticationProvider
  • ตัวเข้ารหัสรหัสผ่านที่จะใช้กำหนด bean passwordEncoder
  • ตัวจัดการการตรวจสอบสิทธิ์ bean
  • การกำหนดค่าความปลอดภัยสำหรับพาธที่เผยแพร่โดยใช้ HttpSecurity
  • การใช้ AuthenticationEntryPoint แบบกำหนดเองเพื่อจัดการกับข้อความแสดงข้อผิดพลาดภายนอกตัวจัดการข้อผิดพลาด Spring REST มาตรฐาน ResponseEntityExceptionHandler
 package net.reliqs.gleeometer.security; import net.reliqs.gleeometer.errors.CustomAccessDeniedHandler; import net.reliqs.gleeometer.errors.CustomAuthenticationEntryPoint; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true) public class ServerSecurityConfig extends WebSecurityConfigurerAdapter { private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint; private final UserDetailsService userDetailsService; public ServerSecurityConfig(CustomAuthenticationEntryPoint customAuthenticationEntryPoint, @Qualifier("userService") UserDetailsService userDetailsService) { this.customAuthenticationEntryPoint = customAuthenticationEntryPoint; this.userDetailsService = userDetailsService; } @Bean public DaoAuthenticationProvider authenticationProvider() { DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setPasswordEncoder(passwordEncoder()); provider.setUserDetailsService(userDetailsService); return provider; } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity http) throws Exception { http .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/api/signin/**").permitAll() .antMatchers("/api/glee/**").hasAnyAuthority("ADMIN", "USER") .antMatchers("/api/users/**").hasAuthority("ADMIN") .antMatchers("/api/**").authenticated() .anyRequest().authenticated() .and().exceptionHandling().authenticationEntryPoint(customAuthenticationEntryPoint).accessDeniedHandler(new CustomAccessDeniedHandler()); } }

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

 package net.reliqs.gleeometer.security; import net.reliqs.gleeometer.users.User; import net.reliqs.gleeometer.users.UserRepository; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; @Service public class UserService implements UserDetailsService { private final UserRepository repository; public UserService(UserRepository repository) { this.repository = repository; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = repository.findByEmail(username).orElseThrow(() -> new RuntimeException("User not found: " + username)); GrantedAuthority authority = new SimpleGrantedAuthority(user.getRole().name()); return new org.springframework.security.core.userdetails.User(user.getEmail(), user.getPassword(), Arrays.asList(authority)); } }

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

REST Controller

ภายในตัวควบคุม REST เราสามารถพบสองวิธีในการใช้การควบคุมการเข้าถึงสำหรับแต่ละวิธีทรัพยากร:

  • การใช้อินสแตนซ์ของ OAuth2Authentication ส่งผ่านโดย Spring เป็นพารามิเตอร์
  • การใช้คำอธิบายประกอบ @PreAuthorize หรือ @PostAuthorize
 package net.reliqs.gleeometer.users; import lombok.extern.slf4j.Slf4j; import net.reliqs.gleeometer.errors.EntityNotFoundException; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.security.access.prepost.PostAuthorize; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.validation.ConstraintViolationException; import javax.validation.Valid; import javax.validation.constraints.Size; import java.util.HashSet; @RestController @RequestMapping("/api/users") @Slf4j @Validated class UserController { private final UserRepository repository; private final PasswordEncoder passwordEncoder; UserController(UserRepository repository, PasswordEncoder passwordEncoder) { this.repository = repository; this.passwordEncoder = passwordEncoder; } @GetMapping Page<User> all(@PageableDefault(size = Integer.MAX_VALUE) Pageable pageable, OAuth2Authentication authentication) { String auth = (String) authentication.getUserAuthentication().getPrincipal(); String role = authentication.getAuthorities().iterator().next().getAuthority(); if (role.equals(User.Role.USER.name())) { return repository.findAllByEmail(auth, pageable); } return repository.findAll(pageable); } @GetMapping("/search") Page<User> search(@RequestParam String email, Pageable pageable, OAuth2Authentication authentication) { String auth = (String) authentication.getUserAuthentication().getPrincipal(); String role = authentication.getAuthorities().iterator().next().getAuthority(); if (role.equals(User.Role.USER.name())) { return repository.findAllByEmailContainsAndEmail(email, auth, pageable); } return repository.findByEmailContains(email, pageable); } @GetMapping("/findByEmail") @PreAuthorize("!hasAuthority('USER') || (authentication.principal == #email)") User findByEmail(@RequestParam String email, OAuth2Authentication authentication) { return repository.findByEmail(email).orElseThrow(() -> new EntityNotFoundException(User.class, "email", email)); } @GetMapping("/{id}") @PostAuthorize("!hasAuthority('USER') || (returnObject != null && returnObject.email == authentication.principal)") User one(@PathVariable Long id) { return repository.findById(id).orElseThrow(() -> new EntityNotFoundException(User.class, "id", id.toString())); } @PutMapping("/{id}") @PreAuthorize("!hasAuthority('USER') || (authentication.principal == @userRepository.findById(#id).orElse(new net.reliqs.gleeometer.users.User()).email)") void update(@PathVariable Long id, @Valid @RequestBody User res) { User u = repository.findById(id).orElseThrow(() -> new EntityNotFoundException(User.class, "id", id.toString())); res.setPassword(u.getPassword()); res.setGlee(u.getGlee()); repository.save(res); } @PostMapping @PreAuthorize("!hasAuthority('USER')") User create(@Valid @RequestBody User res) { return repository.save(res); } @DeleteMapping("/{id}") @PreAuthorize("!hasAuthority('USER')") void delete(@PathVariable Long id) { if (repository.existsById(id)) { repository.deleteById(id); } else { throw new EntityNotFoundException(User.class, "id", id.toString()); } } @PutMapping("/{id}/changePassword") @PreAuthorize("!hasAuthority('USER') || (#oldPassword != null && !#oldPassword.isEmpty() && authentication.principal == @userRepository.findById(#id).orElse(new net.reliqs.gleeometer.users.User()).email)") void changePassword(@PathVariable Long id, @RequestParam(required = false) String oldPassword, @Valid @Size(min = 3) @RequestParam String newPassword) { User user = repository.findById(id).orElseThrow(() -> new EntityNotFoundException(User.class, "id", id.toString())); if (oldPassword == null || oldPassword.isEmpty() || passwordEncoder.matches(oldPassword, user.getPassword())) { user.setPassword(passwordEncoder.encode(newPassword)); repository.save(user); } else { throw new ConstraintViolationException("old password doesn't match", new HashSet<>()); } } }

บทสรุป

Spring Security และ Spring Boot อนุญาตให้ตั้งค่าเซิร์ฟเวอร์การอนุญาต/การตรวจสอบสิทธิ์ OAuth2 ที่สมบูรณ์อย่างรวดเร็วในลักษณะที่เกือบจะเปิดเผย คุณสามารถย่อการตั้งค่าเพิ่มเติมได้โดยกำหนดค่าคุณสมบัติของไคลเอ็นต์ OAuth2 โดยตรงจากไฟล์ application.properties/yml ตามที่อธิบายไว้ในบทช่วยสอนนี้

ซอร์สโค้ดทั้งหมดมีอยู่ในที่เก็บ GitHub นี้: spring-glee-o-meter ไคลเอนต์เชิงมุมที่ใช้ทรัพยากรที่เผยแพร่สามารถพบได้ในที่เก็บ GitHub นี้: glee-o-meter