在 Spring Security 中授權特定 URL 和 HTTP 方法的請求
1.概述
在 Web 應用程式開發中,基於使用者角色和 HTTP 方法保護資源對於防止未經授權的存取和操作至關重要。 Spring Security 提供了一種靈活且強大的機制,可根據使用者角色和 HTTP 請求類型限製或允許對某些端點的存取。 Spring Security 中的授權機制會根據目前使用者的角色或權限限制對應用程式某些部分的存取。
在本教學中,我們將探討如何使用 Spring Security 授權特定 URL 和 HTTP 方法的請求。我們將介紹配置過程,了解其幕後工作原理,並在一個簡單的部落格平台中演示其實現。
2. 設定項目
在實現這些功能之前,我們必須先為專案設定必要的依賴項和配置。我們的範例部落格平台需要:
- 允許無需身份驗證的公開
/users/register
register ) - 允許經過身份驗證的用戶(具有
USER
角色)創建、查看、更新和刪除他們的帖子 - 允許管理員(具有
ADMIN
角色)刪除任何帖子 - 為開發和測試目的提供對 H2 資料庫控制台 (
/h2-console
) 的開放訪問
2.1. Maven依賴
首先確保將spring-boot-starter-security
、 spring-boot-starter-data-jpa
、spring-boot-starter-web
和h2-database
加入我們的pom.xml
檔案中的項目:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>3.4.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>3.4.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>3.4.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.4.4</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.3.232</version>
</dependency>
2.2. 應用程式屬性
現在,讓我們根據 H2 資料庫需求設定application.properties
檔案:
spring.application.name=spring-security
spring.datasource.url=jdbc:h2:file:C:/your_folder_here/test;DB_CLOSE_DELAY=-1;IFEXISTS=FALSE
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=qwerty
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
3.配置
讓我們定義一個SecurityConfig
類別來控制對特定 URL 和 HTTP 方法的存取:
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
.authorizeHttpRequests(auth -> auth
.requestMatchers(new AntPathRequestMatcher("/users/register")).permitAll()
.requestMatchers(new AntPathRequestMatcher("/h2-console/**")).permitAll()
.requestMatchers(HttpMethod.GET, "/users/profile").hasAnyRole("USER", "ADMIN")
.requestMatchers(HttpMethod.GET, "/posts/mine").hasRole("USER")
.requestMatchers(HttpMethod.POST, "/posts/create").hasRole("USER")
.requestMatchers(HttpMethod.PUT, "/posts/**").hasRole("USER")
.requestMatchers(HttpMethod.DELETE, "/posts/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults());
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
此SecurityConfig
類別使用 Spring Security 配置基於 Spring 的 Web 應用程式的安全設定。讓我們來看看這個配置的作用:
-
@Configuration
表示該類別提供 Spring 配置 -
@EnableWebSecurity
啟用 Spring Security 的 Web 安全支援 -
@EnableMethodSecurity
允許使用 @PreAuthorize 等註解實現方法級安全性 -
SecurityFilterChain
bean 自訂HttpSecurity
物件中的 HTTP 安全設定 - 停用 CSRF 保護,這通常針對無狀態 API 或在開發期間完成
- 停用框架選項標頭以允許存取使用 iframe 的 H2 控制台
- 允許未經身份驗證存取
/users/**
端點(例如註冊)和嵌入式 H2 資料庫控制台的/h2-console/**
端點 - 限制具有
USER
角色的使用者存取特定於使用者的貼文操作(GET, POST, PUT)
- 允許具有
USER
或ADMIN
角色的使用者刪除帖子 - 對於未明確提及的任何其他請求,都需要進行身份驗證
- 使用預設設定啟用基本 HTTP 驗證
- 使用
BCrypt
聲明一個PasswordEncoder
bean,這是一種用於雜湊密碼的安全演算法
此配置可確保應用程式對端點具有適當的存取控制,特別是區分公共路由和受保護路由,並強制執行基於角色的存取以執行與貼文相關的操作。
4. 實施
現在我們已經完成了資料模型和安全性配置,現在是時候實現核心應用程式邏輯了。
在本節中,我們將介紹我們的應用程式如何處理使用者註冊、身份驗證和貼文管理,同時根據使用者角色實施方法層級的安全性。
4.1. 註冊並取得個人資料
現在,我們實作一個UserController
來處理與驗證相關的操作。端點包括:
-
/users/register
register ) - 檢索已驗證
/users/profile
profile )
註冊端點是公開可存取的,而設定檔端點需要身份驗證:
@RestController
@RequestMapping("users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@PostMapping("register")
public ResponseEntity<String> register(@RequestBody RegisterRequestDto request) {
String result = userService.register(request);
return new ResponseEntity<>(result, HttpStatus.OK);
}
@GetMapping("profile")
@PreAuthorize("hasAnyRole('USER', 'ADMIN')")
public ResponseEntity<UserProfileDto> profile(Authentication authentication) {
UserProfileDto userProfileDto = userService.profile(authentication.getName());
return new ResponseEntity<>(userProfileDto, HttpStatus.OK);
}
}
現在讓我們創建我們的 DTO:
public class RegisterRequestDto {
private String username;
private String email;
private String password;
private Role role;
// constructor here
// setter and getter here
}
public class UserProfileDto {
private String username;
private String email;
private Role role;
// constructor here
// setter and getter here
}
4.2. 創建帖子
讓我們建立一個 POST /posts/create
端點來建立新貼文。只有具有USER
角色的使用者才允許建立貼文:
@RestController
@RequestMapping("posts")
public class PostController {
private final PostService postService;
public PostController(PostService postService) {
this.postService = postService;
}
@PostMapping("create")
@PreAuthorize("hasRole('USER')")
public ResponseEntity<PostResponseDto> create(@RequestBody PostRequestDto dto, Authentication auth) {
PostResponseDto result = postService.create(dto, auth.getName());
return new ResponseEntity<>(result, HttpStatus.CREATED);
}
}
此方法將貼文建立委託給服務層。它還使用 Spring Authentication
物件來識別目前登入的使用者。
Spring Security 中的 @PreAuthorize註解**@PreAuthorize
方法執行之前控制存取**。它檢查目前經過身份驗證的使用者是否具有存取該方法所需的角色或權限。
4.3. 列出用戶的帖子
現在,讓我們建立一個 GET /posts/mine
端點,以允許用戶只查看他們的貼文:
@GetMapping("mine")
@PreAuthorize("hasRole('USER')")
public ResponseEntity<List<PostResponseDto>> myPosts(Authentication auth) {
List<PostResponseDto> result = postService.myPosts(auth.getName());
return new ResponseEntity<>(result, HttpStatus.OK);
}
4.4. 更新帖子
讓我們建立一個 PUT /posts/{id}
端點,以便使用者可以更新他們的貼文:
@PutMapping("{id}")
@PreAuthorize("hasRole('USER')")
public ResponseEntity<String> update(@PathVariable Long id, @RequestBody PostRequestDto req, Authentication auth) {
try {
postService.update(id, req, auth.getName());
return new ResponseEntity<>("updated", HttpStatus.OK);
} catch (AccessDeniedException ade) {
return new ResponseEntity<>(ade.getMessage(), HttpStatus.FORBIDDEN);
}
}
4.5. 刪除帖子
接下來,讓我們建立一個 DELETE /posts/{id}
端點,以便使用者可以刪除他們的帖子,管理員可以刪除任何帖子:
@DeleteMapping("{id}")
@PreAuthorize("hasAnyRole('USER', 'ADMIN')")
public ResponseEntity<?> delete(@PathVariable Long id, Authentication auth) {
try {
boolean isAdmin = auth.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
postService.delete(id, isAdmin, auth.getName());
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
} catch (AccessDeniedException ade) {
return new ResponseEntity<>(ade.getMessage(), HttpStatus.FORBIDDEN);
} catch (NoSuchElementException nse) {
return new ResponseEntity<>(nse.getMessage(), HttpStatus.NOT_FOUND);
}
}
我們使用@PreAuthorize
在方法層級檢查角色,因此只有USER
角色可以取得、更新或刪除其帖子,除非他們是管理員。在此範例中, USER
和ADMIN
角色可以存取刪除端點,但程式碼確保普通使用者只能刪除自己的貼文。只有 ADMIN 角色可以刪除其他使用者建立的貼文。
現在,讓我們為該控制器建立 DTO:
public class PostRequestDto {
private String title;
private String content;
// constructor here
// setter and getter here
}
4.6. 建立UserService
我們建立一個服務類別來實現身份驗證邏輯,以處理與使用者相關的操作,例如註冊和個人資料檢索。下面是UserService
類別的實現,它提供了註冊新使用者和根據身份驗證獲取使用者詳細資訊的方法:
@Service
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
public String register(RegisterRequestDto request) {
if (userRepository.findByUsername(request.getUsername()).isPresent()) {
return "Username already exists";
}
User user = new User();
user.setUsername(request.getUsername());
user.setEmail(request.getEmail());
user.setPassword(passwordEncoder.encode(request.getPassword()));
user.setRole(request.getRole());
userRepository.save(user);
return "User registered successfully";
}
public UserProfileDto profile(String username) {
Optional<User> user = userRepository.findByUsername(username);
return user.map(value -> new UserProfileDto(value.getUsername(), value.getEmail(), value.getRole())).orElseThrow();
}
public User getUser(String username) {
Optional<User> user = userRepository.findByUsername(username);
return user.orElse(null);
}
}
該服務執行三個主要功能:
-
register()
檢查使用者名稱是否已被使用,使用BCrypt
對密碼進行雜湊處理,並將新使用者儲存到資料庫 -
profile()
從Authentication
物件中提取目前使用者的身份,並將其對應到UserProfileDto
-
getUser()
提供對User
實體的直接訪問,這在需要完整User
物件的應用程式的其他部分非常有用
有了這項服務,我們可以將用戶註冊和設定檔功能整合到我們的控制器中,並確保安全處理密碼等敏感資料。
4.7. 建立UserDetailService
為了讓 Spring Security 能夠根據資料庫中的資料對使用者進行身份驗證,我們需要實作一個自訂的UserDetailsService
。此服務負責在身份驗證過程中載入特定於使用者的資料。以下是使用CustomUserDetailService
類別實現此目的的方法:
@Service
public class CustomUserDetailService implements UserDetailsService {
private final UserRepository userRepository;
public CustomUserDetailService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
return org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(user.getPassword())
.roles(user.getRole().name())
.build();
}
}
此CustomUserDetailService
類別執行下列操作:
- 實作
UserDetailsService
,這是 Spring Security 中用於檢索使用者資訊的核心介面。 - 在
loadUserByUsername()
方法中,它會根據使用者名稱從資料庫中取得使用者。如果使用者不存在,則會拋出UsernameNotFoundException
。 - 使用來自
User
實體的使用者名稱、密碼和角色建置並傳回 Spring SecurityUserDetails
物件。
透過提供這種自訂實現,Spring Security 可以與我們應用程式的用戶資料無縫集成,從而實現整個系統的安全性和基於角色的存取控制。
4.8. 建立PostService
PostService
類別負責處理應用程式中與貼文管理相關的所有業務邏輯。它與PostRepository
互動以實現資料持久化,並與UserService
互動以獲取已認證的使用者資訊。讓我們分解一下它的實作:
@Service
public class PostService {
private final PostRepository postRepository;
private final UserService userService;
public PostService(PostRepository postRepository, UserService userService) {
this.postRepository = postRepository;
this.userService = userService;
}
public PostResponseDto create(PostRequestDto req, String username) {
User user = userService.getUser(username);
Post post = new Post();
post.setTitle(req.getTitle());
post.setContent(req.getContent());
post.setUser(user);
return toDto(postRepository.save(post));
}
public void update(Long id, PostRequestDto dto, String username) {
Post post = postRepository.findById(id).orElseThrow();
if (!post.getUser().getUsername().equals(username)) {
throw new AccessDeniedException("You can only edit your own posts");
}
post.setTitle(dto.getTitle());
post.setContent(dto.getContent());
postRepository.save(post);
}
public void delete(Long id, boolean isAdmin, String username) {
Post post = postRepository.findById(id).orElseThrow();
if (!isAdmin && !post.getUser().getUsername().equals(username)) {
throw new AccessDeniedException("You can only delete your own posts");
}
postRepository.delete(post);
}
public List<PostResponseDto> myPosts(String username) {
User user = userService.getUser(username);
return postRepository.findByUser(user).stream().map(this::toDto).toList();
}
private PostResponseDto toDto(Post post) {
return new PostResponseDto(post.getId(), post.getTitle(), post.getContent(), post.getUser().getUsername());
}
}
此PostService
類別處理:
- 建立帖子以允許經過身份驗證的用戶建立新帖子
- 更新帖子以允許經過身份驗證的用戶更新自己的帖子,嘗試更新其他人的帖子會導致訪問被拒絕
- 刪除帖子允許用戶刪除自己的帖子,而管理員則有權刪除任何帖子
- 查看個人貼文允許用戶檢索其貼文列表
使用Authentication
可確保每個操作都尊重使用者身份和基於角色的存取控制。此服務層將業務邏輯與控制器邏輯分離,從而保持架構的簡潔性和可維護性。
5. 結論
在本文中,我們學習如何透過設定 Spring Security 來保護 Spring Boot 應用程式中的 HTTP 請求:
- 根據角色授予或限制對特定端點的存取權限
- 根據 HTTP 方法控制存取
- 使用
@PreAuthorize
應用方法級授權
這種結構不僅可以保證應用程式的安全,還可以確保基於角色的正確資料所有權和存取控制,這對於任何多用戶系統都是至關重要的。
與往常一樣,本文的源代碼可在 GitHub 上找到。