การใช้ 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
โฟลว์แบบง่ายได้อธิบายไว้ด้านล่าง:
- คำขอการอนุญาตถูกส่งจากไคลเอนต์ไปยังเซิร์ฟเวอร์ (ทำหน้าที่เป็นเจ้าของทรัพยากร) โดยใช้การให้สิทธิ์รหัสผ่าน
- โทเค็นการเข้าถึงจะถูกส่งคืนไปยังไคลเอนต์ (พร้อมกับโทเค็นการรีเฟรช)
- โทเค็นการเข้าถึงจะถูกส่งจากไคลเอนต์ไปยังเซิร์ฟเวอร์ (ทำหน้าที่เป็นเซิร์ฟเวอร์ทรัพยากร) ในแต่ละคำขอสำหรับการเข้าถึงทรัพยากรที่มีการป้องกัน
- เซิร์ฟเวอร์ตอบสนองด้วยทรัพยากรที่มีการป้องกันที่จำเป็น
Spring Security และ Spring Boot
ก่อนอื่น แนะนำสั้นๆ เกี่ยวกับกลุ่มเทคโนโลยีที่เลือกสำหรับโครงการนี้
เครื่องมือการจัดการโครงการที่เลือกคือ Maven แต่เนื่องจากความเรียบง่ายของโครงการ การเปลี่ยนไปใช้เครื่องมืออื่นๆ เช่น Gradle ไม่ใช่เรื่องยาก
ในความต่อเนื่องของบทความ เราเน้นที่ด้าน Spring Security เท่านั้น แต่โค้ดที่ตัดตอนมาทั้งหมดนำมาจากแอปพลิเคชันฝั่งเซิร์ฟเวอร์ที่ทำงานได้อย่างสมบูรณ์ ซึ่งซอร์สโค้ดมีอยู่ในที่เก็บสาธารณะพร้อมกับไคลเอ็นต์ที่ใช้ทรัพยากร REST
Spring Security เป็นเฟรมเวิร์กที่ให้บริการการรักษาความปลอดภัยที่เกือบจะเปิดเผยสำหรับแอปพลิเคชันที่ใช้สปริง รากของมันมาจากการเริ่มต้นครั้งแรกของฤดูใบไม้ผลิและจัดเป็นชุดของโมดูลเนื่องจากมีเทคโนโลยีความปลอดภัยที่แตกต่างกันจำนวนมากครอบคลุม
มาดูสถาปัตยกรรม Spring Security กันอย่างรวดเร็ว (ดูคำแนะนำโดยละเอียดเพิ่มเติมได้ที่นี่)
ความปลอดภัยเป็นส่วนใหญ่เกี่ยวกับ การรับรองความถูกต้อง เช่น การยืนยันตัวตน และการ อนุญาต การให้สิทธิ์การเข้าถึงทรัพยากร
ความปลอดภัยของ Spring รองรับรูปแบบการพิสูจน์ตัวตนที่หลากหลาย ทั้งที่จัดหาให้โดยบุคคลที่สามหรือนำไปใช้โดยกำเนิด รายชื่อสามารถพบได้ที่นี่
เกี่ยวกับการอนุญาตมีการระบุพื้นที่หลักสามประการ:
- เว็บขออนุญาติ
- การอนุญาตระดับวิธีการ
- การเข้าถึงการอนุญาตอินสแตนซ์อ็อบเจ็กต์โดเมน
การตรวจสอบสิทธิ์
อินเทอร์เฟซพื้นฐานคือ 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
- ระบุทรัพยากรที่ลูกค้าเข้าถึงได้
- การเลือกใช้งานหน่วยความจำในหน่วยความจำหรือที่เก็บข้อมูลแบบ JDBC สำหรับรายละเอียดไคลเอ็นต์ด้วย
- ปลายทางเซิร์ฟเวอร์การให้สิทธิ์ (โดยใช้
AuthorizationServerEndpointsConfigurer
)- กำหนดการใช้โทเค็น JWT ด้วย
accessTokenConverter
- กำหนดการใช้อินเทอร์เฟซ
UserDetailsService
และAuthenticationManager
เพื่อดำเนินการรับรองความถูกต้อง (ในฐานะเจ้าของทรัพยากร)
- กำหนดการใช้โทเค็น JWT ด้วย
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