跳到主要内容

方法级安全

除了请求级别的授权控制,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) {
// ...
}
}

防御性编程

由于未加注解的方法不受方法级安全保护,建议:

  1. 在请求级配置一个兜底规则
  2. 对敏感操作始终添加方法级注解
  3. 使用代码审查确保安全配置完整
// 请求级兜底规则
.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 防护机制。