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 角色可访问
  • 允许 USERADMIN 角色删除文章
  • 其他所有请求需要认证
  • 启用默认 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 实现方法级角色检查,普通用户只能操作自己的文章(除非是管理员)。虽然 USERADMIN 都能访问删除接口,但代码确保普通用户只能删除自己的文章,只有管理员可删除他人文章。

控制器相关 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 获取。


原始标题:Authorize Request for Certain URL and HTTP Method in Spring Security | Baeldung