SpringSecurity(二十一):权限管理
Spring Security提供的功能主要包含两方面:认证和授权。Spring Security支持多种不同的认证方式,但是无论开发者采用哪种认证方式,都不会影响授权功能的使用,Spring Security很好的实现了认证和授权两大功能的解耦,这也是它受欢迎的原因之一
认证就是确认用户身份,也就是我们常说的登录, 授权则是根据系统提前设置好的规则,给用户分配可以访问某一资源的权限,用户根据自己所具有的权限,去执行相应的操作。
从技术上来说,Spring Security提供的权限管理功能主要有两种类型:
1.基于过滤器的权限管理
2.基于AOP的权限管理
基于过滤器的权限管理主要是用来拦截HTTP请求,拦截下来之后,根据HTTP请求地址进行权限校验。基于AOP的权限管理则主要是用来处理方法级别的权限问题,当调用某一个方法是,通过AOP将操作拦截下来,然后判断用户是否具备相关的权限,如果具备,则允许方法调用,否则禁止方法调用。
核心概念
角色和权限
根据之前的介绍,我们已经知道,在Spring Security用户登录成功后,会将当前用户登录信息保存在Authentication对象中,Authentication对象有一个getAuthorities方法用来返回当前对象具备的权限信息。
getAuthorities的返回值是Collection<? extends GrantedAuthority>即集合中存放的是GrantedAuthority的子类,当需要进行权限判断的时候就会调用该方法获得用户的权限。无论用户是通过何种方式登录的,都是通过这个方法获得权限信息。
那么对于Collection<? extends GrantedAuthority>,我们应该理解为是用户的角色还是用户的权限呢?
从设计层面来讲,角色和权限是两个完全不同的东西,权限是一些具体的操作,比如读权限,写权限;角色是某些权限的集合,比如管理员角色(ROLE_ADMIN)
从代码层面来讲,角色和权限并没有太大的不同,在Spring Security中,角色和权限的处理方式基本是一样的,唯一的区别在于角色在一些地方会自动添加一个ROLE_前缀,而权限不会添加任何前缀
至于getAuthorities的返回值具体是角色还是权限,取决于我们权限系统的设计。
如果系统同时存在角色和权限,我们可以使用GrantedAuthority的实现类SimpleGrantedAuthority来表示一个权限
public class Role implements GrantedAuthority{
private String name;
private List<SimpleGrantedAuthority> allowedOperations=new ArrayList<>();
@Override
public String getAuthority{
return name;
}
省略getter/setter方法
}
角色继承自GrantedAuthority,一个角色对应多个权限,然后在定义用户类的时候,将角色转为权限即可
public class User implements UserDetails{
private List<Role> roles=new ArrayList<>();
@Override
public Collection<? extends GrantedAuthority> getAuthorities(){
List<SimpleGrantedAuthority> authorities=new ArrayList<>();
for(Role role:roles){
authorities.addAll(role.getAllowedOperations());
}
return authorities.stream().distinct().collect(Collectors.toList());
}
省略getter/setter方法
}
角色继承
角色继承就是指角色存在一个上下级关系,例如ROLE_ADMIN继承自ROLE_USER,那么ROLE_ADMIN就自动具备ROLE_USER的所有权限。
Spring Security通过RoleHierarchy类对角色继承提供支持,我们来看下它的源码
public interface RoleHierarchy{
Collection<? extends GrantedAuthority> getReachableGrantedAuthorities(Collection<? extends GrantedAuthority> authorities)
}
只有一个getReachableGrantedAuthorities方法,该方法返回用户真正的权限,假设用户定义了ROLE_ADMIN继承ROLE_USER,ROLE_USER继承ROLE_GUEST,那么ROLE_ADMIN角色实际具有的权限也包括ROLE_USER和ROLE_GUEST的权限。
RoleHierarchy只有一个实现类RoleHierarchyImpl,开发者一般通过RoleHierarchyImpl类定义角色的层级关系。如下面代码表示ROLE_C继承ROLE_D,ROLE_B继承ROLE_C,ROLE_A继承ROLE_B
@Bean
RoleHierarchy roleHierarchy(){
RoleHierarchyImpl hierarchy=new RoleHierarchyImpl();
hierarchy.setHierarchy("ROLE_A>ROLE_B>ROLE_C>ROLE_D");
return hierarchy;
}
前置处理器与后置处理器
无论是基于过滤器的权限管理还是基于方法的权限管理,前置处理器都是重中之重,二者都会用到,而后置处理器只在基于方法的权限管理中会用到
前置处理器
要了解前置处理器,我们需要先了解投票器
投票器
在Spring Security中,投票器是由 AccessDecisionVoter 接口来规范的,我们来看下 AccessDecisionVoter 接口:
public interface AccessDecisionVoter<S> {
int ACCESS_GRANTED = 1;
int ACCESS_ABSTAIN = 0;
int ACCESS_DENIED = -1;
boolean supports(ConfigAttribute attribute);
boolean supports(Class<?> clazz);
int vote(Authentication authentication, S object,
Collection<ConfigAttribute> attributes);
}
1.首先一上来定义了三个常量,从常量名字中就可以看出每个常量的含义,1 表示赞成;0 表示弃权;-1 表示拒绝。
2.两个 supports 方法用来判断投票器是否支持当前请求。
3.vote 则是具体的投票方法。在不同的实现类中实现。三个参数,authentication 表示当前登录主体;object 表示受保护的安全对象,如果受保护的是URL地址,则object就是一个FilterInvocation对象,如果受保护的是一个方法,则object就是一个MethodInvocation;attributes 表示当前所访问的接口所需要的权限集合。vote方法的返回值就是之前定义的三个常量之一
Spring Security为AccessDecisionVoter提供了不同的实现类:
RoleVoter:RoleVoter是根据登陆主体的角色进行投票,即判断当前用户是否具备受保护对象所需要的角色。需要注意的是,默认情况下,角色需要以ROLE_开始,否则supports方法会直接返回false
RoleHierarchyVoter:继承自RoleVoter,投票逻辑和RoleVoter一样,不同的是RoleHierarchyVoter支持角色的继承,它通过RoleHierarchyImpl对象对用户所具有的角色进行解析,获得用户真正可触达的角色,而RoleVoter则直接调用 authentication.getAuthorities()方法获得用户的角色
WebExpressionVoter:基于URL地址进行权限控制时的投票器(支持SpEL)
Jsr250Voter:处理JSR-250权限注解的投票器,如@PermitAll @DenyAll等
AuthenticatedVoter:用来判断当前用户的认证形式
AbstractAclVoter:基于ACL进行权限控制时的投票器,这是一个抽象类,没有绑定到某个具体的ACL系统
AclEntryVoter:继承自AbstractAclVoter,基于Spring Security提供的ACL权限系统的投票器
PreInvocationAuthorizationAdviceVoter:处理@PreFilter和@PreAuthorize注解的投票器
决策器
决策器由AccessDecisionManager负责,AccessDecisionManager会同时管理多个投票器,由AccessDecisionManager调用投票器进行投票,然后根据投票结果做出相应的决策
有一个抽象实现类AbstractAccessDecisionManager,AbstractAccessDecisionManager一共有三个子类:
AffirmativeBased:有一个投票器同意了,就通过。
ConsensusBased:多数投票器同意就通过,平局的话,则看 allowIfEqualGrantedDeniedDecisions 参数的取值。
UnanimousBased 所有投票器都同意,请求才通过。
这是Spring Security提供的三个决策期,如果这三个决策器无法满足需求,开发者也可以自定义类继承自AbstractAccessDecisionManager实现自己的决策器
权限元数据
ConfigAttribute
在介绍投票器的时候,在具体的投票方法vote中,受保护的对象所需要的权限保存在一个Collection
ConfigAttribute用来存储与安全系统相关的配置属性,也就是系统关于权限的配置,我们看下ConfigAttribute接口
public interface ConfigAttribute extends Serializable{
String getAttribute();
}
只有一个getAttribute方法返回具体的权限字符串,而GrantedAuthority则是通过getAuthority方法返回用户所具有的权限,二者的返回值都是字符串是可以比较的。
SecurityMetadataSource
SecurityMetadataSource是用来提供受保护对象所需要的权限。
public interface SecurityMetadataSource extends AopInfrastructureBean {
Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException;
Collection<ConfigAttribute> getAllConfigAttributes();
boolean supports(Class<?> clazz);
}
这里只有三个方法
getAttributes:根据传入的安全对象参数返回所需要的权限,object 表示受保护的安全对象,如果受保护的是URL地址,则object就是一个FilterInvocation对象,如果受保护的是一个方法,则object就是一个MethodInvocation
getAllConfigAttributes:返回所有的角色/权限 以便验证是否支持,不过这个方法不是必须的,可以直接返回null
supports:返回当前的SecurityMetadataSource 是否支持受保护的对象如FilterInvocation或者MethodInvocation
直接继承SecurityMetadataSource的接口有两个:FilterInvocationSecurityMetadataSource和MethodSecurityMetadataSource。
在实际开发中,URL地址以及访问它所需要的权限可能保存在数据库中,此时我们可以自定义类实现FilterInvocationSecurityMetadataSource,然后重写里边的getAttributes方法,在getAttributes方法中根据当前请求的URL地址去数据库中查询其所需要的权限,然后将查询结果封装为ConfigAttibute集合即可
权限表达式
Spring Security 3.0引入了SpEL表达式进行权限配置,我们可以在请求的URL或者访问的方法上,通过SpEL来配置需要的权限
总结
首先有一个过滤器(AbstractSecurityInterceptor),FilterSecurityIntercepto就是它的URL鉴权的子类。过滤器有一个决策器AccessDecisionManager,决策器有三个子类代表不同的决策策略,每个决策器可能有多个投票器AccessDecisionVoter(我们常用的http.authorizeRequests() 就为AccessDecisionManager配置了一个WebExpressionVoter投票器)。
从SecurityMetadataSource(FilterInvocationSecurityMetadataSource是它的一个URL鉴权的实现类)中获得URL的需要权限的集合Collection
Collection
Collection
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话