FilterSecurityInterceptor
简介
Spring Security对于权限的控制有2种方式,1.通过ExpressionInterceptUrlRegistry进行配置,2.通过注解和切面的方式。FilterSecurityInterceptor是针对于第一种方式权限配置的控制机制,在项目或服务启动时,Spring Security会把ExpressionInterceptUrlRegistry中配置的权限控制规则转换成SecurityMetadataSource,客户端或者浏览器发起请求时,会经过FilterSecurityInterceptor过滤器,过滤器根据SecurityMetadataSource进行权限校验。第二种方式是通过且AOP切面实现的,与FilterSecurityInterceptor无关,因为同为权限控制,所以这里附带分析一下。
代码分析
配置鉴权
步骤1:配置鉴权
笔者项目主要用与配置swagger以及feign api白名单,考虑到白名单变更的频率也比较高,进行硬编码也不太合适。于是写了个SpringBootAutoConfiguration,将白名单配置到配置文件中修改起来也比较方便,笔者这里只是配置白名单URL进行放行,用了permitAll(),还有其他权限校验方法,比如hasRole(),hasAnyRole()等等,可以根据自己项目情况进行硬编码设置,配置如下:
carp:
security:
oauth2:
resource:
enable: true
whitelabel-clients: carp
whitelabel-urls:
#内部接口加密但不拦截
- /feign/**
#swagger接口
- /doc/api/**
- /swagger-ui.html
- /swagger-ui.html**
- /webjars/**
- /swagger-resources/**
- /v2/api-docs/**
public abstract class AbstractResourceServerConfig extends ResourceServerConfigurerAdapter {
//注入配置文件中配置的属性
@Resource
protected CarpAuthResourceProperties carpAuthResourceProperties;
@Override
public void configure(HttpSecurity http) throws Exception {
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http
.authorizeRequests();
//是否开启资源权限校验
if (!carpAuthResourceProperties.getEnable()) {
registry.anyRequest().permitAll();
return;
}
if (carpAuthResourceProperties != null && carpAuthResourceProperties.getWhitelabelUrls().size() > 0) {
//白名单URL放行
carpAuthResourceProperties.getWhitelabelUrls().forEach(s -> registry.antMatchers(s).permitAll());
}
registry.anyRequest().authenticated();
}
}
步骤2:鉴权拦截处理
FilterSecurityInterceptor的鉴权逻辑集中在父类AbstractSecurityInterceptor#beforeInvocation()方法中,对于FilterSecurityInterceptor而言,是针对请求url的权限拦截和鉴权,所以Collection attributes取到也是针对请求路径的EL表达式鉴权配置,因此accessDecisionManager.decide()这里进行的投票也是基于请求路径进行规则匹配来进行投票,beforeInvocation()的大致逻辑,代码如下:
//权限规则全局变量
private FilterInvocationSecurityMetadataSource securityMetadataSource;
//通过Set注入配置的权限过滤规则
public void setSecurityMetadataSource(FilterInvocationSecurityMetadataSource newSource) {
this.securityMetadataSource = newSource;
}
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
//chain是Servlet原生容器的过滤链,并非Spring Security中的过滤链
FilterInvocation fi = new FilterInvocation(request, response, chain);
//执行过滤的核心规则
invoke(fi);
}
public void invoke(FilterInvocation fi) throws IOException, ServletException {
if ((fi.getRequest() != null)
&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
&& observeOncePerRequest) {
//同一次请求中,过滤器已经执行过,直接进入原生过滤链
//FilterSecurityInterceptor是Spring Security过滤链的最后一个,所以要进入Servlet原生的过滤前
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
else {
if (fi.getRequest() != null && observeOncePerRequest) {
//打一个过滤器已执行标识
fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}
//原生servlet过滤链执行后进行权限规则校验
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
finally {
//还原SpringSecurity上下文
super.finallyInvocation(token);
}
//后置处理,不做深入研究
super.afterInvocation(token, null);
}
}
protected InterceptorStatusToken beforeInvocation(Object object) {
Assert.notNull(object, "Object was null");
final boolean debug = logger.isDebugEnabled();
//对于FilterSecurityInterceptor SecureObjectClass 是FilterInvocation.class,判断是否是合法的Invocation
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);
//没有配置任何权限过滤规则时,是否允许访问接口
if (attributes == null || attributes.isEmpty()) {
if (rejectPublicInvocations) {
throw new IllegalArgumentException(
"Secure object invocation "
+ object
+ " was denied as public invocations are not allowed via this interceptor. "
+ "This indicates a configuration error because the "
+ "rejectPublicInvocations property is set to 'true'");
}
publishEvent(new PublicInvocationEvent(object));
return null; // no further work post-invocation
}
if (SecurityContextHolder.getContext().getAuthentication() == null) {
//身份认证信息为空,发送对应事件,并抛出AuthenticationCredentialsNotFoundException异常
credentialsNotFound(messages.getMessage(
"AbstractSecurityInterceptor.authenticationNotFound",
"An Authentication object was not found in the SecurityContext"),
object, attributes);
}
//如果必要,则进行身份认证
Authentication authenticated = authenticateIfRequired();
// Attempt authorization
try {
//访问投票决定是否可以访问
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException accessDeniedException) {
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
accessDeniedException));
throw accessDeniedException;
}
//发送授权事件
if (publishAuthorizationSuccess) {
publishEvent(new AuthorizedEvent(object, attributes, authenticated));
}
// Attempt to run as a different user
// 当attributes有属性值以RUN_AS_XXX开头时,buildRunAs会解析为ROLE_XXX,重新根据权限生成新的身份认证信息
Authentication runAs = this.runAsManager.buildRunAs(authenticated, object,
attributes);
if (runAs == null) {
//包装成一个复合对象
return new InterceptorStatusToken(SecurityContextHolder.getContext(), false,
attributes, object);
}
else {
//一下4行代码,1:取出旧的上下文 2:创建一个空的上下文 3:将新生成的RunAs身份认证信息放到信息的上下文中 4:将旧的上下文放到复合对象
SecurityContext origCtx = SecurityContextHolder.getContext();
SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
SecurityContextHolder.getContext().setAuthentication(runAs);
//包装成一个复合对象,由于上下被替换,当前放入的是旧上下文,故contextHolderRefreshRequired这里为true
return new InterceptorStatusToken(origCtx, true, attributes, object);
}
}
this.obtainSecurityMetadataSource().getAttributes(object)这行代码直观可能会疑惑,这里就是把Request请求的取出,找出与请求路径能够匹配的ConfigAttribute,通过Debug,观察变量值可能更直观,代码和截图如下:
public Collection<ConfigAttribute> getAttributes(Object object) {
final HttpServletRequest request = ((FilterInvocation) object).getRequest();
for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap
.entrySet()) {
if (entry.getKey().matches(request)) {
return entry.getValue();
}
}
return null;
}
Authentication authenticated = authenticateIfRequired()是取出上下文中的身份认证信息,判断是否进行必要验证,代码解析注释到代码当中了,如下:
private Authentication authenticateIfRequired() {
Authentication authentication = SecurityContextHolder.getContext()
.getAuthentication();
//一般 1:未登录的用户,匿名过滤器会生成一个匿名Token 2:登录过的用户会有一个Bearer Token
//在笔者的配置中,尚未执行到authenticationManager.authenticate(authentication);就return了
if (authentication.isAuthenticated() && !alwaysReauthenticate) {
return authentication;
}
authentication = authenticationManager.authenticate(authentication);
SecurityContextHolder.getContext().setAuthentication(authentication);
return authentication;
}
紧接着就是非常重要的accessDecisionManager.decide()方法,这里的accessDecisionManager是一个AffirmativeBased实例,投票有3种结果ACCESS_GRANTED(授权)、ACCESS_ABSTAIN(弃权)、ACCESS_DENIED(拒绝),投票规则为:所有的投票器进行一轮投票,一旦有一票通过则授权通过,弃权票作废,全部拒绝则授权失败。对于FilterSecurityInterceptor,AffirmativeBased持有的是一个WebExpressionVoter关键代码如下:
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);
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
//deny=0情况可能有:1. 没有投票器 2. 所有投票器弃权
//这里就是判断是否允许全部弃权的
checkAllowIfAllAbstainDecisions();
}
那么decisionVoters是什么时候注入的?我们在AbstractAccessDecisionManager构造方法方法上打个短断点,执行堆栈和创建decisionManager的代码如下:
@Override
@SuppressWarnings("rawtypes")
List<AccessDecisionVoter<?>> getDecisionVoters(H http) {
List<AccessDecisionVoter<?>> decisionVoters = new ArrayList<>();
WebExpressionVoter expressionVoter = new WebExpressionVoter();
expressionVoter.setExpressionHandler(getExpressionHandler(http));
decisionVoters.add(expressionVoter);
return decisionVoters;
}
至于RunAsManager,代码中也做简要的注释,笔者对这部分代码还没有较深入的了解和实践,不作过多说明。
FilterSecurityInterceptor是Spring Security过滤器中最后一个,执行beforeInvocation()后就要执行servlet中的过滤器了,截图比较直观,如下:
finallyInvocation()代码比较简单,当在beforeInvocation()中RunAsManager试图模拟另外一个用户身份创建新的身份认证信息成功时,contextHolderRefreshRequired=true,在执行finallyInvocation()就能进入if分支,将原来的认证信息给还原,代码如下:
protected void finallyInvocation(InterceptorStatusToken token) {
//上下文被RunAsManager替换过,则进行恢复
if (token != null && token.isContextHolderRefreshRequired()) {
if (logger.isDebugEnabled()) {
logger.debug("Reverting to original Authentication: "
+ token.getSecurityContext().getAuthentication());
}
SecurityContextHolder.setContext(token.getSecurityContext());
}
}
afterInvocation()查了一些资料,是通过afterInvocationManager改变返回值的,但是有一点疑问,super.afterInvocation(token, null),这里默认传入了一个null,所以在invoke()中没有任何地方引用,所以有点让人费解,笔者大致看了一下afterInvocationManager的代码,没有深入研究,猜测afterInvocation()传入的null,应该是一个默认值,对于FilterSecurityInterceptor返回值void,可能无需更改,但对于MethodSecurityInterceptor是Aop实现的,每个方法都有可能有返回值,估计是针对MethodSecurityInterceptor实现的。这里不做深入研究了。
2. 注解式鉴权
步骤1:配置
注解式鉴权是Aop切面的机制,FilterSecurityInterceptor对其不能进行控制,因此二者关系并不大。其主要的实现类为AspectJMethodSecurityInterceptor->MethodSecurityInterceptor->AbstractSecurityInterceptor,按箭头方向依次继承,使用注解式鉴权,开启配置以及实例如下:
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Configuration
public class WebSecurityConfig {
}
@PreAuthorize("hasRole('ADMIN')")
@ApiOperation("异步获取上下文用户信息")
@GetMapping("/security/async")
public R<Object> asyncGetContextUser() throws Exception {
}
@PreAuthorize("hasAnyRole('ROOT','ADMIN')")
@ApiOperation("获取上下文用户信息")
@GetMapping("/security/sync")
public R<Object> getContextUser() {
}
步骤2:注解使用方式
securedEnabled
@EnableGlobalMethodSecurity(securedEnabled=true) 开启@Secured 注解过滤权限
jsr250Enabled
@EnableGlobalMethodSecurity(jsr250Enabled=true)开启@RolesAllowed 注解过滤权限
prePostEnabled
@PreAuthorize 在方法调用之前,基于表达式的计算结果来限制对方法的访问
@PostAuthorize 允许方法调用,但是如果表达式计算结果为false,将抛出一个安全性异常
@PostFilter 允许方法调用,但必须按照表达式来过滤方法的结果
@PreFilter 允许方法调用,但必须在进入方法之前过滤输入值
接下来我们分析MethodSecurityInterceptor的相关代码,代码如下:
public Object invoke(MethodInvocation mi) throws Throwable {
InterceptorStatusToken token = super.beforeInvocation(mi);
Object result;
try {
result = mi.proceed();
}
finally {
super.finallyInvocation(token);
}
//可以在此处对Aop的返回结果进行修改
return super.afterInvocation(token, result);
}
步骤3:源码分析
MethodSecurityInterceptor实现方式不同,beforeInvocation()中,obtainSecurityMetadataSource().getAttributes(object)这行代代码与FilterSecurityInterceptor进行的操作也很不一样,MethodSecurityInterceptor是通过反射,取到方法上的注解,然后包装成ConfigAttribute,截图和代码如下:
public final Collection<ConfigAttribute> getAttributes(Object object) {
if (object instanceof MethodInvocation) {
MethodInvocation mi = (MethodInvocation) object;
Object target = mi.getThis();
Class<?> targetClass = null;
if (target != null) {
targetClass = target instanceof Class<?> ? (Class<?>) target
: AopProxyUtils.ultimateTargetClass(target);
}
Collection<ConfigAttribute> attrs = getAttributes(mi.getMethod(), targetClass);
if (attrs != null && !attrs.isEmpty()) {
return attrs;
}
if (target != null && !(target instanceof Class<?>)) {
attrs = getAttributes(mi.getMethod(), target.getClass());
}
return attrs;
}
同样,this.accessDecisionManager.decide(authenticated, object, attributes);这行代码也很不一样,accessDecisionManager包含了三个投票器,如下:
- PreInvocationAuthorizationAdviceVoter
该投票器通过解析传入attibutes,根据SpEL表达式判断用户的权限是否满足条件,进行投票。
- RoleVoter
该投票器先判断ConfigAttribute的属性值是否以ROLE_前缀开头,如果满足条件,则进行权限比对,比对成功则投票通过。但是PreInvocationAttribute的getAttribute()始终返回null,因此PreInvocationAttribute不可能认证通过。
- AuthenticatedVoter
该投票器先判断ConfigAttribute的属性值是IS_AUTHENTICATED_FULLY、IS_AUTHENTICATED_REMEMBERED、IS_AUTHENTICATED_ANONYMOUSLY中哪一种认证方式,匹配到其中一种,则采用对应的方式进行认证,匹配不到则投弃权票。
这3种投票器是什么时候注入的?通过debug,我们发现是在GlobalMethodSecurityConfiguration#accessDecisionManager()硬编码写入的,调用堆栈和代码如下:
protected AccessDecisionManager accessDecisionManager() {
List<AccessDecisionVoter<?>> decisionVoters = new ArrayList<>();
if (prePostEnabled()) {
ExpressionBasedPreInvocationAdvice expressionAdvice =
new ExpressionBasedPreInvocationAdvice();
expressionAdvice.setExpressionHandler(getExpressionHandler());
decisionVoters
.add(new PreInvocationAuthorizationAdviceVoter(expressionAdvice));
}
if (jsr250Enabled()) {
decisionVoters.add(new Jsr250Voter());
}
RoleVoter roleVoter = new RoleVoter();
GrantedAuthorityDefaults grantedAuthorityDefaults =
getSingleBeanOrNull(GrantedAuthorityDefaults.class);
if (grantedAuthorityDefaults != null) {
roleVoter.setRolePrefix(grantedAuthorityDefaults.getRolePrefix());
}
decisionVoters.add(roleVoter);
decisionVoters.add(new AuthenticatedVoter());
return new AffirmativeBased(decisionVoters);
}