LOGI

Spring Security Architecture

这是一篇介绍 Spring Security 设计和基本构建块的入门指南。尽管我们只提及应用安全的基本概念,但这足以扫清开发者使用 Spring Security 过程中的许多困惑。为此,我们需要了解如何通过过滤器,通常来说,是使用方法注解来保护网页应用安全。当你需要从更高等级理解安全应用如何工作,如何自定义,或者想深入研究应用安全,请阅读这份指南。

这份指南不想成为解决非基本问题的手册或攻略(请寻找其他资源),但它可能对初学者和专家都很有用。Spring Boot 为安全应用提供了一些默认行为,后文会反复提及它,这有助于理解一些行为是如何与整体架构相适应的。

[tip type="info" title="备注"]
注意,所有原则同样适用于未使用 Spring Boot 的应用。
[/tip]

Authentication and Access Control

应用安全归根结底是两个或多或少的独立问题:认证(你是谁?)和授权(你被允许做什么?)。有时人们会说 “访问控制” 而不是 “授权”,这可能令人困惑,但由于 “授权” 在其他地方表达的含义太过丰富,所以 “访问控制” 更利于理解。Spring Security 在架构设计上分离了认证和授权,并且分别为两者提供了策略和扩展点。

Authentication

提供认证策略的接口主要是 AuthenticationManager,它仅有一个方法:

public interface AuthenticationManager {
    Authentication authenticate(Authentication authentication)
        throws AuthenticationException;
}

AuthenticationManager 实例的 authenticate() 方法会执行以下 3 种操作之一:

AuthenticationException 是一个运行时异常。应用通常以通用方式处理它,这取决于应用的风格或用途。换句话说,用户代码通常不需要捕捉和处理它。例如,网页可能渲染一个告知用户认证失败的页面,而后端 HTTP 服务可能会响应 401 状态码,并根据上下文决定是否附带 WWW-Authenticate 头。

最常用的 AuthenticationManager 实现类是 ProviderManager,它的功能委托给了由多个 AuthenticationProvider 实例构成的认证链。单个 AuthenticationProvider 类似于 AuthenticationManager,但它多了一个额外方法,允许调用者查询自身是否支持给定的 Authentication 类型:

public interface AuthenticationProvider {
    Authentication authenticate(Authentication authentication)
        throws AuthenticationException;

    boolean supports(Class<?> authentication);
}

supports() 方法的参数 Class<?> 其实是 Class<? extends Authentication>(它仅被询问是否支持传入 authenticate() 方法的对象)。通过委托的 AuthenticationProviders 认证链,ProviderManger 可以在单个应用中支持多个不同的认证机制。如果 ProviderManger 无法识别某个特定的 Authentication 实例类型,该认证将被跳过。

ProviderManger 有个可选的父节点,可以询问它是否所有 provider 都返回 null。如果父节点不存在,一个 null Authentication 引用将产生 AuthenticationException

有时,应用包含若干受保护资源的逻辑分组(例如,所有网页资源都匹配像 /api/** 的路径模式),每个分组可以拥有自己专用的 AuthenticationManager。通常,它们都是 ProviderManager,并且共享一个父节点。所以父节点是一种 “全局” 资源,扮演着其他 provider 的后备服务。

图一:使用 ProviderManager 构成的 AuthenticationManager 层级

Customizing Authentication Managers

Spring Security 提供了一些配置助手,可以让你在应用中快速建立通用的认证管理功能。最常用的助手是 AuthenticationManagerBuilder,它可以出色地完成内存,JDBC,或 LDAP 用户细节的构建,也可以添加自定义 UserDetailsService。下例中的应用配置了全局 (parent) AuthenticationManager:

@Configuration
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {
    ... // web stuff here

    @Autowired
    public void initialize(AuthenticationManagerBuilder builder, DataSource dataSource) {
        builder.jdbcAuthentication().dataSource(dataSource).withUser("dave")
            .password("secret").roles("USER");
    }
}

AuthenticationManagerBuilder 的适应范围并非仅限示例中的网页应用(阅读 Web Security 详细了解网页应用安全的实现细节)。注意到 AuthenticationManagerBuilder@Autowired 注解到 @Bean 中的方法上,这就是它能够构建全局 (parent) AuthenticationManager 的原因。相反,看下面的例子:

@Configuration
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {

    @Autowired
    DataSource dataSource;

    ... // web stuff here

    @Override
    public void configure(AuthenticationManagerBuilder builder) {
        builder.jdbcAuthentication().dataSource(dataSource).withUser("data")
            .password("secret")。roles("USER");
    }
}

如果我们 @Override configurer 的方法,AuthenticationManagerBuilder 构建的将是 “本地” AuthenticationManager,成为全局下的子节点。在 Spring Boot 应用中,你可以把全局节点 @Autowired 到另一个 bean 中,但你无法连接本地节点,除非显示地暴露它。

Authorization or Access Control

一旦认证成功,下一步便是授权,授权的核心策略是 AccessDecisionManager。Spring Security 提供了三种实现,它们均可委托给由多个 AccessDecisionVoter 构成的代理链,有点像 ProviderManager 委托给了 AuthenticationProvider

AccessDecisionVoter 会考察 Authentication (代表 principal)和一个被 ConfigAttributes 修饰的安全 Object

boolean supports(ConfigAttribute attribute);

boolean supports(Class<?> clazz);

int vote(Authentication authentication, S object,
    Collection<ConfigAttribute> attributes);

ObjectAccessDecisionManagerAccessDecisionVoter 的签名里是完全通用的。它代表任何用户想要访问的东西(最常见的例子是一个网页或 Java 类中的一个方法)。ConfigAttributes 也是相当通用的,它们使用元数据装饰安全 Object,这些元数据决定访问此 Object 必须具有的许可等级。ConfigAttribute 是一个接口,它仅含一个方法(这是非常常见的,并且该方法返回一个 String),这些字符串以资源所有者想要的方式编码, 规定了谁有权访问它。典型的 ConfigAttribute 是用户角色名称(像 ROLE_AMDINROLE_AUDIT),它们通常有特定的格式(如 ROLE_ 前缀)或者是需要计算的表达式。

大多数用户使用默认的 AccessDecisionManager,它是基于肯定的(如果任何一个投票者同意,访问权就被授予)。想要自定义时,要么通过增加新的投票者,要么通过修改已有投票者的行为。

ConfigAttributes 是十分常用的,它的格式是 Spring 表达式语言(SpEL)—— 例如,isFullyAuthenticated() && hasRole('user')AccessDecisionVoter 能够处理这种格式,并且会为它们创建上下文。如果要扩大表达式的支持范围,需要实现自定义的 SecurityExpressionRoot,有时也需要实现 SecurityExpressionHandler

Web Security

Spring Security 在 WEB 层(用于编写 UI 和 HTTP 后端)是基于 Servlet Filters (过滤器) 的,所以先粗略看一眼 Filters 的角色是很有帮助的。下图展示了处理单个 HTTP 请求的典型层级。

客户端发送请求给服务端,容器根据路径决定哪个 Filter 和哪个 Servlet 去响应它。大多数情况下,单个 servlet 处理单个请求,但过滤器组成了一个链条,所以它们是有顺序的。事实上,一个过滤器如果想自己处理某个请求,可以否决链条中的其余部分。过滤器也可以修改被下层过滤器和 servlet 使用的请求和响应。过滤器链的顺序非常重要,Spring Boot 通过两套机制管理它:Filter 类型的 @Bean 可以用 @Order 注解,或者实现 Ordered 接口;也可以是 FilterRegistrationBean 的一部分,该类同样实现了 Ordered 接口。一些内置过滤器定义了自己的常量来表明它们与其他过滤器组合时想要的顺序(例如,Spring Session 下的 SessionRepositoryFilter 有一个值为 Integer.MIN_VALUE + 50DEFAULT_ORDER,它告诉我们它想位于过滤链的靠前的位置,但也不排除其他过滤器在它前面)。

Spring Security 以单个过滤器形式安装到过滤器链中,它的具体类型是 FilterChainProxy,原因我们马上介绍。在 Sprint Boot 应用中,安全过滤器是 ApplicationContext 中的一个 @Bean,并且它被默认安装以处理每个请求。它安装的位置由 SecurityProperties.DEFAULT_FILTER_ORDER 定义,而后者又是由 FilterRegistrationBean.REQUEST_WRAPPER_FILTER_MAX_ORDER(Spring Boot 应用期待过滤器拥有的最大顺序,这些过滤器包裹请求,并修改请求行为) 决定的。还有一点要说:从容器角度看,Spring Security 是单个过滤器,但其内部包含了若干附加过滤器,它们中的每个都扮演着不同的角色。下图展示了这种关系:

图二:Spring Security 在物理上是单个 Filter,但它把处理功能委托给内部过滤器链。

事实上,安全过滤器还有一个间接层:它通常作为 DelegatingFilterProxy 安装到容器中,并且不必是一个 @Bean。该代理委托给作为 @Bean 并且有固定名称 springSecurityFilterChainFilterChainProxyFilterChainProxy 包含了安排于内部的一或多条过滤器链的完整安全逻辑。所有过滤器链都有相同的 API(它们都实现了 Servlet 规范中的 Filter 接口),并且都有机会终止剩余链条。

Spring Security 管理的多个过滤器链拥有相同的顶级节点 FilterChainProxy,其内部对容器透明。Spring Security 过滤器包含了一系列过滤器链,会将请求分发给第一个匹配的链条。下图展示了基于请求路径的过滤器分发(/foo/**/** 之前匹配)。这很常见但并不是匹配请求的唯一方法。这一分发过程最重要的特性是每个请求仅被一条过滤器链处理。

图三:Spring Security FilterChainProxy 把请求分发给第一个匹配的过滤器。

无自定义安全配置的普通 Spring Boot 应用有若干条(记为 n)过滤器链,n 通常等于 6。前 (n-1)条仅仅用来忽略静态资源路径,像 /css/** /images/**,以及错误视图 /error。(路径可以被带有 security.ignored 的用户控制,该属性来自配置 bean SecurityProperties。)最后一条链匹配未被处理的所有路径 /**,并且更活跃,包含了认证、授权、异常处理、会话处理、请求头写入等等逻辑。默认情况下该链条中一共有 11 个过滤器,但通常来说用户不必关心哪些过滤器在什么时候被使用。

[tip type="info" title="备注"]
事实上,Spring Security 内部的所有过滤器对容器透明是十分重要的,尤其是在 Spring Boot 应用中,默认情况下,所有 @Bean 类型的 Filter 都自动注册到容器中。所以如果你想添加自定义安全链,要么不要在过滤器上使用 @Bean 注解,要么使用 FilterRegistrationBean 包裹它,来显示关闭容器注册。
[/tip]

Creating and Customizing Filter Chains

Spring Boot 应用中的默认回退过滤器链(匹配 /** 请求的那条)有一个预定义顺序 SecurityProperties.BASIC_AUTH_ORDER。你可以通过设置 security.basic.enabled=false 来完全关闭它,也可以把它当作备用链,使用更小的顺序定义其他规则。要做到后者,像下面这样,添加一个类型为 WebSecurityConfigurerAdapter (或 WebSecurityConfigurer)的 @Bean,随后用 @Order 修饰它。

@Configuration
@Order(SecurityProperties.BASIC_AUTH_ORDER - 10)
public class ApplicationConfigurerAdapter extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.antMatcher("/match1/**")
        ...;
    }
}

Request Matching for Dispatch and Authorization

一个安全过滤器链(或者,等价于一个 WebSecurityConfigurerAdapter)包含一个请求匹配器,它决定了是否把自身应用到某个 HTTP 请求上。一旦某条过滤器链被选定,其它链条就不予考虑。但是在链条内部,你可以通过在 HttpSecurity 中设置额外的匹配器,更细粒度地对授权进行控制,就像下面这样:

@Configuration
@Order(SecurityPropereties.BASIC_AUTH_ORDER - 10)
public class ApplicationConfigurerAdapter extends WebSecurityConfigurerAdapter {
    @Override
    protected void configurer(HttpSecurity http) throws Exception {
        http.antMatcher("/match1/**").authorizedRequests()
            .antMatcher("/match1/user").hasRole("USER")
            .antMatcher("/match1/spam").hasRole("SPAM")
            .anyRequest().isAuthenticated();
    }
}

配置 Spring Security 时最容易犯的错误之一就是忘记这些匹配器适用于不同的处理过程。WebSecurityConfigurerAdapter 属于全局过滤器链,而 HttpSecurity 只选择性地应用访问规则。

Combining Application Security Rules with Actuator Rules

如果你使用 Spring Boot Actuator 管理多个端点,你大概希望它们是安全的,默认情况下,它们的确如此。实际上,只要你把 Actuator 添加到安全应用中,你就获得了一条仅作用了 Actuator 端点的过滤器链。链条中定义了一个仅匹配 Actuator 端点的请求匹配器,它的 Order 是 ManagementServerProperties.BASIC_AUTH_ORDER,比默认 SecurityProperties 后备过滤器的 Order 小 5,所以会先于后备链条被咨询。

如果你想把应用的安全规则应用到 Actuator 端点上,可以添加一条比 Actuator Order 小的过滤器链,让它的请求匹配器包含所有 Actuator 端点。如果你倾向于为 Actuator 端点保留默认安全设置,最容易的方法是让你的过滤器排在 Actuator 后面,但早于后备过滤器(例如,ManagementServerProperties.BASIC_AUTH_ORDER + 1),像下面这样:

@Configuraiton
@Order(ManagementServerProperties.BASIC_AUTH_ORDER + 1)
public class ApplicationConfigurerAdapter extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.antMatcher("/foo/**")
        ...;
    }
}

[tip type="info" title="备注"]
Spring Security 如今在 Web 层是和 Servlet API 绑定的,所以实际上它只适用于在 servlet 容器中运行的应用,可以是嵌入的或者其他形式。但它与 Spring MVC 或其他 Spring Web 技术栈没有关联,所以可以在任何 servlet 应用中使用——例如,JAX-RS 应用。
[/tip]

Method Security

除了可以保护网页应用,Spring Security 也支持在 Java 方法执行上设定访问规则。对 Spring Security 来说,这只不过是一种不同形式的 “受保护资源”。而对于开发者,则意味着访问规则可以声明为与 ConfigAttribute 字符串相同的形式(例如,角色和表达式),仅仅是在代码中换个地方。首先要做的是开启 method security——例如,在应用的顶层配置上:

@SpringBootApplicatioin
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SampleSecureApplication {}

之后便可以直接声明方法资源:

@Service
public class MyService {
    @Secured("ROLE_USER")
    public String secure() {
        return "Hello Security";
    }
}

例子中是一个包含安全方法的服务。如果 Spring 创建了一个这种类型的 @Bean,它就被代理,调用者必须通过安全拦截器才能真正执行方法。一旦访问被否定,调用者将得到一个 AccessDeniedException,而非真正的方法执行结果。

还可以在方法上使用其他注解来增强安全限制,最为显著的是 @PreAuthorize@PostAuthorize,它们分别允许你编写包含方法参数和返回值引用的表达式。

[tip type="info" title="建议"]
同时使用 Web Security 和 Method Security 是不常见的。过滤器链提供了诸如认证,重定向到登录页面等用户体验功能,而方法安全提供了更加细粒度的安全防护。
[/tip]

Working with Threads

Spring Security 在架构上就是线程绑定的,因为它需要为诸多下游消费者提供当前通过认证的实体。SecurityContext 是最基本的构建块,包含了一个 Authentication (当一个用户登录后他就成为一个 Authentication,显然他已通过认证)。你总是可以方便地通过 SecurityContextHolder 中的静态方法访问和操纵 SecurityContext,它反过来又去操纵一个 ThreadLocal。下面的示例展示了这种设定:

SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
assert(authentication.isAuthenticated);

上面的代码在用户程序中并不常用,除非你需要编写自定义认证过滤器(即便是那样,Spring Security 中仍有基本类可以使用,没有必要通过 SecurityContextHolder)。

如果需要在 Web 端点访问当前认证的用户,你可以像下面这样,在 @RequestMapping 下使用一个方法参数:

@RequestMapping("/foo")
public String foo(@AuthenticationPrincipal User user) {
    ... // do stuff with user
}

该注解会从 SecurityContext 中拉取当前 Authentication,调用它的 getPrincipal() ,生成方法参数。Principal 的类型取决于验证 AuthenticationAuthenticationManager,所以这是一个获得类型安全的用户数据引用的小技巧。

如果正在使用 Spring Security,HttpServletRequest 中的 Principal 的类型也是 Authentication,你可以直接使用它。

@RequestMapping("/foo")
public String foo(Principal principal) {
    Authentication authentication = (Authentication) principal;
    User = (User) authentication.getPrincipal();
    ... // do stuff with user
}

如果你需要编写不使用 Spring Security 仍能工作的代码,这可以会有所帮助(你可能需要更谨慎地加载 Authentication 类)。

Processing Secure Methods Asynchronously

由于 SecurityContext 是线程绑定的,如果你想做任何调用安全方法的后台处理(例如,使用 @Async),你需要确保上下文已经传送。这归结为使用在后台运行的任务(RunnableCallable 等)包裹 SecurityContext。为了把 SecurityContext 传送到 @Async 方法中,你需要提供一个 AsynConfigurer 并且确保 Executor 是正确类型:

@Configuration
public class ApplicationConfiguration extends AsyncConfigurerSupport {
    @Override
    public Executor getAsyncExecutor() {
        return new DelegatingSecurityContextExecutorService(Executors.newFixedThreadPool(5));
    }
}

本文译自 spring-security-architecture,译者:LOGI

当前页面是本站的「Google AMP」版。查看和发表评论请点击:完整版 »