1. 概述
基於用户角色和 HTTP 方法在 Web 應用程序開發中對資源進行安全保護,對於防止未經授權的訪問和操作至關重要。Spring Security 提供了一種靈活且強大的機制,可以根據用户角色和 HTTP 請求類型限制或允許對特定端點的訪問。Spring Security 中的授權 限制 根據當前用户的角色或權限對應用程序的某些部分訪問。
在本教程中,我們將探索如何使用 Spring Security 針對特定 URL 和 HTTP 方法進行授權。我們將學習配置、瞭解其背後的工作原理,並在一個簡單的博客平台上演示其實現。
2. 項目設置
在實施功能之前,必須使用必要的依賴項和配置設置好我們的項目。 我們的示例博客平台需要:
- 允許公共註冊(/users/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 的 Web 應用程序的安全設置,使用 Spring Security。 讓我們逐步瞭解此配置的作用:
- @Configuration 表示此類提供 Spring 配置
- @EnableWebSecurity 啓用 Spring Security 的 Web 安全支持
- @EnableMethodSecurity 允許使用註解(如 @PreAuthorize)進行方法級別安全設置
- SecurityFilterChain Bean 自定義 HTTP 安全設置,在 HttpSecurity 對象中
- 禁用 CSRF 保護,這通常在無狀態 API 或開發期間完成
- 禁用 Frame Options 頭部,允許訪問 H2 控制枱,該控制枱使用 iframe
- 允許未身份驗證訪問 /users/** 端點(例如註冊)和 /h2-console/** 端點,用於嵌入的 H2 數據庫控制枱
- 限制對用户特定操作的訪問 (GET, POST, PUT),這些操作對具有 USER 角色的用户
- 允許具有 USER 或 ADMIN 角色的用户刪除帖子
- 要求對未明確提及的任何其他請求進行身份驗證
- 啓用基本 HTTP 身份驗證,具有默認設置
- 聲明一個 PasswordEncoder Bean,使用 BCrypt,這是一個用於安全地哈希密碼的算法
此配置確保應用程序在端點上具有適當的訪問控制,尤其是在區分公共和受保護路由以及對與操作相關的角色基於訪問權限方面,並強制執行設置。
4. 實現
現在我們已經完成了數據模型和安全配置,是時候實施核心應用程序邏輯。
在這一部分,我們將介紹我們的應用程序如何處理用户註冊、身份驗證和帖子管理,同時根據用户角色執行基於方法的安全策略。
4.1. 註冊與獲取個人資料
我們現在實現一個 UserController 用於處理認證相關操作。 接口包括:
- 註冊新用户 (POST users/register)
- 獲取已認證用户個人資料 (GET users/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 註解在執行方法之前控制訪問權限。它檢查當前身份驗證的用户是否具有訪問該方法的所需角色或權限。
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 角色可以訪問刪除端點,但代碼確保普通用户只能刪除自己的帖子。 只有 ADMINs 才能刪除其他用户創建的帖子。
現在,讓我們為這個控制器創建我們的 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. 創建<em 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。
- 構建並返回 一個 Spring Security 的 UserDetails 對象,該對象使用來自我們 User 實體中的用户名、密碼和角色。
通過提供此自定義實現,Spring Security 可以無縫地與我們應用程序的用户數據集成,從而在整個系統中啓用安全和基於角色的訪問控制。
4.8. 創建 PostService
<em>PostService</em> 類處理應用程序中所有與帖子管理相關的業務邏輯。它與 <em>PostRepository</em> 進行數據持久化交互,並與 <em>UserService</em> 檢索經過身份驗證的用户信息。下面我們來分解其實現:
@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 類負責以下操作:
- 創建帖子,允許經過身份驗證的用户創建新的帖子
- 更新帖子,允許經過身份驗證的用户更新他們的帖子,嘗試更新其他用户的帖子會導致訪問拒絕
- 刪除帖子,允許用户刪除他們的帖子,而管理員擁有刪除任何帖子的權限
- 查看個人帖子,允許用户檢索他們自己的帖子列表
使用 身份驗證 確保每項操作都尊重用户身份和基於角色的訪問控制。該服務層將業務邏輯與控制器邏輯分離,保持架構的清潔和可維護性。
5. 結論
在本文中,我們學習瞭如何通過配置 Spring Security 來保護 Spring Boot 應用程序中的 HTTP 請求:
- 根據角色授予或限制對特定端點的訪問權限
- 控制基於 HTTP 方法的訪問權限
- 使用 @PreAuthorize 在方法級別應用授權
這種結構不僅能確保應用程序的安全,還能確保基於角色的正確數據所有權和訪問控制,這對任何多用户系統都至關重要。