概述
在 Spring Security 的实战中,我们通常首先接触的是基于 URL 的访问控制,例如通过 antMatchers 限制某个接口的访问角色。但在复杂业务场景下,我们需要更细粒度的控制------不仅限制谁能调用某个接口,更要限制谁能执行某个 Service 方法或拿到返回结果。这就是方法级安全的用武之地。
本文将从概念、配置、核心注解到完整可运行代码,带你快速掌握 @PreAuthorize、@PostAuthorize 等方法级安全注解的用法,并给出清晰的授权流程分析。
纲要
- 方法级安全概述与启用配置
- 四个核心注解:
@PreAuthorize、@PostAuthorize、@PreFilter、@PostFilter的定位 @PreAuthorize与@PostAuthorize的执行流程与 AOP 拦截机制- 授权投票结果与
AccessDeniedException的产生条件 - 项目结构总览
- 完整可运行代码:从实体、Repository、Service 到 Controller 与安全配置
- 测试验证:方法前授权失败与通过场景,方法后授权通过与失败场景
- 最佳实践与使用建议
启用方法级安全
要使用这些注解,只需在一个配置类上添加 @EnableGlobalMethodSecurity 并开启 prePostEnabled = true。该注解会激活 Spring AOP 拦截器,自动处理带有安全注解的方法。
java
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig {
}
这一步之后,我们就可以在任意 Bean 的公共方法上使用以下注解:
@PreAuthorize-- 方法执行前鉴权@PostAuthorize-- 方法执行后鉴权(基于返回对象)@PreFilter-- 方法执行前对集合参数进行过滤@PostFilter-- 方法执行后对返回集合进行过滤
本文重点演示前两个注解的使用,后两个将在后续篇章介绍。
授权流程与 AOP 拦截原理
Spring Security 在方法级安全底层使用 AOP 切面,当一个方法被 @PreAuthorize 或 @PostAuthorize 标注时,会触发相应的 Advice。
流程时序图
PostInvocationAdvice Service PreInvocationAdvice MethodSecurityInterceptor Controller Client PostInvocationAdvice Service PreInvocationAdvice MethodSecurityInterceptor Controller Client #mermaid-svg-UWfWEKndWdD7snqc{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-UWfWEKndWdD7snqc .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-UWfWEKndWdD7snqc .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-UWfWEKndWdD7snqc .error-icon{fill:#552222;}#mermaid-svg-UWfWEKndWdD7snqc .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-UWfWEKndWdD7snqc .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-UWfWEKndWdD7snqc .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-UWfWEKndWdD7snqc .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-UWfWEKndWdD7snqc .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-UWfWEKndWdD7snqc .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-UWfWEKndWdD7snqc .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-UWfWEKndWdD7snqc .marker{fill:#333333;stroke:#333333;}#mermaid-svg-UWfWEKndWdD7snqc .marker.cross{stroke:#333333;}#mermaid-svg-UWfWEKndWdD7snqc svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-UWfWEKndWdD7snqc p{margin:0;}#mermaid-svg-UWfWEKndWdD7snqc .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-UWfWEKndWdD7snqc text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-UWfWEKndWdD7snqc .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-UWfWEKndWdD7snqc .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-UWfWEKndWdD7snqc .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-UWfWEKndWdD7snqc .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-UWfWEKndWdD7snqc #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-UWfWEKndWdD7snqc .sequenceNumber{fill:white;}#mermaid-svg-UWfWEKndWdD7snqc #sequencenumber{fill:#333;}#mermaid-svg-UWfWEKndWdD7snqc #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-UWfWEKndWdD7snqc .messageText{fill:#333;stroke:none;}#mermaid-svg-UWfWEKndWdD7snqc .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-UWfWEKndWdD7snqc .labelText,#mermaid-svg-UWfWEKndWdD7snqc .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-UWfWEKndWdD7snqc .loopText,#mermaid-svg-UWfWEKndWdD7snqc .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-UWfWEKndWdD7snqc .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-UWfWEKndWdD7snqc .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-UWfWEKndWdD7snqc .noteText,#mermaid-svg-UWfWEKndWdD7snqc .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-UWfWEKndWdD7snqc .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-UWfWEKndWdD7snqc .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-UWfWEKndWdD7snqc .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-UWfWEKndWdD7snqc .actorPopupMenu{position:absolute;}#mermaid-svg-UWfWEKndWdD7snqc .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-UWfWEKndWdD7snqc .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-UWfWEKndWdD7snqc .actor-man circle,#mermaid-svg-UWfWEKndWdD7snqc line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-UWfWEKndWdD7snqc :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} alt前置投票通过前置投票拒绝 HTTP Request调用 Service 方法执行前置投票赞成票执行目标方法返回结果执行后置投票(如有注解)投票结果返回结果或异常否决票AccessDeniedExceptionResponse
Spring Security 内置的 AffirmativeBased 决策管理器要求至少有一票赞成才能通过。如果前置投票器(例如 PreInvocationAuthorizationAdviceVoter)返回否决票(-1),而其他投票器弃权,授权将失败,抛出 AccessDeniedException。
项目结构
本次示例采用 Spring Boot + Spring Data JPA + H2 内存数据库,方便直接运行。
dir
src/main/java/com/example/security
├── config
│ └── SecurityConfig.java
│ └── MethodSecurityConfig.java
├── entity
│ └── UserEntity.java
├── repository
│ └── UserRepository.java
├── service
│ └── UserService.java
├── controller
│ └── UserController.java
└── SecurityApplication.java
src/main/resources
└── application.properties
完整可运行代码
1. 依赖与入口
pom.xml 中需包含以下主要依赖(基于 Spring Boot 2.7):
spring-boot-starter-webspring-boot-starter-securityspring-boot-starter-data-jpah2
入口类保持默认即可。
2. 实体 UserEntity
java
package com.example.security.entity;
import javax.persistence.*;
@Entity
@Table(name = "users")
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String email;
private String role; // ROLE_ADMIN, ROLE_USER
public UserEntity() {}
public UserEntity(String username, String email, String role) {
this.username = username;
this.email = email;
this.role = role;
}
// getters and setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getRole() { return role; }
public void setRole(String role) { this.role = role; }
}
3. Repository
java
package com.example.security.repository;
import com.example.security.entity.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<UserEntity, Long> {
Optional<UserEntity> findByEmail(String email);
}
4. Service
java
package com.example.security.service;
import com.example.security.entity.UserEntity;
import com.example.security.repository.UserRepository;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
// 只有 ROLE_ADMIN 可以调用
@PreAuthorize("hasRole('ADMIN')")
public String getAdminOnlyResource() {
return "Sensitive admin data";
}
// 方法执行后,判断返回对象的 username 是否与当前认证用户一致
@PostAuthorize("returnObject.username == authentication.name")
public UserEntity getUserByEmail(String email) {
Optional<UserEntity> userOpt = userRepository.findByEmail(email);
return userOpt.orElse(null);
}
public Optional<UserEntity> findByEmail(String email) {
return userRepository.findByEmail(email);
}
}
5. Controller
java
package com.example.security.controller;
import com.example.security.entity.UserEntity;
import com.example.security.service.UserService;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
// 该接口本身允许匿名访问,但调用的 Service 方法有 @PreAuthorize
@GetMapping("/admin/resource")
public String getAdminResource() {
return userService.getAdminOnlyResource();
}
// 通过 email 查询用户,Service 方法上有 @PostAuthorize
@GetMapping("/users/by-email")
public UserEntity getUserByEmail(@RequestParam String email) {
return userService.getUserByEmail(email);
}
}
6. 安全配置
java
package com.example.security.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
// URL 级别:允许匿名访问这两个路径(方法级会做进一步控制)
.antMatchers("/api/admin/resource", "/api/users/by-email").permitAll()
.anyRequest().authenticated()
)
.httpBasic(); // 便于测试
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails admin = User.withDefaultPasswordEncoder()
.username("admin")
.password("1234")
.roles("ADMIN")
.build();
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("1234")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(admin, user);
}
}
7. 启用方法级安全
java
package com.example.security.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig {
}
8. 初始化数据(可选测试用)
在 SecurityApplication 中添加 CommandLineRunner 向数据库插入用户数据:
java
package com.example.security;
import com.example.security.entity.UserEntity;
import com.example.security.repository.UserRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class SecurityApplication {
public static void main(String[] args) {
SpringApplication.run(SecurityApplication.class, args);
}
@Bean
CommandLineRunner init(UserRepository repo) {
return args -> {
repo.save(new UserEntity("admin", "admin@example.com", "ROLE_ADMIN"));
repo.save(new UserEntity("user", "user@example.com", "ROLE_USER"));
};
}
}
9. application.properties
properties
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.h2.console.enabled=true
测试与验证
@PreAuthorize 场景
1. 普通用户调用 /api/admin/resource
URL 允许匿名访问,但 Service 方法要求 ROLE_ADMIN。
以 user:1234 身份通过 HTTP Basic 请求,将得到 403 Forbidden,日志中可见 PreInvocationAuthorizationAdviceVoter 返回否决票,最终抛出 AccessDeniedException。
2. admin 用户调用
admin:1234 请求,返回 Sensitive admin data。
@PostAuthorize 场景
1. admin 查询自己的信息
/api/users/by-email?email=admin@example.com,认证用户 admin。
方法执行后返回 UserEntity,其 username 为 admin,与 authentication.name 一致,授权通过。
2. admin 查询 user 的信息
/api/users/by-email?email=user@example.com,认证用户 admin。
方法执行后返回对象 username 为 user,与当前认证名 admin 不匹配,后置投票拒绝,返回 403。
对比与使用建议
| 注解 | 执行时机 | 适用场景 | 注意事项 |
|---|---|---|---|
@PreAuthorize |
方法执行前 | 权限检查、角色验证、参数值判断 | 推荐优先使用,能避免不必要的业务逻辑执行 |
@PostAuthorize |
方法执行后 | 基于返回结果的安全判断,如数据归属校验 | 方法体已执行,若涉及写操作可能已产生副作用,不建议用于增删改 |
@PreFilter |
方法执行前 | 过滤集合类型的入参 | 后续文章详述 |
@PostFilter |
方法执行后 | 过滤集合类型的返回值 | 后续文章详述 |
总结
本文从 Spring Security 的方法级安全配置出发,详细剖析了 @PreAuthorize 与 @PostAuthorize 的工作原理、投票流程及实战代码。
通过完整的可运行示例,你可以直观感受到细粒度方法控制带来的安全增强效果。在生产实践中,应优先采用 @PreAuthorize 防止危险操作发生,仅在对只读操作做所有权验证时才谨慎使用 @PostAuthorize。