spring security 核心 -- AbstractSecurityInterceptor

spring security 两个核心

  1. authentication 认证
  2. authorization(access-control) 授权(或者说是访问控制)
    认证是声明主体的过程,授权是指确定一个主体是否允许在你的应用程序执行一个动作的过程。

spring security 核心流程

这一套核心流程具体每条的实现由AbstractSecurityInterceptor 实现,也就是说AbstractSecurityInterceptor 只是定义了一些行为,然后这些行为的安排,也就是执行流程则是由具体的子类所实现,AbstractSecurityInterceptor 虽然也叫Interceptor ,但是并没有继承和实现任何和过滤器相关的类,具体和过滤器有关的部分是由子类所定义。每一种受保护对象都拥有继承自AbstrachSecurityInterceptor的拦截器类。spring security 提供了两个具体实现类,MethodSecurityInterceptor 将用于受保护的方法,FilterSecurityInterceptor 用于受保护的web 请求。

public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements
        Filter {
...
}
public class MethodSecurityInterceptor extends AbstractSecurityInterceptor implements
        MethodInterceptor {
...
}
  1. 查找当前请求里分配的"配置属性"。
  2. 把安全对象,当前的Authentication和配置属性,提交给AccessDecisionManager来进行以此认证决定。
  3. 有可能在调用的过程中,对Authentication进行修改。
  4. 允许安全对象进行处理(假设访问被允许了)。
  5. 在调用返回的时候执行配置的AfterInvocationManager。如果调用引发异常,AfterInvocationManager将不会被调用。

AbstractSecurityInterceptor 的两个实现都具有一致的逻辑

  1. 先将正在请求调用的受保护对象传递给beforeInvocation()方法进行权限鉴定。
  2. 权限鉴定失败就直接抛出异常了。
  3. 鉴定成功将尝试调用受保护对象,调用完成后,不管是成功调用,还是抛出异常,都将执行finallyInvocation()。
  4. 如果在调用受保护对象后没有抛出异常,则调用afterInvocation()。
public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements
        Filter {
    // ~ Static fields/initializers
    // =====================================================================================

    private static final String FILTER_APPLIED = "__spring_security_filterSecurityInterceptor_filterApplied";

    // ~ Instance fields
    // ================================================================================================

    private FilterInvocationSecurityMetadataSource securityMetadataSource;
    private boolean observeOncePerRequest = true;

    // ~ Methods
    // ========================================================================================================
      ...
    /**
     * Method that is actually called by the filter chain. Simply delegates to the
     * {@link #invoke(FilterInvocation)} method.
     *
     * @param request the servlet request
     * @param response the servlet response
     * @param chain the filter chain
     *
     * @throws IOException if the filter chain fails
     * @throws ServletException if the filter chain fails
     */
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        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) {
            // 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);
        }
    }
...
}
public class MethodSecurityInterceptor extends AbstractSecurityInterceptor implements
        MethodInterceptor {
    // ~ Instance fields
    // ================================================================================================

    private MethodSecurityMetadataSource securityMetadataSource;
...
    /**
     * This method should be used to enforce security on a MethodInvocation.
     *
     * @param mi The method being invoked which requires a security decision
     *
     * @return The returned value from the method invocation (possibly modified by the
     * {@code AfterInvocationManager}).
     *
     * @throws Throwable if any error occurs
     */
    public Object invoke(MethodInvocation mi) throws Throwable {
        InterceptorStatusToken token = super.beforeInvocation(mi);

        Object result;
        try {
            result = mi.proceed();
        }
        finally {
            super.finallyInvocation(token);
        }
        return super.afterInvocation(token, result);
    }
...
}

spring security 默认的过滤器是FilterSecurityInterceptor。spring security 的方法安全是需要配置启用的。

添加一个注解到类或者接口的方法中可以限制对相应方法的访问。spring security 的原生注解支持定义了一套用于该方法的属性。这些将被传递到AccessDecisionManager 用来做实际的决定:

public interface BankService {

@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account readAccount(Long id);

@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account[] findAccounts();

@Secured("ROLE_TELLER")
public Account post(Account account, double amount);
}

启用 JSR-250 注解使用

这些都是基于标准的,并允许应用简单的基于角色的约束,但是没有Spring Security的原生注解强大。要使用新的基于表达式的语法,你可以使用

public interface BankService {

@PreAuthorize("isAnonymous()")
public Account readAccount(Long id);

@PreAuthorize("isAnonymous()")
public Account[] findAccounts();

@PreAuthorize("hasAuthority('ROLE_TELLER')")
public Account post(Account account, double amount);
}

基于表达式的注解是一个很好的选择,如果你需要定义超过一个检查当前用户列表中的角色名称的简单的规则。

也可以使用注解启用
EnableGlobalMethodSecurity
我们可以在任何使用@Configuration的实例上,使用@EnableGlobalMethodSecurity注解来启用基于注解的安全性。例如下面会启用Spring的@Secured注解。

@EnableGlobalMethodSecurity(securedEnabled = true)
public class MethodSecurityConfig {
// ...
}
@EnableGlobalMethodSecurity(jsr250Enabled = true)
public class MethodSecurityConfig {
// ...
}
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig {
// ...
}

AbstractSecurityInterceptor

spring security 的核心是 AbstractSecurityInterceptor这个过滤器基本上控制着spring security 的整个流程。
spring security 会用到一些spring framework 提供的基础功能

  • spring 事件发布机制(ApplicationEventPublisher)
  • spring AOP advice 思想
  • spring messageSource 本地消息

spring security 的权限鉴定是由AccessDecisionManager 接口中的decide() 方法负责的

void decide(Authentication authentication, Object object,Collection configAttributes)  
 throws AccessDeniedException,InsufficientAuthenticationException;

authentication 就是主体对象,configAttributes 是主体(是受保护对象)的配置属性,至于第二个对象就是表示请求的受保护对象,基本上来说MethodInvocation(使用AOP)、JoinPoint(使用Aspectj) 和 FilterInvocation(web 请求)三种类型。

AbstractSecurityInterceptor 是一个实现了对受保护对象的访问进行拦截的抽象类。

ConfigAttribute

public interface ConfigAttribute extends Serializable {
    String getAttribute();
}

AccessDecisionManager 的decide() 方法是需要接收一个受保护对象对应的configAttribute集合的。一个configAttribute可能只是一个简单的角色名称,具体将视AccessDecisionManager的实现者而定。

一个"配置属性"可以看做是一个字符串,它对于AbstractSecurityInterceptor使用的类是有特殊含义的。它们由框架内接口ConfigAttribute表示。它们可能是简单的角色名称或拥有更复杂的含义,这就与AccessDecisionManager实现的先进程度有关了。AbstractSecurityInterceptor和配置在一起的 SecurityMetadataSource 用来为一个安全对象搜索属性。通常这个属性对用户是不可见的。配置属性将以注解的方式设置在受保护方法上,或者作为受保护URLs的访问属性。例如,当我们看到像命名空间中的介绍,这是说配置属性ROLE_A和ROLE_B适用于匹配Web请求的特定模式。在实践中,使用默认的AccessDecisionManager配置, 这意味着,任何人谁拥有GrantedAuthority只要符合这两个属性将被允许访问。严格来说,它们只是依赖于AccessDecisionManager实施的属性和解释。使用前缀ROLE_是一个标记,以表明这些属性是角色,应该由Spring Security的RoleVoter前缀被消耗掉。这只是使用AccessDecisionManager的选择基础。

RunAsManager

RunAsManagerImpl构建新的Authentication的核心代码如下所示。

    public Authentication buildRunAs(Authentication authentication, Object object, Collection attributes) {

        List newAuthorities = new ArrayList();

        for (ConfigAttribute attribute : attributes) {

            if (this.supports(attribute)) {

                GrantedAuthority extraAuthority = newSimpleGrantedAuthority(getRolePrefix() + attribute.getAttribute());

                newAuthorities.add(extraAuthority);

            }

        }

        if (newAuthorities.size() == 0) {

            returnnull;

        }

        // Add existing authorities

        newAuthorities.addAll(authentication.getAuthorities());

        returnnew RunAsUserToken(this.key, authentication.getPrincipal(), authentication.getCredentials(),

                newAuthorities, authentication.getClass());

    }

在某些情况下你可能会想替换保存在SecurityContext中的Authentication。这可以通过RunAsManager来实现的。在AbstractSecurityInterceptor的beforeInvocation()方法体中,在AccessDecisionManager鉴权成功后,将通过RunAsManager在现有Authentication基础上构建一个新的Authentication,如果新的Authentication不为空则将产生一个新的SecurityContext,并把新产生的Authentication存放在其中。这样在请求受保护资源时从SecurityContext中获取到的Authentication就是新产生的Authentication。待请求完成后会在finallyInvocation()中将原来的SecurityContext重新设置给SecurityContextHolder。AbstractSecurityInterceptor默认持有的是一个对RunAsManager进行空实现的NullRunAsManager。此外,Spring Security对RunAsManager有一个还有一个非空实现类RunAsManagerImpl,其在构造新的Authentication时是这样的逻辑:如果受保护对象对应的ConfigAttribute中拥有以“RUN_AS_”开头的配置属性,则在该属性前加上“ROLE_”,然后再把它作为一个GrantedAuthority赋给将要创建的Authentication(如ConfigAttribute中拥有一个“RUN_AS_ADMIN”的属性,则将构建一个“ROLE_RUN_AS_ADMIN”的GrantedAuthority),最后再利用原Authentication的principal、权限等信息构建一个新的Authentication进行返回;如果不存在任何以“RUN_AS_”开头的ConfigAttribute,则直接返回null。

AfterInvocationManager

按照下面安全对象执行和返回的方式-可能意味着完全的方法调用或过滤器链的执行-在AbstractSecurityInterceptor得到一个最后的机会来处理调用。这种状态下AbstractSecurityInterceptor对有可能修改返回对象感兴趣。你可能想让它发生,因为验证决定不能“关于如何在”一个安全对象调用。高可插拔性,AbstractSecurityInterceptor通过控制AfterInvocationManager在实际需要的时候修改对象。这里类实际上可能替换对象,或者抛出异常,或者什么也不做。如果调用成功后,检查调用才会执行。如果出现异常,额外的检查将被跳过。


AbstractSecurityInterceptor 中的一些方法

  1. afterPropertiesSet()
public void afterPropertiesSet() throws Exception {
  Assert.notNull(getSecureObjectClass(),
                "Subclass must provide a non-null response to getSecureObjectClass()");
  Assert.notNull(this.messages, "A message source must be set");
Assert.notNull(this.authenticationManager, "An AuthenticationManager is required");
Assert.notNull(this.accessDecisionManager, "An AccessDecisionManager is required");
Assert.notNull(this.runAsManager, "A RunAsManager is required");
Assert.notNull(this.obtainSecurityMetadataSource(),
                "An SecurityMetadataSource is required");
   ...
}

主要是对类的属性进行校验。

  1. beforeInvocation()
protected InterceptorStatusToken beforeInvocation(Object object) {
        Assert.notNull(object, "Object was null");
        ...
                // 获取Object 配置属性
        Collection attributes = this.obtainSecurityMetadataSource()
                .getAttributes(object);
                ...
                // authentication 必须存在
        if (SecurityContextHolder.getContext().getAuthentication() == null) {
            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
        Authentication runAs = this.runAsManager.buildRunAs(authenticated, object,
                attributes);
        if (runAs == null) {
            if (debug) {
                logger.debug("RunAsManager did not change Authentication object");
            }
            // no further work post-invocation
            return new InterceptorStatusToken(SecurityContextHolder.getContext(), false,
                    attributes, object);
        }else {
            if (debug) {
                logger.debug("Switching to RunAs Authentication: " + runAs);
            }
            SecurityContext origCtx = SecurityContextHolder.getContext();
            SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
            SecurityContextHolder.getContext().setAuthentication(runAs);
            // need to revert to token.Authenticated post-invocation
            return new InterceptorStatusToken(origCtx, true, attributes, object);
        }
    }

   /**
     * Checks the current authentication token and passes it to the AuthenticationManager
     */
    private Authentication authenticateIfRequired() {
        Authentication authentication = SecurityContextHolder.getContext()
                .getAuthentication();
          // 如果authentication已经被验证过了并且总是重新验证为false
        if (authentication.isAuthenticated() && !alwaysReauthenticate) {
            if (logger.isDebugEnabled()) {
                logger.debug("Previously Authenticated: " + authentication);
            }
              // 则跳过验证
            return authentication;
        }
         // 否则 authenticationManager 执行验证
        authentication = authenticationManager.authenticate(authentication);

        // We don't authenticated.setAuthentication(true), because each provider should do
        // that
        SecurityContextHolder.getContext().setAuthentication(authentication);

        return authentication;
    }

这个方法实现了对访问对象的权限校验,内部使用了AccessDecisionManager 和 AuthenticationManager

  1. finallyInvocation()
protected Object afterInvocation(InterceptorStatusToken token, Object returnedObject) {
        if (token == null) {
            // public object
            return returnedObject;
        }

        finallyInvocation(token); // continue to clean in this method for passivity

        if (afterInvocationManager != null) {
            // Attempt after invocation handling
            try {
                returnedObject = afterInvocationManager.decide(token.getSecurityContext()
                        .getAuthentication(), token.getSecureObject(), token
                        .getAttributes(), returnedObject);
            }
            catch (AccessDeniedException accessDeniedException) {
                AuthorizationFailureEvent event = new AuthorizationFailureEvent(
                        token.getSecureObject(), token.getAttributes(), token
                                .getSecurityContext().getAuthentication(),
                        accessDeniedException);
                publishEvent(event);

                throw accessDeniedException;
            }
        }

        return returnedObject;
    }

这个方法实现了对返回结果的处理,在注入了AfterInvocationManager的情况下默认会调用其decide()的方法

  1. finallyInvocation()
/**
     * Cleans up the work of the AbstractSecurityInterceptor after the secure
     * object invocation has been completed. This method should be invoked after the
     * secure object invocation and before afterInvocation regardless of the secure object
     * invocation returning successfully (i.e. it should be done in a finally block).
     *
     * @param token as returned by the {@link #beforeInvocation(Object)} method
     */
    protected void finallyInvocation(InterceptorStatusToken token) {
        if (token != null && token.isContextHolderRefreshRequired()) {
            if (logger.isDebugEnabled()) {
                logger.debug("Reverting to original Authentication: "
                        + token.getSecurityContext().getAuthentication());
            }

            SecurityContextHolder.setContext(token.getSecurityContext());
        }
    }

方法用于实现受保护对象请求完毕后的一些清理工作,主要是如果在beforeInvocation()中改变了SecurityContext,则在finallyInvocation()中需要将其恢复为原来的SecurityContext,该方法的调用应当包含在子类请求受保护资源时的finally语句块中

AbstractSecurityInterceptor 和它的相关对象

spring security 核心 -- AbstractSecurityInterceptor_第1张图片
security-interception.png

你可能感兴趣的:(spring security 核心 -- AbstractSecurityInterceptor)