1. 概述
在 Web 应用开发中,基于用户角色和 HTTP 方法保护资源至关重要,这能防止未授权的访问和篡改。Spring Security 提供了灵活强大的机制,可以根据用户角色和 HTTP 请求类型限制或允许对特定接口的访问。Spring Security 的授权机制通过当前用户的角色或权限限制应用特定部分的访问。
本文将探讨如何使用 Spring Security 对特定 URL 和 HTTP 方法进行请求授权。我们将通过配置解析其底层原理,并在一个简单的博客平台中演示实现。
2. 项目搭建
实现功能前,需先搭建项目并添加必要依赖和配置。我们的示例博客平台需要满足以下需求:
- 公开注册接口(
/users/register
)无需认证 - 认证用户(
USER
角色)可创建、查看、更新和删除自己的文章 - 管理员(
ADMIN
角色)可删除任意文章 - 开发测试期间开放 H2 数据库控制台(
/h2-console
)访问权限
2.1. Maven 依赖
在 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 数据库需求:
spring.application.name=spring-security
spring.datasource.url=jdbc:h2:file:C:/projects/blog_db;DB_CLOSE_DELAY=-1;IFEXISTS=FALSE
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=admin123
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();
}
}
配置解析:
-
@Configuration
标识该类为 Spring 配置类 -
@EnableWebSecurity
启用 Spring Security 的 Web 安全支持 -
@EnableMethodSecurity
允许使用@PreAuthorize
等注解实现方法级安全 -
SecurityFilterChain
Bean 定制 HTTP 安全设置 - 禁用 CSRF 保护(适用于无状态 API 或开发环境)
- 禁用框架选项头以允许访问 H2 控制台(使用 iframe)
- 公开
/users/register
和/h2-console/**
接口无需认证 - 限制用户文章操作(GET/POST/PUT)仅
USER
角色可访问 - 允许
USER
和ADMIN
角色删除文章 - 其他所有请求需要认证
- 启用默认 HTTP 基础认证
- 使用 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;
// 构造器、getter/setter 省略
}
public class UserProfileDto {
private String username;
private String email;
private Role role;
// 构造器、getter/setter 省略
}
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
对象获取当前登录用户。
@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
和 ADMIN
都能访问删除接口,但代码确保普通用户只能删除自己的文章,只有管理员可删除他人文章。
控制器相关 DTO:
public class PostRequestDto {
private String title;
private String content;
// 构造器、getter/setter 省略
}
4.6. 用户服务实现
创建 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
实体的能力
4.7. 用户详情服务
实现自定义 UserDetailsService
使 Spring Security 能基于数据库认证用户:
@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();
}
}
实现要点:
- 实现
UserDetailsService
接口(Spring Security 核心接口) -
loadUserByUsername()
从数据库加载用户信息,不存在则抛出异常 - 构建返回 Spring Security 的
UserDetails
对象
4.8. 文章服务实现
PostService
处理文章管理业务逻辑:
@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());
}
}
服务核心功能:
- ✅ 创建文章:认证用户可创建新文章
- ✅ 更新文章:用户仅能更新自己的文章(否则拒绝访问)
- ✅ 删除文章:用户可删除自己的文章,管理员可删除任意文章
- ✅ 查看个人文章:用户获取自己的文章列表
通过 Authentication
确保所有操作遵循用户身份和基于角色的访问控制,实现业务逻辑与控制器逻辑分离,保持架构清晰可维护。
5. 总结
本文学习了在 Spring Boot 应用中通过配置 Spring Security 实现以下目标:
- ✅ 基于角色授予/限制特定接口访问权限
- ✅ 根据 HTTP 方法控制访问权限
- ✅ 使用
@PreAuthorize
实现方法级授权
这种架构不仅保障应用安全,还确保了基于角色的数据所有权和访问控制,在多用户系统中至关重要。
完整源代码可在 GitHub 获取。