整个认证的过程其实一直在围绕图中过滤链的绿色部分,而我们今天要说的动态鉴权主要是围绕其橙色部分,也就是图上标的:FilterSecurityInterceptor
1.基本流程
1.1FilterSecurityInterceptor
想知道怎么动态鉴权首先我们要搞明白SpringSecurity的鉴权逻辑,从上图中我们也可以看出:FilterSecurityInterceptor
是这个过滤链的最后一环,而认证之后就是鉴权,所以我们的FilterSecurityInterceptor
主要是负责鉴权这部分。
一个请求完成了认证,且没有抛出异常之后就会到达FilterSecurityInterceptor
所负责的鉴权部分,也就是说鉴权的入口就在FilterSecurityInterceptor
。
我们先来看看FilterSecurityInterceptor
的定义和主要方法:
public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements
Filter {
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
invoke(fi);
}
}
上文代码可以看出FilterSecurityInterceptor
是实现了抽象类AbstractSecurityInterceptor
的一个实现类,这个AbstractSecurityInterceptor
中预先写好了一段很重要的代码(后面会说到)。
FilterSecurityInterceptor
的主要方法是doFilter
方法,过滤器的特性大家应该都知道,请求过来之后会执行这个doFilter
方法,FilterSecurityInterceptor
的doFilter
方法出奇的简单,总共只有两行:
第一行是创建了一个FilterInvocation
对象,这个FilterInvocation
对象你可以当作它封装了request,它的主要工作就是拿请求里面的信息,比如请求的URI。
第二行就调用了自身的invoke
方法,并将FilterInvocation
对象传入。
所以我们主要逻辑肯定是在这个invoke
方法里面了,我们来打开看看:
public void invoke(FilterInvocation fi) throws IOException, ServletException {
if ((fi.getRequest() != null)
&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
&& observeOncePerRequest) {
// filter already applied to this request and user wants us to observe
// once-per-request handling, so don't re-do security checking
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
else {
// first time this request being called, so perform security checking
if (fi.getRequest() != null && observeOncePerRequest) {
fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}
// 进入鉴权
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, null);
}
}
invoke
方法中只有一个if-else,一般都是不满足if中的那三个条件的,然后执行逻辑会来到else。
else的代码也可以概括为两部分:
- 调用了
super.beforeInvocation(fi)
。 - 调用完之后过滤器继续往下走。
第二步可以不看,每个过滤器都有这么一步,所以我们主要看super.beforeInvocation(fi)
,前文我已经说过,FilterSecurityInterceptor
实现了抽象类AbstractSecurityInterceptor
,
所以这个里super其实指的就是AbstractSecurityInterceptor
,
那这段代码其实调用了AbstractSecurityInterceptor.beforeInvocation(fi)
,
前文我说过AbstractSecurityInterceptor
中有一段很重要的代码就是这一段,
那我们继续来看这个beforeInvocation(fi)
方法的源码:
protected InterceptorStatusToken beforeInvocation(Object object) {
Assert.notNull(object, "Object was null");
final boolean debug = logger.isDebugEnabled();
if (!getSecureObjectClass().isAssignableFrom(object.getClass())) {
throw new IllegalArgumentException(
"Security invocation attempted for object "
+ object.getClass().getName()
+ " but AbstractSecurityInterceptor only configured to support secure objects of type: "
+ getSecureObjectClass());
}
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
.getAttributes(object);
Authentication authenticated = authenticateIfRequired();
try {
// 鉴权需要调用的接口
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException accessDeniedException) {
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
accessDeniedException));
throw accessDeniedException;
}
}
源码较长,这里我精简了中间的一部分,这段代码大致可以分为三步:
- 拿到了一个
Collection<ConfigAttribute>
对象,这个对象是一个List,其实里面就是我们在配置文件中配置的过滤规则。 - 拿到了
Authentication
,这里是调用authenticateIfRequired
方法拿到了,其实里面还是通过SecurityContextHolder
拿到的,上一篇文章我讲过如何拿取。 - 调用了
accessDecisionManager.decide(authenticated, object, attributes)
,前两步都是对decide
方法做参数的准备,第三步才是正式去到鉴权的逻辑,既然这里面才是真正鉴权的逻辑,那也就是说鉴权其实是accessDecisionManager
在做。
1.2. AccessDecisionManager
前面通过源码我们看到了鉴权的真正处理者:AccessDecisionManager
,是不是觉得一层接着一层,就像套娃一样,别急,下面还有。先来看看源码接口定义:
public interface AccessDecisionManager {
// 主要鉴权方法
void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) throws AccessDeniedException,
InsufficientAuthenticationException;
boolean supports(ConfigAttribute attribute);
boolean supports(Class<?> clazz);
}
AccessDecisionManager
是一个接口,它声明了三个方法,除了第一个鉴权方法以外,还有两个是辅助性的方法,其作用都是甄别 decide
方法中参数的有效性。
那既然是一个接口,上文中所调用的肯定是他的实现类了,我们来看看这个接口的结构树:
从图中我们可以看到它主要有三个实现类,分别代表了三种不同的鉴权逻辑:
- AffirmativeBased:一票通过,只要有一票通过就算通过,默认是它。
- UnanimousBased:一票反对,只要有一票反对就不能通过。
- ConsensusBased:少数票服从多数票。
这里的表述为什么要用票呢?因为在实现类里面采用了委托的形式,将请求委托给投票器,每个投票器拿着这个请求根据自身的逻辑来计算出能不能通过然后进行投票,所以会有上面的表述。
也就是说这三个实现类,其实还不是真正判断请求能不能通过的类,真正判断请求是否通过的是投票器,然后实现类把投票器的结果综合起来来决定到底能不能通过。
刚刚已经说过,实现类把投票器的结果综合起来进行决定,也就是说投票器可以放入多个,每个实现类里的投票器数量取决于构造的时候放入了多少投票器,我们可以看看默认的AffirmativeBased
的源码。
public class AffirmativeBased extends AbstractAccessDecisionManager {
public AffirmativeBased(List<AccessDecisionVoter<?>> decisionVoters) {
super(decisionVoters);
}
// 拿到所有的投票器,循环遍历进行投票
public void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
int deny = 0;
for (AccessDecisionVoter voter : getDecisionVoters()) {
int result = voter.vote(authentication, object, configAttributes);
if (logger.isDebugEnabled()) {
logger.debug("Voter: " + voter + ", returned: " + result);
}
switch (result) {
case AccessDecisionVoter.ACCESS_GRANTED:
return;
case AccessDecisionVoter.ACCESS_DENIED:
deny++;
break;
default:
break;
}
}
if (deny > 0) {
throw new AccessDeniedException(messages.getMessage(
"AbstractAccessDecisionManager.accessDenied", "Access is denied"));
}
// To get this far, every AccessDecisionVoter abstained
checkAllowIfAllAbstainDecisions();
}
}
AffirmativeBased
的构造是传入投票器List,其主要鉴权逻辑交给投票器去判断,投票器返回不同的数字代表不同的结果,然后AffirmativeBased
根据自身一票通过的策略决定放行还是抛出异常。
AffirmativeBased
默认传入的构造器只有一个->WebExpressionVoter
,这个构造器会根据你在配置文件中的配置进行逻辑处理得出投票结果。
所以SpringSecurity
默认的鉴权逻辑就是根据配置文件中的配置进行鉴权,这是符合我们现有认知的。
1.3. ✍动态鉴权实现
通过上面一步步的讲述,我想你也应该理解了SpringSecurity
到底是什么实现鉴权的,那我们想要做到动态的给予某个角色不同的访问权限应该怎么做呢?
既然是动态鉴权了,那我们的权限URI肯定是放在数据库中了,我们要做的就是实时的在数据库中去读取不同角色对应的权限然后与当前登录的用户做个比较。
1.重写AbstractSecurityInterceptor
1)doFilter:请求过来之后会执行这个doFilter方法,FilterSecurityInterceptor的doFilter方法出奇的简单,总共只有两行:
第一行是创建了一个FilterInvocation对象,这个FilterInvocation对象你可以当作它封装了request,它的主要工作就是拿请求里面的信息,比如请求的URI。
第二行就调用了自身的invoke方法,并将FilterInvocation对象传入
以上是源码说明,自定义的部分主要判断
a)请求的url是否携带token
b)token是否有效
2)invoke()主要逻辑实现,该方法的重点是 InterceptorStatusToken token = super.beforeInvocation(fi),实现鉴权。大致分三步:
1)拿到了一个Collection<ConfigAttribute>对象,这个对象是一个List,其实里面就是我们在配置文件中配置的过滤规则。
2)拿到了Authentication,这里是调用authenticateIfRequired方法拿到了,其实里面还是通过SecurityContextHolder拿到的,上一篇文章我讲过如何拿取。
3)调用了accessDecisionManager.decide(authenticated, object, attributes),前两步都是对decide方法做参数的准备,第三步才是正式去到鉴权的逻辑,既然这里面才是真正鉴权的逻辑,那也就是说鉴权其实是accessDecisionManager在做。
2.重写FilterInvocationSecurityMetadataSource
从数据库中获取权限
3.重写AccessDecisionManager
权限对比
总结:
- 通过 obtainSecurityMetadataSource().getAttributes() 获取 当前访问地址所需权限信息
- 通过 authenticateIfRequired() 获取当前访问用户的权限信息
- 通过 accessDecisionManager.decide() 使用 投票机制判权,判权失败直接抛出 AccessDeniedException 异常