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

    • 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
    • 最佳实践

PreAuthorize

一句话定义:方法执行前权限校验注解,通过 Spring SpEL 表达式在目标方法调用前评估权限条件,条件不满足时立即抛出 AccessDeniedException,阻止方法执行。


定义与作用

@PreAuthorize 是 Spring Security 方法级别安全中最常用、最灵活的注解。它在方法调用前拦截,使用 Spring Expression Language(SpEL)表达式评估权限,支持引用方法参数、当前认证信息、Spring Bean 方法等。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PreAuthorize {
    String value();  // SpEL 表达式
}

核心表达式变量:

变量类型含义
authenticationAuthentication当前安全上下文
principalObjectauthentication.getPrincipal(),通常是 UserDetails
#paramName方法参数通过参数名引用方法入参
@beanNameSpring Bean调用 Spring 容器中的 Bean 方法
returnObject返回值❌ 在 @PreAuthorize 中不可用(方法未执行)

核心原理

SpEL 表达式评估流程

内置 SpEL 方法

// Spring Security 在 SpEL 中提供的方法(SecurityExpressionRoot)
hasRole(String role)                    // 是否有 ROLE_xxx
hasAnyRole(String... roles)             // 是否有任一角色
hasAuthority(String authority)          // 是否有指定权限
hasAnyAuthority(String... authorities)  // 是否有任一权限
isAuthenticated()                       // 是否已认证
isFullyAuthenticated()                  // 是否完整认证(非 RememberMe)
isAnonymous()                           // 是否匿名
isRememberMe()                          // 是否 RememberMe 登录
permitAll()                             // 总是 true
denyAll()                               // 总是 false
principal                               // 当前主体
authentication                          // 当前认证对象

示例一:基于角色和参数所有权的权限控制

场景说明

用户管理系统,要求:

  • ADMIN 可以修改任何用户
  • 普通用户只能修改自己的信息

操作前配置

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class UserSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
            .withUser("admin").password("{noop}admin").roles("ADMIN")
            .and()
            .withUser("alice").password("{noop}alice").roles("USER")
            .and()
            .withUser("bob").password("{noop}bob").roles("USER");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests(auth -> auth.anyRequest().authenticated())
            .formLogin(withDefaults());
    }
}
@Service
public class UserService {

    // 场景:修改用户信息
    @PreAuthorize("hasRole('ADMIN') or #userDto.username == authentication.name")
    public void updateUser(UserDto userDto) {
        userRepository.save(userDto);
    }
}

public class UserDto {
    private String username;
    private String email;
    // getters/setters
}

操作后配置(更复杂的 SpEL 表达式)

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    // 修改用户:ADMIN 可以修改任何人;用户只能修改自己
    @PreAuthorize("hasRole('ADMIN') or #userDto.username == authentication.name")
    public void updateUser(UserDto userDto) {
        userRepository.save(userDto);
    }

    // 删除用户:只有 ADMIN 可以删除,且不能删除自己
    @PreAuthorize("hasRole('ADMIN') and #username != authentication.name")
    public void deleteUser(String username) {
        userRepository.deleteByUsername(username);
    }

    // 修改密码:必须是自己,且新密码不能和旧密码相同
    @PreAuthorize("#username == authentication.name and #newPassword != #oldPassword")
    public void changePassword(String username, String oldPassword, String newPassword) {
        // 修改密码逻辑
    }
}

结果分析

操作调用用户参数结果表达式评估
updateUser(aliceDto)adminusername="alice"✅hasRole('ADMIN') = true
updateUser(bobDto)aliceusername="bob"❌ 403hasRole('ADMIN')=false, "bob"=="alice"=false
updateUser(aliceDto)aliceusername="alice"✅"alice"=="alice"=true
deleteUser("admin")adminusername="admin"❌ 403hasRole('ADMIN')=true, "admin"!="admin"=false
deleteUser("alice")adminusername="alice"✅hasRole('ADMIN')=true, "alice"!="admin"=true
changePassword("alice", "123", "123")alice新=旧❌ 403"alice"=="alice"=true, "123"!="123"=false
changePassword("alice", "123", "456")alice新≠旧✅全部满足

示例二:引用 Spring Bean 方法进行权限判断

场景说明

权限判断逻辑复杂,需要查询数据库或调用外部服务。将权限判断逻辑封装到 Spring Bean 中,在 @PreAuthorize 中通过 @beanName.method() 调用。

操作前配置

@Component("permissionService")
public class PermissionService {

    @Autowired
    private UserDepartmentRepository deptRepository;

    // 检查用户是否属于指定部门
    public boolean belongsToDepartment(String username, String deptCode) {
        return deptRepository.existsByUsernameAndDeptCode(username, deptCode);
    }

    // 检查用户是否有文档的审批权限
    public boolean canApproveDocument(String username, Long documentId) {
        Document doc = documentRepository.findById(documentId).orElseThrow();
        return doc.getApprover().equals(username) || 
               deptRepository.isManagerOf(username, doc.getDeptCode());
    }
}
@Service
public class DocumentService {

    // 引用 Bean 方法:只有所属部门成员才能创建
    @PreAuthorize("@permissionService.belongsToDepartment(authentication.name, #document.deptCode)")
    public Document createDocument(Document document) {
        return documentRepository.save(document);
    }

    // 引用 Bean 方法:审批权限判断
    @PreAuthorize("@permissionService.canApproveDocument(authentication.name, #documentId)")
    public void approveDocument(Long documentId) {
        Document doc = documentRepository.findById(documentId).orElseThrow();
        doc.setStatus("APPROVED");
        documentRepository.save(doc);
    }

    // 组合表达式:ADMIN 或审批人
    @PreAuthorize("hasRole('ADMIN') or @permissionService.canApproveDocument(authentication.name, #documentId)")
    public void rejectDocument(Long documentId) {
        Document doc = documentRepository.findById(documentId).orElseThrow();
        doc.setStatus("REJECTED");
        documentRepository.save(doc);
    }
}

操作后配置

@Component("permissionService")
public class PermissionService {

    // 缓存权限结果,避免重复查询
    @Cacheable(value = "userDept", key = "#username + '-' + #deptCode")
    public boolean belongsToDepartment(String username, String deptCode) {
        return deptRepository.existsByUsernameAndDeptCode(username, deptCode);
    }
}

结果分析

  • alice(属于 D100 部门)创建 deptCode="D100" 的文档:

    • @permissionService.belongsToDepartment("alice", "D100") → true
    • 方法执行
  • alice(属于 D100)创建 deptCode="D200" 的文档:

    • @permissionService.belongsToDepartment("alice", "D200") → false
    • 抛出 AccessDeniedException
  • admin(无部门归属)调用 rejectDocument:

    • hasRole('ADMIN') → true,短路求值,直接放行
    • 不会执行 canApproveDocument(SpEL or 的短路特性)

易错场景:SpEL 表达式参数名编译后丢失

场景:使用 #paramName 引用参数时,运行时抛出 SpelEvaluationException: Variable paramName is not defined。

原因:Java 编译后默认不保留方法参数名。Spring Security 的 SpEL 解析器无法找到参数名对应的变量。

错误代码:

@Service
public class OrderService {
    @PreAuthorize("#orderId == 1")  // 如果编译未保留参数名,可能报错
    public Order getOrder(Long orderId) { ... }
}

解决方案:

// 方案一:编译时保留参数名(推荐)
// Maven 配置:
// <compilerArgs><arg>-parameters</arg></compilerArgs>
// 或 Gradle:compileJava { options.compilerArgs += ['-parameters'] }

// 方案二:使用 @P 或 @Param 注解显式命名
@Service
public class OrderService {
    @PreAuthorize("#id == 1")
    public Order getOrder(@P("id") Long orderId) { ... }
}

// 方案三:使用 args 数组引用(不需要参数名)
@Service
public class OrderService {
    @PreAuthorize("#args[0] == 1")
    public Order getOrder(Long orderId) { ... }
}

面试考点:

问:@PreAuthorize("#userId == authentication.principal.id") 运行时抛出变量未定义异常,为什么? 答:Java 编译后默认擦除方法参数名(除非使用 -parameters 编译选项),Spring Security 的 SpEL 解析器无法将 #userId 映射到实际参数。解决方案有三种:① 编译时添加 -parameters 选项保留参数名;② 使用 Spring Security 的 @P("userId") 注解或 JSR-330 的 @Param("userId") 显式标注参数名;③ 使用 #args[0] 按索引引用参数。生产环境推荐方案一,通过 Maven/Gradle 配置全局开启参数名保留。


@PreAuthorize 表达式速查表

场景表达式说明
角色检查hasRole('ADMIN')检查 ROLE_ADMIN
多角色任选hasAnyRole('ADMIN', 'MANAGER')任一角色满足
权限检查hasAuthority('SCOPE_write')检查具体权限字符串
参数等于用户名#id == authentication.name校验数据所有权
参数属性判断#userDto.age >= 18参数属性 SpEL 访问
调用 Bean 方法@authService.canAccess(#id)外部权限判断服务
组合条件hasRole('ADMIN') and #amount < 10000角色 + 业务规则
否定条件!hasRole('GUEST')非访客
认证状态isFullyAuthenticated()排除 RememberMe
上一页
方法级别安全
下一页
@PostAuthorize