ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
# 春季安全架构 本指南是Spring Security的入门,它提供了对该框架的设计和基本构建块的见解。 我们仅介绍应用程序安全性的最基础知识。 但是,这样做可以消除使用Spring Security的开发人员遇到的一些困惑。 为此,我们看一下通过使用过滤器,更普遍地通过使用方法注释,将安全性应用到Web应用程序中的方式。 当您需要对安全应用程序的工作原理,如何对其进行自定义或需要学习如何考虑应用程序安全性的高级了解时,请使用本指南。 本指南的目的不是用来解决最基本的问题(还有其他来源)的手册或食谱,但对于初学者和专家都可能有用。 还经常引用Spring Boot,因为它为安全的应用程序提供了一些默认行为,并且有助于理解它与整个体系结构之间的关系。 笔记所有这些原则同样适用于不使用Spring Boot的应用程序。 ## 身份验证和访问控制 应用程序安全性可以归结为或多或少的两个独立问题:身份验证(您是谁?)和授权(您被允许做什么?)。 有时人们会说“访问控制”而不是“授权”,这可能会造成混乱,但是以这种方式思考可能会有所帮助,因为“授权”在其他地方很繁重。 Spring Security的体系结构旨在将身份验证与授权分开,并具有策略和扩展点。 ### 验证 身份验证的主要策略界面是 `AuthenticationManager`,它只有一种方法: ~~~java public interface AuthenticationManager { Authentication authenticate(Authentication authentication) throws AuthenticationException; } ~~~ 一个 `AuthenticationManager` 可以做三件事之一 `authenticate()` 方法: * 返回一个 `Authentication` (通常与 `authenticated=true`)是否可以验证输入内容代表有效的委托人。 * 扔一个 `AuthenticationException` 如果它认为输入代表无效的主体。 * 返回 `null` 如果无法决定。 `AuthenticationException`是运行时异常。 它通常由应用程序以通用方式处理,具体取决于应用程序的样式或目的。 换句话说,通常不希望用户代码捕获并处理它。 例如,Web UI可能会呈现一个页面,指出认证失败,而后端HTTP服务则可能会发送401响应(带有或不带有验证码) `WWW-Authenticate` 标头取决于上下文。 最常用的实现 `AuthenticationManager` 是 `ProviderManager`,代表一系列 `AuthenticationProvider`实例。 一个 `AuthenticationProvider` 有点像 `AuthenticationManager`,但是它有一个额外的方法,允许调用者查询它是否支持给定的 `Authentication` 类型: ~~~java public interface AuthenticationProvider { Authentication authenticate(Authentication authentication) throws AuthenticationException; boolean supports(Class<?> authentication); } ~~~ 这 `Class<?>` 的论点 `supports()` 方法是真的 `Class<? extends Authentication>` (只会询问它是否支持传递到 `authenticate()`方法)。 一个 `ProviderManager` 通过委托到一个应用程序链,可以在同一应用程序中支持多种不同的身份验证机制 `AuthenticationProviders`。 如果一个 `ProviderManager` 无法识别特定的 `Authentication` 实例类型,将被跳过。 一个 `ProviderManager` 有一个可选的父级,如果所有提供程序都返回,则可以咨询该父级 `null`。 如果父级不可用,则 `null` `Authentication` 导致 `AuthenticationException`. 有时,应用程序具有逻辑上受保护的资源组(例如,所有与路径模式匹配的Web资源,例如 `/api/**`),每个组可以有自己专用的 `AuthenticationManager`。 通常,这些都是 `ProviderManager`,并且他们共享一个父母。 因此,父级是一种“全局”资源,充当所有提供者的后备。 ![具有公共父级的ProviderManager](https://github.com/spring-guides/top-spring-security-architecture/raw/master/images/authentication.png) 图1. An `AuthenticationManager` 层次结构使用 `ProviderManager` ### 自定义身份验证管理器 Spring Security提供了一些配置助手,可以快速获取在应用程序中设置的通用身份验证管理器功能。 最常用的助手是 `AuthenticationManagerBuilder`,这对于设置内存,JDBC或LDAP用户详细信息或添加自定义非常有用 `UserDetailsService`。 以下示例显示了配置全局(父)的应用程序 `AuthenticationManager`: ~~~java @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"); } } ~~~ 此示例与一个Web应用程序有关,但是 `AuthenticationManagerBuilder`适用范围更广( 请参阅 [Web安全](https://spring.io/guides/topicals/spring-security-architecture/#web-security) 有关如何实现Web应用程序安全性的更多详细信息, 性)。 请注意 `AuthenticationManagerBuilder` 是 `@Autowired` 变成一个方法 `@Bean`\-这就是它构建全球(父)的原因 `AuthenticationManager`。 相比之下,请考虑以下示例: ~~~java @Configuration public class ApplicationSecurity extends WebSecurityConfigurerAdapter { @Autowired DataSource dataSource; ... // web stuff here @Override public void configure(AuthenticationManagerBuilder builder) { builder.jdbcAuthentication().dataSource(dataSource).withUser("dave") .password("secret").roles("USER"); } } ~~~ 如果我们使用了 `@Override` 配置程序中的方法 `AuthenticationManagerBuilder` 仅用于构建“本地” `AuthenticationManager`,这将是全球儿童的子代。 在Spring Boot应用程序中,您可以 `@Autowired` 将全局的一个放到另一个bean中,但是除非您自己显式公开它,否则您不能对本地的那个使用它。 Spring Boot提供了一个默认的全局 `AuthenticationManager` (只有一个用户),除非您通过提供自己的type Bean来抢占它 `AuthenticationManager`。 缺省值本身具有足够的安全性,除非您积极需要自定义全局变量,否则不必担心太多 `AuthenticationManager`。 如果您进行了任何配置来构建 `AuthenticationManager`,您通常可以在本地对要保护的资源执行此操作,而不必担心全局默认值。 ### 授权或访问控制 身份验证成功后,我们可以继续进行授权,这里的核心策略是 `AccessDecisionManager`。 该框架提供了三种实现,所有这三个实现都委托给一个 `AccessDecisionVoter` 实例,有点像 `ProviderManager` 代表参加 `AuthenticationProviders`. 一个 `AccessDecisionVoter` 认为 `Authentication` (代表委托人)和担保人 `Object`,其中已装饰有 `ConfigAttributes`: ~~~java boolean supports(ConfigAttribute attribute); boolean supports(Class<?> clazz); int vote(Authentication authentication, S object, Collection<ConfigAttribute> attributes); ~~~ 这 `Object` 是完全通用的签名 `AccessDecisionManager` 和 `AccessDecisionVoter`。 它代表用户可能想要访问的任何内容(Web资源或Java类中的方法是两种最常见的情况)。 这 `ConfigAttributes` 也很通用,代表安全的装饰 `Object` 带有确定访问它所需权限级别的一些元数据。 `ConfigAttribute`是一个接口。 它只有一个方法(非常通用,并返回一个 `String`),因此这些字符串以某种方式编码了资源所有者的意图,表达了有关允许谁访问资源的规则。 典型的 `ConfigAttribute` 是用户角色的名称(例如 `ROLE_ADMIN` 或者 `ROLE_AUDIT`),并且它们通常具有特殊的格式(例如 `ROLE_` 前缀)或表示需要求值的表达式。 大多数人使用默认 `AccessDecisionManager`,这是 `AffirmativeBased`(如果有任何选民肯定地返回,则将授予访问权限)。 通过添加新的选项或修改现有选项的工作方式,任何定制都倾向于在投票者中发生。 这是很常见的使用 `ConfigAttributes` 是Spring Expression Language(SpEL)表达式-例如, `isFullyAuthenticated() && hasRole('user')`。 这得到了 `AccessDecisionVoter`可以处理表达式并为其创建上下文。 为了扩展可以处理的表达式的范围,需要自定义实现 `SecurityExpressionRoot` 有时也 `SecurityExpressionHandler`. ## 网络安全 Web层(用于UI和HTTP后端)中的Spring Security基于Servlet `Filters`,因此先了解一下 `Filters`一般来说。 下图显示了单个HTTP请求的处理程序的典型分层。 ![委托给Servlet的过滤器链](https://github.com/spring-guides/top-spring-security-architecture/raw/master/images/filters.png) 客户端将请求发送到应用程序,然后容器根据请求URI的路径确定对它应用哪些过滤器和哪个servlet。 一个servlet最多只能处理一个请求,但是过滤器形成一个链,因此它们是有序的。 实际上,如果过滤器要处理请求本身,则可以否决该链的其余部分。 过滤器还可以修改下游过滤器和Servlet中使用的请求或响应。 过滤器链的顺序非常重要,Spring Boot通过两种机制对其进行管理: `@Beans` 类型的 `Filter` 可以有一个 `@Order` 或实施 `Ordered`,并且它们可以成为 `FilterRegistrationBean`该订单本身是其API的一部分。 一些现成的过滤器定义了它们自己的常数,以帮助表示它们相对于彼此的顺序(例如, `SessionRepositoryFilter` 从春季会议有一个 `DEFAULT_ORDER` 的 `Integer.MIN_VALUE + 50`,这告诉我们它喜欢在链中处于早期,但不排除其他过滤器排在它之前。 Spring Security作为单个安装 `Filter` 在链中,其具体类型为 `FilterChainProxy`,我们很快就会讲到。 在Spring Boot应用程序中,安全过滤器是 `@Bean` 在里面 `ApplicationContext`,并且默认情况下已安装,因此它适用于每个请求。 它安装在由以下位置定义的位置 `SecurityProperties.DEFAULT_FILTER_ORDER`,而后者又由 `FilterRegistrationBean.REQUEST_WRAPPER_FILTER_MAX_ORDER`(Spring Boot应用程序期望过滤器包装请求并修改其行为的最大顺序)。 但是,还有更多的功能:从容器的角度来看,Spring Security是单个过滤器,但是在内部,还有其他过滤器,每个过滤器都扮演着特殊的角色。 下图显示了这种关系: ![弹簧安全过滤器](https://github.com/spring-guides/top-spring-security-architecture/raw/master/images/security-filters.png) 图2. Spring Security是一个单一的物理 `Filter` 但将处理委托给一系列内部过滤器 实际上,安全性过滤器中甚至还有一层间接层:通常将它作为容器安装在容器中。 `DelegatingFilterProxy`,不一定是Spring `@Bean`。 代理人委托给 `FilterChainProxy`,这始终是 `@Bean`,通常使用固定名称 `springSecurityFilterChain`。 它是 `FilterChainProxy`包含所有安全逻辑,这些安全逻辑在内部排列为一个或多个过滤器链。 所有过滤器都具有相同的API(它们都实现了 `Filter` Servlet规范中的接口),它们都有机会否决该链的其余部分。 在同一顶层可以有多个由Spring Security管理的过滤器链 `FilterChainProxy`容器不知道所有这些。 Spring Security过滤器包含一个过滤器链列表,并向与其匹配的第一个链调度一个请求。 下图显示了基于匹配请求路径的调度( `/foo/**` 之前有比赛 `/**`)。 这是很常见的,但不是匹配请求的唯一方法。 此调度过程的最重要特征是,只有一个链可以处理请求。 ![安全过滤器派遣](https://github.com/spring-guides/top-spring-security-architecture/raw/master/images/security-filters-dispatch.png) 图3. Spring安全性 `FilterChainProxy` 将请求分派到匹配的第一个链。 没有自定义安全配置的普通Spring Boot应用程序具有多个(称为n)过滤器链,其中通常n = 6。 前(n-1)个链仅用于忽略静态资源模式,例如 `/css/**` 和 `/images/**`,以及错误视图: `/error`。 (路径可以由用户使用 `security.ignored` 来自 `SecurityProperties` 配置Bean。)最后一个链匹配所有路径( `/**`),并且更加活跃,其中包含用于身份验证,授权,异常处理,会话处理,标头写入等的逻辑。 默认情况下,该链中总共有11个过滤器,但通常用户不必担心使用哪种过滤器以及何时使用。 笔记容器不知道Spring Security内部的所有过滤器这一事实非常重要,尤其是在Spring Boot应用程序中,默认情况下,在该应用程序中,所有 @Beans 类型的 Filter已自动向容器注册。 因此,如果您想向安全链添加自定义过滤器,则无需将其设为 @Bean 或包裹在一个 FilterRegistrationBean 明确禁用容器注册。 ### 创建和自定义过滤器链 Spring Boot应用程序中的默认后备过滤器链(带有 `/**` 请求匹配器)的预定义顺序为 `SecurityProperties.BASIC_AUTH_ORDER`。 您可以通过设置将其完全关闭 `security.basic.enabled=false`,也可以将其用作后备并以较低的顺序定义其他规则。 要执行后者,请添加一个 `@Bean` 类型的 `WebSecurityConfigurerAdapter` (或者 `WebSecurityConfigurer`)并用 `@Order`, 如下: ~~~java @Configuration @Order(SecurityProperties.BASIC_AUTH_ORDER - 10) public class ApplicationConfigurerAdapter extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.antMatcher("/match1/**") ...; } } ~~~ 这个bean使Spring Security添加一个新的过滤器链并在回退之前对其进行排序。 与另一套资源相比,许多应用程序对一套资源的访问规则完全不同。 例如,承载UI和支持API的应用程序可能支持基于cookie的身份验证以及对UI部件的登录页面的重定向,以及基于令牌的身份验证以及对API部件的未经身份验证的请求的401响应。 每套资源都有自己的资源 `WebSecurityConfigurerAdapter`具有唯一的订单和自己的请求匹配器。 如果匹配规则重叠,则最早的有序过滤器链将获胜。 ### 请求匹配以进行派遣和授权 安全过滤器链(或等效地, `WebSecurityConfigurerAdapter`)具有一个请求匹配器,该请求匹配器用于决定是否将其应用于HTTP请求。 一旦决定应用特定的过滤器链,就不再应用其他过滤器链。 但是,在过滤器链中,您可以通过在过滤器链中设置其他匹配器来对授权进行更细粒度的控制。 `HttpSecurity` 配置器,如下所示: ~~~java @Configuration @Order(SecurityProperties.BASIC_AUTH_ORDER - 10) public class ApplicationConfigurerAdapter extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.antMatcher("/match1/**") .authorizeRequests() .antMatchers("/match1/user").hasRole("USER") .antMatchers("/match1/spam").hasRole("SPAM") .anyRequest().isAuthenticated(); } } ~~~ 配置Spring Security时最容易犯的错误之一就是忘记了这些匹配器适用于不同的进程。 一个是整个过滤器链的请求匹配器,另一个只是选择要应用的访问规则。 ### 将应用程序安全规则与执行器规则相结合 如果将Spring Boot Actuator用于管理端点,则可能希望它们是安全的,并且默认情况下它们是安全的。 实际上,将执行器添加到安全应用程序后,您将获得仅适用于执行器端点的附加过滤器链。 它由仅匹配执行器端点的请求匹配器定义,并且其顺序为 `ManagementServerProperties.BASIC_AUTH_ORDER`,比默认值少5 `SecurityProperties` 回退过滤器,因此在回退之前请先咨询该过滤器。 如果您希望将应用程序安全规则应用于执行器端点,则可以添加一个过滤器链,该过滤器链的订购要早于执行器一,并且其请求匹配器包括所有执行器端点。 如果您希望执行器端点使用默认的安全性设置,最简单的方法是在执行器端点之后,但在回退时刻之前添加自己的过滤器(例如, `ManagementServerProperties.BASIC_AUTH_ORDER + 1`), 如下: ~~~java @Configuration @Order(ManagementServerProperties.BASIC_AUTH_ORDER + 1) public class ApplicationConfigurerAdapter extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.antMatcher("/foo/**") ...; } } ~~~ 笔记Web层中的Spring Security当前与Servlet API绑定在一起,因此,它仅在以嵌入式或其他方式在Servlet容器中运行应用程序时才真正适用。 但是,它不依赖于Spring MVC或Spring Web堆栈的其余部分,因此可以在任何servlet应用程序中使用,例如,使用JAX-RS的servlet应用程序。 ## 方法安全性 除了支持Web应用程序安全之外,Spring Security还提供了将访问规则应用于Java方法执行的支持。 对于Spring Security,这只是另一种类型的“受保护资源”。 对于用户,这意味着使用相同的格式声明访问规则 `ConfigAttribute`字符串(例如,角色或表达式),但在代码中的其他位置。 第一步是启用方法安全性-例如,在我们应用程序的顶级配置中: ~~~java @SpringBootApplication @EnableGlobalMethodSecurity(securedEnabled = true) public class SampleSecureApplication { } ~~~ 然后,我们可以直接装饰方法资源: ~~~java @Service public class MyService { @Secured("ROLE_USER") public String secure() { return "Hello Security"; } } ~~~ 此示例是具有安全方法的服务。 如果Spring创建一个 `@Bean`这种类型的代理将被代理,并且在方法实际执行之前,调用者必须经过安全拦截器。 如果访问被拒绝,则呼叫者会得到一个 `AccessDeniedException` 而不是实际的方法结果。 您还可以在方法上使用其他注释来强制执行安全性约束,特别是 `@PreAuthorize` 和 `@PostAuthorize`,可让您编写分别包含对方法参数和返回值的引用的表达式。 提示结合使用Web安全性和方法安全性并不少见。 筛选器链提供了用户体验功能,例如身份验证和重定向到登录页面等,并且方法安全性在更精细的级别上提供了保护。 ## 使用线程 Spring Security从根本上讲是线程绑定的,因为它需要使当前经过身份验证的主体可供各种下游使用者使用。 基本的构建块是 `SecurityContext`,其中可能包含一个 `Authentication` (当用户登录时, `Authentication` 那是明确的 `authenticated`)。 您可以随时访问和操作 `SecurityContext` 通过静态便利方法 `SecurityContextHolder`,进而操纵 `ThreadLocal`。 以下示例显示了这种安排: ~~~java SecurityContext context = SecurityContextHolder.getContext(); Authentication authentication = context.getAuthentication(); assert(authentication.isAuthenticated); ~~~ 这是 **不是** 对用户应用程序代码来执行这个共同的,但它可以是有用的,如果你,比如,需要写一个自定义的验证过滤器(虽然,即使如此,也有Spring Security的基类,您可以使用,让你可以避免使用 `SecurityContextHolder`). 如果您需要访问Web端点中当前经过身份验证的用户,则可以在 `@RequestMapping`, 如下: ~~~java @RequestMapping("/foo") public String foo(@AuthenticationPrincipal User user) { ... // do stuff with user } ~~~ 该注释拉动当前 `Authentication` 出于 `SecurityContext` 并致电 `getPrincipal()`在其上生成method参数的method。 的类型 `Principal` 在一个 `Authentication` 取决于 `AuthenticationManager` 用于验证身份验证,因此这对于获得对用户数据的类型安全的引用可能是一个有用的小技巧。 如果使用Spring Security,则 `Principal` 来自 `HttpServletRequest` 是类型 `Authentication`,因此您也可以直接使用它: ~~~java @RequestMapping("/foo") public String foo(Principal principal) { Authentication authentication = (Authentication) principal; User = (User) authentication.getPrincipal(); ... // do stuff with user } ~~~ 如果您需要编写在不使用Spring Security时可以工作的代码,这有时会很有用(您需要在加载代码时更加防御 `Authentication` 班级)。 ### 异步处理安全方法 自从 `SecurityContext` 如果您要进行任何调用安全方法的后台处理(例如,使用 `@Async`),则需要确保传播上下文。 这归结为包装 `SecurityContext` 与任务( `Runnable`, `Callable`,依此类推)在后台执行。 Spring Security提供了一些帮助程序来简化此工作,例如包装 `Runnable` 和 `Callable`。 传播 `SecurityContext` 到 `@Async` 方法,您需要提供一个 `AsyncConfigurer` 并确保 `Executor` 是正确的类型: ~~~java @Configuration public class ApplicationConfiguration extends AsyncConfigurerSupport { @Override public Executor getAsyncExecutor() { return new DelegatingSecurityContextExecutorService(Executors.newFixedThreadPool(5)); } } ~~~