[13] FilterSecurityInterceptor

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;
}

image.png
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();
}

image.png
那么decisionVoters是什么时候注入的?我们在AbstractAccessDecisionManager构造方法方法上打个短断点,执行堆栈和创建decisionManager的代码如下:
image.png

@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中的过滤器了,截图比较直观,如下:
image.png
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,截图和代码如下:
image.png

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中哪一种认证方式,匹配到其中一种,则采用对应的方式进行认证,匹配不到则投弃权票。
image.png

这3种投票器是什么时候注入的?通过debug,我们发现是在GlobalMethodSecurityConfiguration#accessDecisionManager()硬编码写入的,调用堆栈和代码如下:
image.png

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);
}
  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值