如果不能谈情说爱,我们可以自怜自爱。
楔子
上一篇文我们讲过了SpringSecurity
的认证流程,相信大家认真读过了之后一定会对SpringSecurity
的认证流程已经明白个七八分了,本期是我们如约而至的动态鉴权篇,看这篇并不需要一定要弄懂上篇的知识,因为讲述的重点并不相同,你可以将这两篇看成两个独立的章节,从中撷取自己需要的部分。
祝有好收获。
本文代码: 码云地址 GitHub地址
1. 📖SpringSecurity的鉴权原理
上一篇文我们讲认证的时候曾经放了一个图,就是下图:
整个认证的过程其实一直在围绕图中过滤链的绿色部分,而我们今天要说的动态鉴权主要是围绕其橙色部分,也就是图上标的:FilterSecurityInterceptor
。
1. FilterSecurityInterceptor
想知道怎么动态鉴权首先我们要搞明白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) {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
else {
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
在做。
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
方法中参数的有效性。
那既然是一个接口,上文中所调用的肯定是他的实现类了,我们来看看这个接口的结构树: