1.简介
授权是更具系统提前设置好的规则,给用户分配可以访问某一资源的权限,用户根据自己所具有的权限,去执行相应的操作,spring security提供的权限管理功能主要有两种:
- 基于过滤器的权限管理功能(FilterSecurityInterceptor)
- 基于AOP的权限管理功能(MethodSecurityInterceptor)
2. 核心概念
2.1 角色与权限
用户信息都保存在Authentication信息中,Authentication对象中有一个Collection<? extends GrantedAuthority> getAuthorities() 当需要进行鉴权时,就会调用该方法获取用户权限,进而做出判断。无论用户采取何种认证方式,都不影响授权。
从设计层面来讲,角色和权限是两个完全不同的东西,权限是一些具体操作,例如read、write,角色则是某些权限的集合,例如管理员、普通用户。
从代码层面讲,角色和权限并没有太大的不同。
至于Collection<? extends GrantedAuthority> getAuthorities() 的返回值,需要分情况对待:
(1) 如果权限系统设计的比较简单,用户<==>权限<==>资源三者之间的关系,那么getAuthorities就是返回用户的权限
(2) 如果权限系统设计的比较复杂,如用户<==>角色<==>权限<==>资源,此时将getAuthorities的返回值当作权限来理解。由于Spring Security并未提供相关的角色类,因此此时需要我们自定义角色类。
1 2 3 4 5 6 7 8 9 10 |
|
角色继承自GrantedAuthority,一个角色对应多个权限,然后在定义用户类的时候,将角色转为权限即可.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
2.2 角色继承
指角色存在一个上下级关系,例如ADMIN继承自USER,那么ADMIN就自动具备USER的所有权限。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
2.3 FilterSecurityInterceptor处理器
基于过滤器的权限过滤器(FilterSecurityInterceptor)有一个前置过滤器和后置过滤器,首先由前置处理器判断发起当前请求的用户是否具备相应的权限,如果具备则请求继续向下走,到达目标方法并执行完毕。在相应时,会经过FilterSecurityInterceptor,次吃由后置处理器再去完成收尾工作(后置过滤器一般不工作),如图2-3所示。
图2-3
2.4 前置过滤器
前置过滤器主要有两个核心组件,分别是投票器和决策器。
2.4.1 投票器
当投票器在投票时,需要两方面的权限:其一是当前用户具备哪些权限;其二是当前访问的URL需要哪些权限才能访问,投票器就是对这两种权限进行比较。其接口如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
AccessDecisionVoter常用的实现类如下图2-4-1所示,本项目中主要用到RoleVoter和RoleHierarchyVoter。
RoleVoter:RoleVoter是根据登陆主体的角色进行投票,即判断当前用户是否具备受保护对象所需要的角色。需要注意的是,默认情况下,角色需要以ROLE_开始,否则supports方法会直接返回false。
RoleHierarchyVoter:继承自RoleVoter,投票逻辑和RoleVoter一样,不同的是RoleHierarchyVoter支持角色的继承,它通过RoleHierarchyImpl对象对用户所具有的角色进行解析,获得用户真正可触达的角色,而RoleVoter则直接调用 authentication.getAuthorities()方法获得用户的角色。
图2-4-1
2.4.2 决策器
决策器由AccessDecisionManager负责,AccessDecisionManager会同时管理多个投票器,由AccessDecisionManager调用投票器进行投票,然后根据投票结果做出相应的决策。
1 2 3 4 5 6 7 8 9 |
|
decide方法:是核心的决策方法,在这个方法中判断是否允许当前URL调用, 如果拒绝访问的话会抛出AccessDeniedException异常。
supports(ConfigAttribute) 方法:用来判断是否支持ConfigAttribute对象。
supports(Class)方法:用来判断是否支持当前安全对象。
和决策器相关的类关系,如图2-4-2所示,AccessDecisionManager会同时管理多个投票器,由AccessDecisionManager调用投票器进行投票,然后根据投票结果做出相应的决策,有一个抽象实现类AbstractAccessDecisionManager,AbstractAccessDecisionManager一共有三个子类:
AffirmativeBased:一票通过机制(默认即此)
ConsensusBased:少数服从多数机制,平局的话,则看 allowIfEqualGrantedDeniedDecisions 参数的取值
UnanimousBased:一票否决制
图2-4-2
2.5 权限元数据
2.5.1 ConfigAttribute
在介绍投票器的时候,在具体的投票方法vote中,受保护的对象所需要的权限保存在一个Collection集合中,集合中的对象是ConfigAttribute而不是GrantedAuthority,ConfigAttribute用来存储与安全系统相关的配置属性,也就是系统关于权限的配置,接口如下:
1 2 3 |
|
只有一个getAttribute方法返回具体的权限字符串,而GrantedAuthority则是通过getAuthority方法返回用户所具有的权限,随着两个类不同,但是二者的返回值都是字符串是可以比较的。其部分实现类如图2-5-1所示。
图2-5-1
2.5.2 SecurityMetadataSource
提供受保护的对象所需的权限,例如,用户访问了一个URL地址需要哪些权限才能访问?这个就由SecurityMetadataSource提供,接口源码如下:
public interface SecurityMetadataSource extends AopInfrastructureBean {
//根据传入的安全对象参数返回其所需要的权限,如果受保护的对象是一个URL地址,那么传入的参数object就是一个FilterInvocation对象
Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException;
Collection<ConfigAttribute> getAllConfigAttributes();
boolean supports(Class<?> clazz);
}
继承SecurityMetadataSource的接口主要有两个,其依赖关系如图2-5-2所示,FilterInvocationSecurityMetadataSource和MethodSecurityMetadataSource,FilterInvocationSecurityMetadataSource是一个空接口,更像一个标记,如果被保护的对象是一个URL地址,将有该接口的实现类提供访问URL地址所需的权限。
图2-5-2
在实际开发中,URL地址以及访问他所需的权限可能保存在数据库中,此时我们可以自定义类实现FilterInvocationSecurityMetadataSource接口,然后重写里面的getAttributes方法,将查询结果封装为相应的ConfigAttribute集合返回即可。
3. 基于URL地址的权限管理
基于URL地址的权限管理主要是通过过滤器FilterSecurityInterceptor来实现的。如果开发者配置了基于URL地址的权限管理,那么FilterSecurityInterceptor就会被自动添加到spring security过滤器链中,在过滤器链中拦截下请求,然后分析当前用户是否具备请求所需要的权限,如果不具备,则抛出异常。
FilterSecurityInterceptor将请求拦截下来之后,会交给AccessDecisionManager进行处理,AccessDecisionManager则会调用投票器进行投票,然后对投票结果进行决策,最终决定请求是否通过。
3.1 原理剖析
3.1.1 AbstractSecurityInterceptor
该类统筹着关于权限处理的一切。方法很多,不过只需要关注其中的三个方法:beforeInvocation、afterInvocation以及finallyInvocation。
在这三个方法中,beforeInvocation中会调用前置处理器完成权限校验,afterInvocation中调用后置处理器完成权限校验,finallyInvocation则主要做一些校验后的清理工作。
先来看下beforeInvocation:
//为了提升可读性,代码有删减
protected InterceptorStatusToken beforeInvocation(Object object) {
// 首先调用obtainSecurityMetadataSource方法获取SecurityMetadataSource对象,然后调用其getAttributes方法获取,受保护对象所需要的权限
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
// 如果获取到的权限值为空
if (CollectionUtils.isEmpty(attributes)) {
//实际代码中会检查rejectPublicInvocations,默认false,如果为true表示拒绝公开调用,并会抛出异常
return null;
}
// 检查当前用户是否已经登录
Authentication authenticated = authenticateIfRequired();
// 尝试授权
attemptAuthorization(object, attributes, authenticated);
// 如果runAs为空,则直接创建一个InterceptorStatusToken对象返回即可
return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object);
}
private void attemptAuthorization(Object object, Collection<ConfigAttribute> attributes,
Authentication authenticated) {
try {
// 核心功能:进行决策,该方法中会调用投票器进行投票,如果该方法执行抛出异常,则说明权限不足
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException ex) {
...
}
}
finallyInvocation(FilterSecurityInterceptor很少用到):
/**
* 如果临时替换了用户身份,那么最终要将用户身份恢复,finallyInvocation方法所做的事情就是恢复用户身份。这里的参数token就是
* beforeInvocation方法的返回值,用户原始的身份信息都保存在token中,从token中取出用户身份信息,并设置到SecurityContextHolder
* 中去即可。
*/
protected void finallyInvocation(InterceptorStatusToken token) {
if (token != null && token.isContextHolderRefreshRequired()) {
SecurityContextHolder.setContext(token.getSecurityContext());
}
}
afterInvocation在FilterSecurityInterceptor中也很少用到,此处省略。
3.1.2
FilterSecurityInterceptor
基于URL地址的权限管理,此时最终使用的是AbstractSecurityInterceptor的子类FilterSecurityInterceptor,这是一个过滤器。当在configure(HttpSecurity)方法中调用http.authorizeRequests()开启URL路径拦截规则配置时,就会通过AbstractInterceptUrlConfigurer#configure方法将FilterSecurityInterceptor添加到spring security过滤器链中,对过滤器而言,最重要的就是doFilter方法:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 构建受保护对象FilterInvocation,然后调用invoke方法
invoke(new FilterInvocation(request, response, chain));
}
public void invoke(FilterInvocation filterInvocation) throws IOException, ServletException {
// 判断当前过滤器是否已经执行过,如果是,则继续执行剩下的过滤器
if (isApplied(filterInvocation) && this.observeOncePerRequest) {
// filter already applied to this request and user wants us to observe
// once-per-request handling, so don't re-do security checking
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
return;
}
// first time this request being called, so perform security checking
if (filterInvocation.getRequest() != null && this.observeOncePerRequest) {
filterInvocation.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}
// 调用父类的beforeInvocation方法进行权限校验
InterceptorStatusToken token = super.beforeInvocation(filterInvocation);
try {
// 校验通过后,继续执行剩余的过滤器
filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());
}
finally {
// 调用父类的finallyInvocation方法
super.finallyInvocation(token);
}
// 最后调用父类的afterInvocation方法,可以看到,前置处理器和后置处理器都是在invoke方法中触发的
super.afterInvocation(token, null);
}
3.1.3 AbstractInterceptUrlConfigurer
该类主要负责创建FilterSecurityInterceptor对象,AbstractInterceptUrlConfigurer有两个不同的子类,两个子类创建出来的FilterSecurityInterceptor对象略有差异:
- ExpressionUrlAuthorizationConfigurer
- UrlAuthorizationConfigurer
通过ExpressionUrlAuthorizationConfigurer构建出来的FilterSecurityInterceptor,使用的投票器是WebExpressionVoter,使用的权限元数据对象是ExpressionBasedFilterInvocationSecurityMetadataSource,所以它支持权限表达式。
通过UrlAuthorizationConfigurer构建出来的FilterSecurityInterceptor,使用的投票器是RoleVoter和AuthenticatedVoter,使用的权限元数据对象是DefaultFilterInvocationSecurityMetadataSource,所以它不支持权限表达式。实际开发中我们常常是基于数据库动态加载权限而不是将代码通过权限表达式写在代码里面,所以这里更多的是关注UrlAuthorizationConfigurer。
3.2 动态管理权限规则
SecurityMetadataSource接口负责提供受保护对象所需要的权限,实现动态管理权限的关键就是通过自定义类继承自FilterInvocationSecurityMetadataSource,并重写getAttributes方法,从数据库中来提供受保护对象所需要的权限。
@Component
public class CustomSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
AntPathMatcher antPathMatcher = new AntPathMatcher();
/**
* 在基于URL地址的权限控制中,受保护对象就是FilterInvocation。
* @param object 受保护对象
* @return 受保护对象所需要的权限
*/
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
//从数据库中查询受保护URL所需的权限
}
/**
* 方便在项目启动阶段做校验,如果不需要校验,则直接返回null即可。
* @return 所有的权限属性
*/
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
/**
* 表示当前对象支持处理的受保护对象是FilterInvocation。
*/
@Override
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
之后我们将自定义的CustomSecurityMetadataSource放入配置类中
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
CustomSecurityMetadataSource customSecurityMetadataSource;
@Override
protected void configure(HttpSecurity http) throws Exception {
ApplicationContext applicationContext = http.getSharedObject(ApplicationContext.class);
http.apply(new UrlAuthorizationConfigurer<>(applicationContext))
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
// 使用配置好的CustomSecurityMetadataSource来代替默认的SecurityMetadataSource对象
object.setSecurityMetadataSource(customSecurityMetadataSource);
return object;
}
});
http.formLogin()
.and()
.csrf().disable();
}
}