1. 概述
在 Web 應用程序開發中,基於用户角色和 HTTP 方法保護資源對於防止未經授權的訪問和操縱至關重要。Spring Security 提供了一種靈活且強大的機制,可以根據用户角色和 HTTP 請求類型限制或允許對特定端點的訪問。Spring Security 中的授權會根據當前用户的角色或權限限制對應用程序的某些部分的訪問。
在本教程中,我們將探索如何使用 Spring Security 針對特定 URL 和 HTTP 方法進行授權。我們將學習配置、瞭解其背後的工作原理,並在一個簡單的博客平台上演示其實現。
2. Set up Project
在實施功能之前,我們必須使用必要的依賴項和配置設置我們的項目。 我們的示例博客平台需要:
- 允許公共註冊(/users/register)而無需身份驗證
- 允許具有 USER 角色的身份驗證用户創建、查看、更新和刪除他們的帖子
- 允許管理員(具有 ADMIN 角色的用户)刪除任何帖子
- 為開發和測試目的提供對 H2 數據庫控制枱的開放訪問(/h2-console)
2.1. Maven Dependency
讓我們首先確保 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. Application Properties
現在,讓我們為 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. Implementation
Now that we’ve finished the data model and security configuration, it’s time to implement the core application logic.
In this section, we’ll walk through how our app handles user registration, authentication, and post management, while enforcing method-level security based on user roles.
4.1. Register and Get Profile
We now implement a UserController to handle auth-related operations. The endpoints include:
- Registering a new user (POST
/users/register) - Retrieving the profile of the authenticated user (GET
/users/profile)
The registration endpoint is publicly accessible, while the profile endpoint requires authentication:
@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);
}
}
Now let’s create our DTOs:
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. Creating a Post
Let’s create a POST /posts/create endpoint to create a new post. Only users with the USER role are allowed to create posts:
@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);
}
}
This method delegates post creation to the service layer. It also uses the Spring Authentication object to identify the currently logged-in user.
@PreAuthorize annotation in Spring Security controls access before a method is executed. It checks whether the currently authenticated user has the required role or permission to access the method.
4.3. Listing Users’ Posts
Now, let’s create a GET /posts/mine endpoint to allow users to view only their posts:
@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. Updating a Post
Let’s create a PUT /posts/{id} endpoint so users can update their posts:
@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. Deleting a Post
Next, let’s create a DELETE /posts/{id} endpoint so users can delete their posts, and admins can delete any post:
@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);
}
}
We use @PreAuthorize to check roles on method-level, so only the USER role can get, update, or delete their posts unless they’re an admin. Only ADMINs are allowed to delete posts created by other users.
4.6. Creating a PostService
The PostService class handles all the business logic related to post management in the application. It interacts with the PostRepository for data persistence and with the UserService to retrieve authenticated user information. Let’s break down its implementation:
@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());
}
}
This PostService class handles:
- Creating posts to allow authenticated users to create a new post
- Updating posts to allow authenticated users to update their posts, attempting to update others’ posts results in an access denial
- Deleting posts allows users to delete their posts, while admins have the privilege to delete any post
- Viewing personal posts allows users to retrieve a list of their posts
The use of Authentication ensures that every operation respects user identity and role-based access control. This service layer separates business logic from controller logic, keeping the architecture clean and maintainable.
4.7. Creating a UserDetailService
To enable Spring Security to authenticate users based on the data in our database, we need to implement a custom UserDetailsService. This service is responsible for loading user-specific data during the authentication process. Here’s how we can achieve that using the CustomUserDetailService class:
@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();
}
}
This CustomUserDetailService class does the following:
- Implements
UserDetailsService, a core interface in Spring Security used to retrieve user information. - Inside
loadUserByUsername(), itfetches the user by username from the database. If the user doesn’t exist, it throws aUsernameNotFoundException. - Builds and returns a Spring Security
UserDetailsobject using the username, password, and role from ourUserentity.
By providing this custom implementation, Spring Security can integrate seamlessly with our application’s user data, enabling secure and role-based access control throughout the system.
5. 結論
在本文中,我們學習瞭如何通過配置 Spring Security,在 Spring Boot 應用程序中安全地保護 HTTP 請求:
- 根據角色授予或限制對特定端點的訪問權限
- 控制基於 HTTP 方法的訪問權限
- 使用 @PreAuthorize 進行方法級別的授權
這種結構不僅能保持應用程序的安全,還能確保基於角色的數據所有權和訪問控制,這對任何多用户系統都至關重要。