Secured
一句话定义:Spring Security 内置的基于角色的方法安全注解,通过声明式角色字符串限制方法访问,是
prePostEnabled复杂 SpEL 表达式之外的轻量级替代方案。
定义与作用
@Secured 是 Spring Security 早期提供的方法级别安全注解,语法简单直接,只支持角色字符串数组,不支持 SpEL 表达式。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Secured {
String[] value(); // 角色数组,必须全部满足(逻辑与)
}
与 JSR-250 @RolesAllowed 的区别:
| 特性 | @Secured | @RolesAllowed |
|---|---|---|
| 来源 | Spring Security 自有 | JSR-250 标准(Java 规范) |
| 启用方式 | securedEnabled = true | jsr250Enabled = true |
| 角色前缀 | 可选 ROLE_(AccessDecisionManager 处理) | 必须显式写 ROLE_ |
| 表达式支持 | ❌ 不支持 | ❌ 不支持 |
| 跨框架兼容 | 仅 Spring Security | 任何支持 JSR-250 的框架 |
| 多角色逻辑 | 必须同时满足(AND) | 必须同时满足(AND) |
本教程选择讲解
@Secured(@RolesAllowed机制类似,仅需切换注解和启用开关)。
核心原理
拦截机制
@Secured 注解中的角色字符串,会被 MethodSecurityMetadataSource 解析为 ConfigAttribute 集合,然后传递给 AccessDecisionManager 进行投票。RoleVoter 检查 authentication.authorities 是否包含指定角色。
角色前缀处理
@Secured 的角色字符串不自动添加 ROLE_ 前缀,这与 hasRole() 不同。如果使用 RoleVoter(默认),需要显式写 ROLE_ 前缀;如果配置了自定义 Voter,可以不带前缀。
// RoleVoter 默认检查 ROLE_ 前缀
@Secured("ROLE_ADMIN") // ✅ 正确:RoleVoter 能识别
@Secured("ADMIN") // ❌ 错误:RoleVoter 不识别,除非自定义 Voter
示例一:@Secured 与 @PreAuthorize 的对比使用
场景说明
同一套权限需求,分别用 @Secured 和 @PreAuthorize 实现,展示两者差异。
操作前配置(启用 @Secured)
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true) // 只启用 @Secured
public class SecuredConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin").password("{noop}admin").roles("ADMIN", "MANAGER")
.and()
.withUser("manager").password("{noop}manager").roles("MANAGER")
.and()
.withUser("user").password("{noop}user").roles("USER");
}
}
@Service
public class ReportService {
@Secured("ROLE_ADMIN") // 只有 ADMIN 能执行
public void generateSystemReport() {
// 生成系统级报表
}
@Secured({"ROLE_ADMIN", "ROLE_MANAGER"}) // 必须同时拥有 ADMIN 和 MANAGER
public void generateFinanceReport() {
// 生成财务报表(需要双重角色)
}
@Secured("ROLE_USER")
public void generatePersonalReport() {
// 生成个人报表
}
}
操作后配置(切换到 @PreAuthorize 的等价写法)
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 启用 @PreAuthorize
public class PrePostConfig extends WebSecurityConfigurerAdapter {
// 用户配置相同...
}
@Service
public class ReportService {
@PreAuthorize("hasRole('ADMIN')") // 等价于 @Secured("ROLE_ADMIN")
public void generateSystemReport() { ... }
// @Secured({"ROLE_ADMIN", "ROLE_MANAGER"}) 是 AND 逻辑
// @PreAuthorize 中 hasRole('ADMIN') and hasRole('MANAGER')
@PreAuthorize("hasRole('ADMIN') and hasRole('MANAGER')")
public void generateFinanceReport() { ... }
@PreAuthorize("hasRole('USER')")
public void generatePersonalReport() { ... }
// @PreAuthorize 额外能力:SpEL 表达式
@PreAuthorize("hasRole('ADMIN') or #department == 'IT'")
public void generateDepartmentReport(String department) { ... }
}
结果分析
| 方法 | 注解 | admin 用户 | manager 用户 | user 用户 |
|---|---|---|---|---|
generateSystemReport | @Secured("ROLE_ADMIN") | ✅ | ❌ | ❌ |
generateFinanceReport | @Secured({"ROLE_ADMIN","ROLE_MANAGER"}) | ✅ | ❌ | ❌ |
generatePersonalReport | @Secured("ROLE_USER") | ❌ | ❌ | ✅ |
注意:
admin用户有ROLE_ADMIN和ROLE_MANAG ER,但@Secured的数组表示必须同时满足所有角色。generateFinanceReport要求同时有ADMIN和MANAGER,admin 满足,manager 只有MANAGER不满足。
示例二:类级别 @Secured 与方法级别组合
场景说明
在类上添加 @Secured 作为默认角色要求,在个别方法上覆盖或补充。
操作前配置
@Service
@Secured("ROLE_USER") // 类级别:所有方法默认需要 USER 角色
public class TaskService {
// 继承类级别:需要 ROLE_USER
public List<Task> getTasks() { ... }
// 方法级别覆盖:额外要求 MANAGER
@Secured("ROLE_MANAGER") // ❌ 注意:这会覆盖类级别,不是叠加!
public void approveTask(Long taskId) { ... }
// 显式叠加:需要同时满足 USER 和 MANAGER
@Secured({"ROLE_USER", "ROLE_MANAGER"})
public void assignTask(Long taskId, String user) { ... }
}
问题分析:Spring Security 的方法级别注解会覆盖类级别注解,而不是叠加。approveTask 上的 @Secured("ROLE_MANAGER") 会完全替代类上的 @Secured("ROLE_USER"),导致一个只拥有 MANAGER 角色的用户(没有 USER)也能调用 approveTask,这可能不符合预期。
操作后配置(显式继承类级别角色)
@Service
@Secured("ROLE_USER") // 基线角色
public class SafeTaskService {
// 默认继承 ROLE_USER
public List<Task> getTasks() { ... }
// 显式写出所有需要的角色(包含基线 + 额外)
@Secured({"ROLE_USER", "ROLE_MANAGER"})
public void approveTask(Long taskId) { ... }
// 使用 @PreAuthorize 实现更清晰的叠加逻辑
@PreAuthorize("hasRole('USER') and hasRole('MANAGER')")
public void assignTask(Long taskId, String user) { ... }
// 类级别 + 方法级别:管理员拥有所有权限(通过 RoleHierarchy)
@Secured("ROLE_ADMIN")
public void deleteTask(Long taskId) { ... }
}
结果分析
| 方法 | 所需角色 | 只有 USER | 只有 MANAGER | USER+MANAGER | ADMIN |
|---|---|---|---|---|---|
getTasks | ROLE_USER | ✅ | ❌ | ✅ | ✅(若配层级) |
approveTask | ROLE_USER + ROLE_MANAGER | ❌ | ❌ | ✅ | ✅ |
assignTask | ROLE_USER + ROLE_MANAGER | ❌ | ❌ | ✅ | ✅ |
deleteTask | ROLE_ADMIN | ❌ | ❌ | ❌ | ✅ |
易错场景:@Secured 角色前缀遗漏
场景:开发者从 @PreAuthorize 切换到 @Secured,忘记 @Secured 不会自动添加 ROLE_ 前缀。
@Service
public class ConfusedService {
@PreAuthorize("hasRole('ADMIN')") // 内部转换为 ROLE_ADMIN
public void method1() { ... }
@Secured("ADMIN") // ❌ 错误:RoleVoter 检查 ROLE_ADMIN,不会匹配 ADMIN
public void method2() { ... }
@Secured("ROLE_ADMIN") // ✅ 正确:必须显式写 ROLE_ 前缀
public void method3() { ... }
}
问题分析:
hasRole('ADMIN')在 Spring Security 内部会自动添加ROLE_前缀,生成ROLE_ADMIN的ConfigAttribute@Secured("ADMIN")直接生成ADMIN的ConfigAttribute,RoleVoter的supports()检查ROLE_前缀时,"ADMIN"不匹配,导致ACCESS_ABSTAIN- 如果使用
AffirmativeBased,且没有别的 Voter 投GRANTED,可能意外放行
正确做法:
// @Secured 必须显式使用 ROLE_ 前缀
@Secured("ROLE_ADMIN")
@Secured({"ROLE_USER", "ROLE_MANAGER"})
// 如果确实不想用 ROLE_ 前缀,需要自定义 Voter
public class NoPrefixRoleVoter implements AccessDecisionVoter<Object> {
@Override
public boolean supports(ConfigAttribute attribute) {
return attribute.getAttribute() != null; // 接受任何非空属性
}
// ... 投票逻辑中不检查 ROLE_ 前缀
}
面试考点:
问:
@Secured("ADMIN")和@PreAuthorize("hasRole('ADMIN')")有什么区别?为什么前者可能失效? 答:@PreAuthorize("hasRole('ADMIN')")在 Spring Security 内部会自动将ADMIN转换为ROLE_ADMIN,生成ROLE_ADMIN的ConfigAttribute,由RoleVoter检查。@Secured("ADMIN")直接生成ADMIN的ConfigAttribute,RoleVoter的supports()方法只处理ROLE_前缀的属性,因此"ADMIN"不匹配,Voter 投ACCESS_ABSTAIN。如果AccessDecisionManager是AffirmativeBased且没有别的 Voter 处理"ADMIN",该注解可能完全不起作用。@Secured必须显式写ROLE_ADMIN才能被RoleVoter识别。
@Secured 适用场景速查
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 简单角色检查 | @Secured | 代码简洁,无 SpEL 学习成本 |
| 复杂权限表达式 | @PreAuthorize | 支持 SpEL、参数、Bean 引用 |
| 多角色"或"关系 | @PreAuthorize | @Secured 是 AND 逻辑 |
| 需要跨框架兼容 | @RolesAllowed | JSR-250 标准注解 |
| 数据所有权检查 | @PreAuthorize | @Secured 无法访问方法参数 |
| 返回结果过滤 | @PostFilter | @Secured 不处理返回值 |