乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第1章 Spring Security 基础

    • 本章定位
    • Spring Security 是什么
    • DelegatingFilterProxy
    • 安全过滤器链
    • SecurityFilterChain
    • 过滤器执行顺序
  • 第2章 认证

    • 本章定位
    • Authentication
    • 认证流程
    • AuthenticationManager
    • ProviderManager
    • DaoAuthenticationProvider
    • UserDetails
    • UserDetailsService
    • PasswordEncoder
    • BCryptPasswordEncoder
    • DelegatingPasswordEncoder
    • 表单登录
    • SecurityContext
    • SecurityContextHolder
    • UsernamePasswordAuthenticationToken
  • 第3章 授权

    • 本章定位
    • 授权模型
    • GrantedAuthority
    • AccessDecisionManager
    • AccessDecisionVoter
    • URL 级别授权
    • 方法级别安全
    • @PreAuthorize
    • @PostAuthorize
    • @PreFilter
    • @PostFilter
    • @Secured
    • RoleHierarchy
  • 第4章 过滤器链

    • 本章定位
    • FilterChainProxy
    • SecurityContextHolderFilter
    • LogoutFilter
    • BasicAuthenticationFilter
    • CsrfFilter
    • CorsFilter
    • HeaderWriterFilter
    • AnonymousAuthenticationFilter
    • RequestCacheAwareFilter
    • ExceptionTranslationFilter
    • FilterSecurityInterceptor
  • 第5章 会话管理

    • 本章定位
    • 会话管理
    • SessionFixation
    • 会话并发控制
    • SessionCreationPolicy
    • RememberMe
  • 第6章 JWT

    • 本章定位
    • JWT
    • JwtDecoder
    • JWT 认证
    • JwtAuthenticationConverter
  • 第7章 OAuth2

    • 本章定位
    • OAuth2 基础
    • OAuth2 Client
    • OAuth2 Resource Server
    • 第三方登录配置
  • 第8章 攻击防护

    • 本章定位
    • CSRF 跨站请求伪造防护
    • CORS 跨域防护
    • Clickjacking 点击劫持防护
    • 安全响应头
    • Session Fixation 会话固定防护
  • 第9章 测试

    • 本章定位
    • 安全测试
    • @WithMockUser
    • 最佳实践

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 = truejsr250Enabled = 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只有 MANAGERUSER+MANAGERADMIN
getTasksROLE_USER✅❌✅✅(若配层级)
approveTaskROLE_USER + ROLE_MANAGER❌❌✅✅
assignTaskROLE_USER + ROLE_MANAGER❌❌✅✅
deleteTaskROLE_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 逻辑
需要跨框架兼容@RolesAllowedJSR-250 标准注解
数据所有权检查@PreAuthorize@Secured 无法访问方法参数
返回结果过滤@PostFilter@Secured 不处理返回值
上一页
@PostFilter
下一页
RoleHierarchy