方法级安全
除了请求级别的授权控制,Spring Security 还提供了方法级别的安全控制能力。通过注解,可以在服务层方法上实现细粒度的权限控制,支持基于方法参数和返回值的复杂授权逻辑。本章将详细介绍方法级安全的配置和使用。
启用方法级安全
基本配置
在 Spring Security 6 中,使用 @EnableMethodSecurity 注解启用方法级安全:
@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {
// 配置内容
}
这个注解替代了旧版本的 @EnableGlobalMethodSecurity,提供了更现代化的配置方式。
启用选项
@EnableMethodSecurity 支持启用不同的注解类型:
@Configuration
@EnableMethodSecurity(
prePostEnabled = true, // 启用 @PreAuthorize、@PostAuthorize 等(默认 true)
securedEnabled = true, // 启用 @Secured 注解(默认 false)
jsr250Enabled = true // 启用 @RolesAllowed 等 JSR-250 注解(默认 false)
)
public class MethodSecurityConfig {
}
授权注解详解
@PreAuthorize:方法调用前授权
@PreAuthorize 在方法执行前进行授权检查,如果授权失败,方法不会被调用。
@Service
public class DocumentService {
// 检查角色
@PreAuthorize("hasRole('ADMIN')")
public Document getAdminDocument(Long id) {
return documentRepository.findById(id);
}
// 检查权限
@PreAuthorize("hasAuthority('document:read')")
public Document readDocument(Long id) {
return documentRepository.findById(id);
}
// 组合条件
@PreAuthorize("hasRole('ADMIN') or hasAuthority('document:read')")
public List<Document> listDocuments() {
return documentRepository.findAll();
}
// 使用方法参数
@PreAuthorize("#id == authentication.principal.id")
public Document getMyDocument(Long id) {
return documentRepository.findById(id);
}
// 调用 Bean 方法
@PreAuthorize("@documentService.canAccess(#id, authentication)")
public Document getDocument(Long id) {
return documentRepository.findById(id);
}
}
@PostAuthorize:方法返回后授权
@PostAuthorize 在方法执行后进行授权检查,可以基于返回值进行判断。如果授权失败,已执行的方法结果会被丢弃。
@Service
public class DocumentService {
// 检查返回值的所有者
@PostAuthorize("returnObject.owner == authentication.name")
public Document getDocument(Long id) {
return documentRepository.findById(id);
}
// 检查返回值属性
@PostAuthorize("returnObject.status == 'PUBLISHED' or hasRole('ADMIN')")
public Document getPublishedDocument(Long id) {
return documentRepository.findById(id);
}
// 组合使用
@PreAuthorize("hasAuthority('document:read')")
@PostAuthorize("returnObject.owner == authentication.name or hasRole('ADMIN')")
public Document readDocument(Long id) {
return documentRepository.findById(id);
}
}
returnObject 是内置变量,代表方法的返回值。
@PreFilter:过滤方法参数
@PreFilter 在方法执行前过滤集合类型的参数,只保留满足条件的元素。
@Service
public class DocumentService {
// 过滤输入集合
@PreFilter("filterObject.owner == authentication.name")
public void updateDocuments(List<Document> documents) {
// documents 集合已被过滤,只包含当前用户拥有的文档
documentRepository.saveAll(documents);
}
// 指定过滤的参数
@PreFilter(value = "filterObject.owner == authentication.name", filterTarget = "documents")
public void processDocuments(List<Document> documents, String operation) {
// ...
}
}
filterObject 是内置变量,代表集合中的每个元素。
@PostFilter:过滤方法返回值
@PostFilter 在方法返回后过滤集合类型的返回值,只保留满足条件的元素。
@Service
public class DocumentService {
// 过滤返回集合
@PostFilter("filterObject.owner == authentication.name")
public List<Document> listAllDocuments() {
return documentRepository.findAll();
// 返回的列表会被过滤,只包含当前用户拥有的文档
}
// 过滤 Map 返回值
@PostFilter("filterObject.value.owner == authentication.name")
public Map<Long, Document> getDocumentMap(List<Long> ids) {
return documentRepository.findByIds(ids);
}
}
@Secured:简单角色检查
@Secured 是较旧的注解,只支持角色检查,功能有限:
@Service
public class AdminService {
@Secured("ROLE_ADMIN")
public void performAdminTask() {
// 仅 ADMIN 角色可访问
}
@Secured({"ROLE_ADMIN", "ROLE_MANAGER"})
public void performManagerTask() {
// ADMIN 或 MANAGER 角色可访问
}
}
需要启用 securedEnabled = true。
JSR-250 注解
Spring Security 支持 JSR-250 标准注解:
@Service
public class UserService {
@RolesAllowed("ADMIN")
public void deleteUser(Long id) {
// 仅 ADMIN 角色可访问
}
@PermitAll
public List<User> listUsers() {
// 允许所有已认证用户访问
}
@DenyAll
public void dangerousOperation() {
// 拒绝所有访问
}
}
需要启用 jsr250Enabled = true。
SpEL 表达式详解
方法安全注解使用 Spring Expression Language (SpEL) 编写授权规则。
内置表达式
| 表达式 | 说明 |
|---|---|
hasRole(String) | 拥有指定角色 |
hasAnyRole(String...) | 拥有任一角色 |
hasAuthority(String) | 拥有指定权限 |
hasAnyAuthority(String...) | 拥有任一权限 |
permitAll | 允许所有 |
denyAll | 拒绝所有 |
isAnonymous() | 是否匿名用户 |
isAuthenticated() | 是否已认证 |
isRememberMe() | 是否通过记住我登录 |
isFullyAuthenticated() | 是否完全认证(非记住我) |
principal | 当前用户主体 |
authentication | 当前认证对象 |
访问方法参数
使用 #参数名 访问方法参数:
@PreAuthorize("#userId == authentication.principal.id")
public User getUser(Long userId) {
return userRepository.findById(userId);
}
@PreAuthorize("#user.id == authentication.principal.id")
public void updateUser(User user) {
userRepository.save(user);
}
// 多个参数
@PreAuthorize("#document.owner == authentication.name and #operation == 'read'")
public void processDocument(Document document, String operation) {
// ...
}
访问认证信息
@PreAuthorize("authentication.principal.username == #username")
public User getProfile(String username) {
return userRepository.findByUsername(username);
}
@PreAuthorize("authentication.credentials == null") // 记住我登录
public User getBasicInfo(Long id) {
return userRepository.findById(id);
}
调用外部 Bean
在表达式中调用其他 Bean 的方法:
@Service
public class SecurityService {
public boolean hasAccessToDocument(Long documentId, Authentication authentication) {
// 复杂的权限检查逻辑
Document document = documentRepository.findById(documentId);
User user = (User) authentication.getPrincipal();
return document.getOwner().equals(user) ||
document.getSharedUsers().contains(user) ||
user.getRoles().contains("ROLE_ADMIN");
}
public boolean belongsToDepartment(Long departmentId, Authentication authentication) {
User user = (User) authentication.getPrincipal();
return user.getDepartment().getId().equals(departmentId);
}
}
@Service
public class DocumentService {
@PreAuthorize("@securityService.hasAccessToDocument(#id, authentication)")
public Document getDocument(Long id) {
return documentRepository.findById(id);
}
}
逻辑运算
@PreAuthorize("hasRole('ADMIN') and @securityService.isWorkingHours()")
public void performDaytimeAdminTask() {
// ...
}
@PreAuthorize("hasRole('USER') or hasRole('GUEST')")
public List<Document> getPublicDocuments() {
// ...
}
@PreAuthorize("!isAnonymous()")
public UserProfile getProfile() {
// ...
}
自定义注解
元注解
可以创建自定义注解来简化重复的授权规则:
// 定义元注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('ADMIN')")
public @interface IsAdmin {
}
// 定义带参数的元注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('ROLE_' + #value)")
public @interface HasRole {
String value();
}
// 使用元注解
@Service
public class AdminService {
@IsAdmin
public void deleteAllUsers() {
// ...
}
@HasRole("MANAGER")
public void manageTeam() {
// ...
}
}
模板化元注解
Spring Security 6 支持模板化的元注解:
@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {
@Bean
static AnnotationTemplateExpressionDefaults templateExpressionDefaults() {
return new AnnotationTemplateExpressionDefaults();
}
}
// 定义模板化注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAuthority('{authority}')")
public @interface HasPermission {
String authority();
}
// 使用
@HasPermission("document:read")
public Document readDocument(Long id) {
// ...
}
类级别注解
注解可以放在类上,应用到所有方法:
@Service
@PreAuthorize("hasRole('USER')")
public class DocumentService {
// 继承类级别注解
public List<Document> listMyDocuments() {
// ...
}
// 方法级别覆盖类级别
@PreAuthorize("hasRole('ADMIN')")
public List<Document> listAllDocuments() {
// ...
}
// 不受保护(注意:不加注解并不意味着不受保护)
public Document getPublicDocument(Long id) {
// 这个方法仍然受类级别注解保护
}
}
方法安全与请求安全的关系
区别与配合
| 特性 | 请求级安全 | 方法级安全 |
|---|---|---|
| 控制粒度 | 粗粒度(URL) | 细粒度(方法) |
| 配置方式 | 集中式配置 | 注解式配置 |
| 表达能力 | 基于请求属性 | 基于方法参数和返回值 |
| 适用层次 | 控制器层 | 服务层 |
两者通常配合使用:请求级安全控制 URL 访问,方法级安全在服务层实现细粒度控制。
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/**").authenticated()
.anyRequest().permitAll()
);
return http.build();
}
}
@Service
public class DocumentService {
@PreAuthorize("hasAuthority('document:read')")
public Document readDocument(Long id) {
// ...
}
@PreAuthorize("hasAuthority('document:write')")
public Document writeDocument(Document document) {
// ...
}
}
防御性编程
由于未加注解的方法不受方法级安全保护,建议:
- 在请求级配置一个兜底规则
- 对敏感操作始终添加方法级注解
- 使用代码审查确保安全配置完整
// 请求级兜底规则
.authorizeHttpRequests(auth -> auth
.anyRequest().denyAll() // 默认拒绝所有
)
// 或使用 Spring Security 的默认行为
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated() // 默认需要认证
)
自定义权限评估器
实现 PermissionEvaluator
对于复杂的领域对象权限检查,可以实现自定义 PermissionEvaluator:
@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {
private final DocumentRepository documentRepository;
@Override
public boolean hasPermission(Authentication authentication,
Object targetDomainObject,
Object permission) {
if (targetDomainObject instanceof Document document) {
return checkDocumentPermission(authentication, document, permission.toString());
}
return false;
}
@Override
public boolean hasPermission(Authentication authentication,
Serializable targetId,
String targetType,
Object permission) {
if ("document".equals(targetType)) {
Document document = documentRepository.findById((Long) targetId);
return checkDocumentPermission(authentication, document, permission.toString());
}
return false;
}
private boolean checkDocumentPermission(Authentication auth,
Document document,
String permission) {
User user = (User) auth.getPrincipal();
// 所有者拥有所有权限
if (document.getOwner().equals(user)) {
return true;
}
// 检查共享权限
DocumentPermission docPermission = document.getPermissions().get(user);
if (docPermission != null) {
return docPermission.getPermissions().contains(permission);
}
// 管理员拥有所有权限
return auth.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"));
}
}
使用自定义权限评估器
@Service
public class DocumentService {
// 使用 hasPermission 表达式
@PreAuthorize("hasPermission(#id, 'document', 'read')")
public Document readDocument(Long id) {
return documentRepository.findById(id);
}
@PreAuthorize("hasPermission(#document, 'write')")
public void updateDocument(Document document) {
documentRepository.save(document);
}
@PostAuthorize("hasPermission(returnObject, 'delete')")
public Document getDocumentForDelete(Long id) {
return documentRepository.findById(id);
}
}
测试方法安全
测试配置
@SpringBootTest
@Import(MethodSecurityConfig.class)
class DocumentServiceTests {
@Autowired
private DocumentService documentService;
@Test
@WithMockUser(roles = "ADMIN")
void adminCanReadAnyDocument() {
Document doc = documentService.readDocument(1L);
assertThat(doc).isNotNull();
}
@Test
@WithMockUser(roles = "USER")
void userCannotReadAdminDocument() {
assertThatThrownBy(() -> documentService.getAdminDocument(1L))
.isInstanceOf(AccessDeniedException.class);
}
@Test
@WithUserDetails("[email protected]")
void userCanReadOwnDocument() {
Document doc = documentService.getMyDocument(1L);
assertThat(doc).isNotNull();
}
@Test
void anonymousCannotAccessProtectedMethod() {
assertThatThrownBy(() -> documentService.readDocument(1L))
.isInstanceOf(AuthenticationCredentialsNotFoundException.class);
}
}
自定义测试用户
@Test
@WithMockUser(
username = "admin",
password = "password",
roles = {"ADMIN", "USER"},
authorities = {"document:read", "document:write"}
)
void userWithMultipleRolesAndAuthorities() {
// ...
}
// 使用自定义 UserDetailsService 创建用户
@Test
@WithUserDetails(
value = "admin",
userDetailsServiceBeanName = "customUserDetailsService"
)
void withCustomUserDetailsService() {
// ...
}
小结
本章详细介绍了方法级安全的配置和使用:
@EnableMethodSecurity启用方法级安全控制@PreAuthorize和@PostAuthorize是最常用的注解@PreFilter和@PostFilter用于过滤集合数据- SpEL 表达式提供强大的授权规则表达能力
- 自定义注解可以简化重复的授权配置
PermissionEvaluator支持复杂的领域对象权限检查- 方法级安全与请求级安全配合使用,构建完整的安全体系
方法级安全是 Spring Security 的重要特性,特别适合在服务层实现细粒度的权限控制。下一章将学习 CSRF 防护机制。