纲要
- 授权流程核心类与投票机制
PreInvocationAuthorizationAdviceVoter:前置调用授权投票器ExpressionBasedPreInvocationAdvice:基于表达式的通知AffirmativeBased:一票通过制决策管理器
- 授权检查调用链解析
- 安全表达式提取
- 表达式求值与投票循环
- 日志与结果返回
- 用户组(Group)扩展思路
- 角色与用户组的本质差异
- 自定义
groupCheckerBean - 在
@PreAuthorize中组合角色与组校验
- 代码示例:用户组权限控制
MyUserDetails实体扩展GroupChecker组件UserService方法级安全
授权流程源码解析
Spring Security 将方法安全注解(如@PreAuthorize)转化为灵活的授权决策,其核心是一组基于 AOP 的投票器(Voter)与通知(Advice)。理解这一流程有助于排查权限问题并合理扩展。
核心类与调用链
当方法被调用时,PreInvocationAuthorizationAdviceVoter首先被触发。它通过vote()方法获取注解中声明的安全表达式(如hasRole('ADMIN')),随后委托给ExpressionBasedPreInvocationAdvice进行求值。若表达式中包含认证信息或自定义 Bean,则结合当前SecurityContext中的Authentication进行判断。最终结果传递给AffirmativeBased决策管理器,只要有任何一个 Voter 投赞成票,访问即被允许。这一机制保证了权限逻辑的可组合性。
下面是一段精简的决策管理器模拟代码,用于展示投票循环的本质:
// 仅示意核心逻辑,非框架源码publicclassAffirmativeBasedDecisionManager{privateList<AccessDecisionVoter>voters;publicvoiddecide(Authenticationauthentication,Objectobject){for(AccessDecisionVotervoter:voters){intresult=voter.vote(authentication,object);if(result==AccessDecisionVoter.ACCESS_GRANTED){// 一票通过,记录日志并返回logger.info("Voter "+voter+" returned ACCESS_GRANTED");return;}}thrownewAccessDeniedException("拒绝访问");}}用户组扩展思路
在实际项目中,常常需要“部门”、“组织”或“用户组”的概念。角色(Role)是权限的集合,而用户组本质上是用户的集合——将一批用户归入同一组,再给组赋权。Spring Security 虽然提供了基于 Group 的复杂模型,但在多数场景下并不必要。更轻量的做法是直接利用方法安全表达式嵌入自定义检查。
设计方案
- 在
UserDetails实现中增加group字段,存放当前用户所属的组标识(如部门 ID)。 - 编写一个 Spring Bean,注入到 SpEL 上下文中,提供
checkGroup(Authentication, String)方法。 - 在
@PreAuthorize中组合角色判断与组判断,例如:@PreAuthorize("hasRole('ROLE_DEPT_ADMIN') and @groupChecker.checkGroup(authentication, #group)")
这样即可限制用户只能操作自己所在组的资源,而无需引入额外的安全模块。
完整可运行代码示例
以下代码展示如何实现基于用户组的资源访问控制。所有类均包含必要注解,可直接集成到 Spring Boot 项目中。
// MyUserDetails.javapackagecom.example.security;importorg.springframework.security.core.GrantedAuthority;importorg.springframework.security.core.userdetails.UserDetails;importjava.util.Collection;publicclassMyUserDetailsimplementsUserDetails{privateStringusername;privateStringpassword;privateStringgroup;// 用户组标识,如部门IDprivateCollection<?extendsGrantedAuthority>authorities;publicMyUserDetails(Stringusername,Stringpassword,Stringgroup,Collection<?extendsGrantedAuthority>authorities){this.username=username;this.password=password;this.group=group;this.authorities=authorities;}publicStringgetGroup(){returngroup;}@OverridepublicCollection<?extendsGrantedAuthority>getAuthorities(){returnauthorities;}@OverridepublicStringgetPassword(){returnpassword;}@OverridepublicStringgetUsername(){returnusername;}@OverridepublicbooleanisAccountNonExpired(){returntrue;}@OverridepublicbooleanisAccountNonLocked(){returntrue;}@OverridepublicbooleanisCredentialsNonExpired(){returntrue;}@OverridepublicbooleanisEnabled(){returntrue;}}// GroupChecker.javapackagecom.example.security;importorg.springframework.security.core.Authentication;importorg.springframework.stereotype.Component;@Component("groupChecker")publicclassGroupChecker{/** * 检查当前认证用户的 group 是否与目标组一致 */publicbooleancheckGroup(Authenticationauthentication,StringtargetGroup){if(authentication==null||targetGroup==null){returnfalse;}Objectprincipal=authentication.getPrincipal();if(principalinstanceofMyUserDetails){MyUserDetailsuser=(MyUserDetails)principal;returntargetGroup.equals(user.getGroup());}returnfalse;}}// ResourceService.javapackagecom.example.service;importcom.example.domain.Resource;importorg.springframework.security.access.prepost.PreAuthorize;importorg.springframework.stereotype.Service;importjava.util.List;@ServicepublicclassResourceService{/** * 只有管理员角色,并且请求组与用户组一致时才能访问 */@PreAuthorize("hasRole('ROLE_DEPT_ADMIN') and @groupChecker.checkGroup(authentication, #group)")publicList<Resource>getResourcesByGroup(Stringgroup){// 模拟数据库查询returnList.of(newResource(group,"confidential-data"));}}// Resource.java (简单领域对象)packagecom.example.domain;publicclassResource{privateStringgroup;privateStringdata;publicResource(Stringgroup,Stringdata){this.group=group;this.data=data;}// getters & setters ...}项目目录结构如下:
src/main/java/com/example/ ├── security │ ├── MyUserDetails.java │ └── GroupChecker.java ├── service │ └── ResourceService.java └── domain └── Resource.javaAPI 速览
| 组件 | 作用 |
|---|---|
PreInvocationAuthorizationAdviceVoter | 将@PreAuthorize注解转换为投票对象 |
ExpressionBasedPreInvocationAdvice | 对 SpEL 表达式进行求值 |
AffirmativeBased | 一票即过的投票策略 |
@PreAuthorize | 声明方法前置安全表达式 |
@groupChecker(自定义) | 扩展 SpEL 上下文,实现组级权限 |
官方文档
- Spring Security Method Security
- Spring Security Architecture
总结
本文从 Spring Security 授权表决流程的源码切入,剖析了PreInvocationAuthorizationAdviceVoter与AffirmativeBased的协作方式,并在此基础上给出了一种轻量级的用户组权限扩展方案——通过自定义@Bean与@PreAuthorize结合,实现部门级资源隔离。
这一思路在 OAuth2 资源服务器环境中同样适用,因为方法级安全与认证协议无关,只要Authentication中包含足够的信息即可。建议在实际项目中先用最简模型满足需求,再逐步迭代。