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 <code>MethodInvocation</code>.
     *
     * @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 的方法安全是需要配置启用的。
<global-method-security secured-annotations="enabled" />
添加一个注解到类或者接口的方法中可以限制对相应方法的访问。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 注解使用
<global-method-security jsr250-annotations="enabled" />
这些都是基于标准的,并允许应用简单的基于角色的约束,但是没有Spring Security的原生注解强大。要使用新的基于表达式的语法,你可以使用
<global-method-security pre-post-annotations="enabled" />

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<ConfigAttribute> 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的访问属性。例如,当我们看到像<intercept-url pattern='/secure/**' access='ROLE_A,ROLE_B'/>命名空间中的介绍,这是说配置属性ROLE_A和ROLE_B适用于匹配Web请求的特定模式。在实践中,使用默认的AccessDecisionManager配置, 这意味着,任何人谁拥有GrantedAuthority只要符合这两个属性将被允许访问。严格来说,它们只是依赖于AccessDecisionManager实施的属性和解释。使用前缀ROLE_是一个标记,以表明这些属性是角色,应该由Spring Security的RoleVoter前缀被消耗掉。这只是使用AccessDecisionManager的选择基础。

RunAsManager

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

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

        List<GrantedAuthority> newAuthorities = new ArrayList<GrantedAuthority>();

        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<ConfigAttribute> 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 <tt>AbstractSecurityInterceptor</tt> 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 和它的相关对象

security-interception.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,651评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,468评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,931评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,218评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,234评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,198评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,084评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,926评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,341评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,563评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,731评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,430评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,036评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,676评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,829评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,743评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,629评论 2 354

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,652评论 18 139
  • Spring Web MVC Spring Web MVC 是包含在 Spring 框架中的 Web 框架,建立于...
    Hsinwong阅读 22,394评论 1 92
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,806评论 6 342
  • 1.1 spring IoC容器和beans的简介 Spring 框架的最核心基础的功能是IoC(控制反转)容器,...
    simoscode阅读 6,713评论 2 22
  • 前言 本章内容: ▪️Spring Security介绍 ▪️使用Servlet规范中的Filter保护Web应用...
    Chandler_珏瑜阅读 7,172评论 0 68