本指南是 Spring Security 的入门,提供对框架设计和基本构建块的深入了解。我们只涵盖应用程序安全的基础知识。但是,通过这样做,我们可以消除使用 Spring Security 的开发人员所遇到的一些困惑。为此,我们通过使用过滤器,更一般地说,通过使用方法注释来查看在 Web 应用程序中应用安全性的方式。当您需要对安全应用程序的工作原理、如何对其进行自定义,或者需要了解如何考虑应用程序安全性时,请使用本指南。
本指南并非旨在作为解决最基本问题以外的其他问题的手册或方法(还有其他来源),但它可能对初学者和专家都有用。Spring Boot 也经常被引用,因为它为安全应用程序提供了一些默认行为,并且有助于理解它如何适应整体架构。
笔记所有原则同样适用于不使用 Spring Boot 的应用程序。身份验证和访问控制
应用程序安全归结为两个或多或少独立的问题:身份验证(你是谁?)和授权(你被允许做什么?)。有时人们会说“访问控制”而不是“授权”,这可能会让人感到困惑,但这样想是有帮助的,因为“授权”在其他地方超载。Spring Security 的架构旨在将身份验证与授权分开,并为两者提供策略和扩展点。
验证
认证的主要策略接口是AuthenticationManager,它只有一种方法:
public interface AuthenticationManager { Authentication authenticate(Authentication authentication) throws AuthenticationException; }
AnAuthenticationManager可以在其authenticate()方法中执行以下三件事之一:
如果它可以验证输入代表有效的主体,则返回一个Authentication(通常带有authenticated=true)。
AuthenticationException如果它认为输入代表无效的主体,则抛出一个。
null如果不能决定就返回。
AuthenticationException是运行时异常。它通常由应用程序以通用方式处理,具体取决于应用程序的样式或目的。换句话说,用户代码通常不会捕获并处理它。例如,Web UI 可能会呈现一个表明身份验证失败的页面,后端 HTTP 服务可能会发送 401 响应,WWW-Authenticate根据上下文带有或不带有标头。
最常用的实现AuthenticationManager是ProviderManager,它委托给一个AuthenticationProvider实例链。AnAuthenticationProvider有点像 an AuthenticationManager,但它有一个额外的方法来允许调用者查询它是否支持给定Authentication类型:
public interface AuthenticationProvider { Authentication authenticate(Authentication authentication) throws AuthenticationException; boolean supports(Class authentication); }
方法中的Class参数supports()是真的Class(只询问它是否支持传递给authenticate()方法的东西)。AProviderManager可以通过委托给AuthenticationProviders. 如果 aProviderManager无法识别特定的Authentication实例类型,则会跳过它。
AProviderManager有一个可选的父级,如果所有提供者都返回,它可以咨询null。如果父对象不可用,则null Authentication结果为AuthenticationException.
有时,应用程序具有受保护资源的逻辑组(例如,匹配路径模式的所有 Web 资源,例如/api/**),并且每个组都可以有自己的专用AuthenticationManager. 通常,这些中的每一个都是一个ProviderManager,并且它们共享一个父级。父级是一种“全局”资源,充当所有提供者的后备。
图 1.AuthenticationManager使用的层次结构ProviderManager自定义身份验证管理器
Spring Security 提供了一些配置助手来快速获取在您的应用程序中设置的常见身份验证管理器功能。最常用的帮助器是AuthenticationManagerBuilder,它非常适合设置内存、JDBC 或 LDAP 用户详细信息或添加自定义UserDetailsService. 以下示例显示了配置全局(父)的应用程序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"); } }
此示例与 Web 应用程序有关,但 的用法AuthenticationManagerBuilder适用范围更广(有关如何实现 Web 应用程序安全性的更多详细信息,请参阅Web Security)。请注意,AuthenticationManagerBuilderis@Autowired进入 a 中的方法@Bean - 这就是它构建全局(父)的原因AuthenticationManager。相反,请考虑以下示例:
@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在配置器中使用了of a 方法,AuthenticationManagerBuilder则将仅用于构建一个“本地”方法AuthenticationManager,它将是全局方法的子项。在 Spring Boot 应用程序中,您可以@Autowired将全局 bean 放入另一个 bean,但除非您自己显式地公开它,否则不能对本地 bean 执行此操作。
Spring Boot 提供了一个默认的全局变量AuthenticationManager(只有一个用户),除非你通过提供你自己的类型 bean 来抢占它AuthenticationManager。默认值本身就足够安全,您不必担心太多,除非您主动需要自定义全局AuthenticationManager. 如果您执行任何构建AuthenticationManager.
授权或访问控制
一旦认证成功,我们就可以进行授权,这里的核心策略是AccessDecisionManager。有框架提供三种实现与所有三个委托链的AccessDecisionVoter情况下,有点像ProviderManager与会代表AuthenticationProviders。
AnAccessDecisionVoter考虑一个Authentication(代表一个主体)和一个安全的Object,它已经被装饰了ConfigAttributes:
boolean supports(ConfigAttribute attribute); boolean supports(Class clazz); int vote(Authentication authentication, S object, Collection attributes);
该Object是的签名完全通用的AccessDecisionManager和AccessDecisionVoter。它代表用户可能想要访问的任何内容(Web 资源或 Java 类中的方法是两种最常见的情况)。该ConfigAttributes也相当一般,较安全的装修Object用一些确定的权限级别元数据来访问它所需。ConfigAttribute是一个接口。它只有一个方法(非常通用并返回 a String),因此这些字符串以某种方式编码资源所有者的意图,表达允许谁访问它的规则。典型的ConfigAttribute是用户角色的名称(如ROLE_ADMIN或ROLE_AUDIT),它们通常具有特殊格式(如ROLE_ 前缀)或表示需要计算的表达式。
大多数人使用默认值AccessDecisionManager,即AffirmativeBased(如果任何选民肯定返回,则授予访问权限)。通过添加新的或修改现有的工作方式,任何定制都倾向于在选民中发生。
使用ConfigAttributesSpring 表达式语言 (SpEL) 表达式是很常见的——例如,isFullyAuthenticated() && hasRole('user'). 这由AccessDecisionVoter可以处理表达式并为它们创建上下文的 支持。要扩展可以处理的表达式范围,需要自定义实现 ,SecurityExpressionRoot有时还需要SecurityExpressionHandler.
网络安全
Web 层(用于 UI 和 HTTP 后端)中的 Spring Security 是基于 Servlet 的Filters,所以先看看Filters一般的作用是有帮助的。下图显示了单个 HTTP 请求的处理程序的典型分层。
客户端向应用程序发送请求,容器根据请求 URI 的路径决定对其应用哪些过滤器和哪个 servlet。最多一个 servlet 可以处理单个请求,但过滤器形成一个链,因此它们是有序的。事实上,如果过滤器想要自己处理请求,它可以否决链的其余部分。过滤器还可以修改下游过滤器和 servlet 中使用的请求或响应。过滤器链的顺序很重要,Spring Boot 通过两种机制来管理它:@Beans类型Filter可以有一个@Order或实现Ordered,它们可以是一个FilterRegistrationBean它本身有一个订单作为其 API 的一部分。一些现成的过滤器定义了它们自己的常量来帮助表明它们相对于彼此的顺序(例如,SessionRepositoryFilter来自 Spring Session 的 a DEFAULT_ORDERof Integer.MIN_VALUE + 50,它告诉我们它喜欢在链的早期,但是它不排除在它之前出现的其他过滤器)。
Spring SecurityFilter在链中作为单个安装,其具体类型为FilterChainProxy,原因我们稍后会介绍。在 Spring Boot 应用程序中,安全过滤器位于@Bean中ApplicationContext,默认情况下会安装它,以便将其应用于每个请求。它安装在由 定义的位置SecurityProperties.DEFAULT_FILTER_ORDER,而该位置又由FilterRegistrationBean.REQUEST_WRAPPER_FILTER_MAX_ORDER(Spring Boot 应用程序在包装请求、修改其行为时希望过滤器具有的最大顺序)锚定。但是,还有更多:从容器的角度来看,Spring Security 是一个单一的过滤器,但是,在它内部,还有额外的过滤器,每个过滤器都扮演着特殊的角色。下图显示了这种关系:
图 2. Spring Security 是单个物理,Filter但将处理委托给一系列内部过滤器
事实上,安全过滤器中甚至还有一层间接:它通常作为 安装在容器中DelegatingFilterProxy,它不一定是 Spring @Bean。代理委托给 a FilterChainProxy,它始终是 a @Bean,通常具有固定名称springSecurityFilterChain。它FilterChainProxy包含所有内部排列为过滤器链(或链)的安全逻辑。所有过滤器都具有相同的 API(它们都实现了FilterServlet 规范中的接口),并且它们都有机会否决链的其余部分。
可以有多个过滤器链,所有过滤器链都由同一顶层的 Spring Security 管理,FilterChainProxy并且容器都不知道所有过滤器链。Spring Security 过滤器包含一个过滤器链列表,并将请求分派到匹配它的第一个链。下图显示了基于匹配请求路径(/foo/ 匹配之前/ )发生的调度。这是很常见的,但不是匹配请求的唯一方法。这个调度过程最重要的特点是只有一个链处理一个请求。
图 3. Spring SecurityFilterChainProxy将请求分派到第一个匹配的链。
没有自定义安全配置的 vanilla Spring Boot 应用程序有多个(称为 n 个)过滤器链,其中通常 n=6。第一个 (n-1) 链只是为了忽略静态资源模式,比如/css/ 和/images/ ,以及错误视图:/error。(路径可以由用户security.ignored从SecurityProperties配置 bean 中控制。)最后一个链匹配捕获所有路径 ( /**) 并且更活跃,包含用于身份验证、授权、异常处理、会话处理、标题写入等的逻辑在。默认情况下,该链中共有 11 个过滤器,但通常用户无需关心使用哪些过滤器以及何时使用。
笔记Spring Security 内部的所有过滤器对容器来说都是未知的这一事实很重要,特别是在 Spring Boot 应用程序中,默认情况下,所有@Beans类型都会Filter自动注册到容器中。因此,如果您想向安全链添加自定义过滤器,则需要不将其设为 a@Bean或将其包装在FilterRegistrationBean显式禁用容器注册的 a 中。创建和自定义过滤器链
Spring Boot 应用程序(带有/**请求匹配器的应用程序)中的默认回退过滤器链的预定义顺序为SecurityProperties.BASIC_AUTH_ORDER. 您可以通过设置将其完全关闭security.basic.enabled=false,或者您可以将其用作后备并以较低的顺序定义其他规则。要执行后者,请添加@Bean类型WebSecurityConfigurerAdapter(或WebSecurityConfigurer)并使用 装饰类@Order,如下所示:
@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的唯一顺序和自己的请求匹配器。如果匹配规则重叠,则最早排序的过滤器链获胜。
请求匹配调度和授权
安全过滤器链(或等效的 a WebSecurityConfigurerAdapter)有一个请求匹配器,用于决定是否将其应用于 HTTP 请求。一旦决定应用特定的过滤器链,就不会再应用其他过滤器链。但是,在过滤器链中,您可以通过在HttpSecurity配置器中设置额外的匹配器来对授权进行更细粒度的控制,如下所示:
@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 用于管理端点,您可能希望它们是安全的,并且默认情况下,它们是安全的。事实上,只要您将 Actuator 添加到安全应用程序中,您就会获得一个仅适用于执行器端点的附加过滤器链。它使用仅匹配执行器端点的请求匹配器定义,它的顺序为ManagementServerProperties.BASIC_AUTH_ORDER,比默认SecurityProperties回退过滤器少 5 ,因此在回退之前进行咨询。
如果您希望您的应用程序安全规则应用于执行器端点,您可以添加一个过滤器链,该过滤器链的顺序早于执行器,并且具有包含所有执行器端点的请求匹配器。如果您更喜欢执行器端点的默认安全设置,最简单的方法是在执行器之后添加您自己的过滤器,但在回退之前(例如,ManagementServerProperties.BASIC_AUTH_ORDER + 1),如下所示:
@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 的应用程序。方法安全
除了支持保护 Web 应用程序,Spring Security 还支持将访问规则应用于 Java 方法执行。对于 Spring Security,这只是一种不同类型的“受保护资源”。对于用户来说,这意味着访问规则是使用相同格式的ConfigAttribute字符串(例如,角色或表达式)声明的,但在代码中的不同位置。第一步是启用方法安全性——例如,在我们应用程序的顶级配置中:
@SpringBootApplication @EnableGlobalMethodSecurity(securedEnabled = true) public class SampleSecureApplication { }
然后我们可以直接装饰方法资源:
@Service public class MyService { @Secured("ROLE_USER") public String secure() { return "Hello Security"; } }
此示例是具有安全方法的服务。如果 Spring 创建了@Bean这种类型的 a ,它会被代理并且调用者必须在该方法实际执行之前通过一个安全拦截器。如果访问被拒绝,调用者会得到一个AccessDeniedException而不是实际的方法结果。
您还可以在方法上使用其他注释来强制实施安全约束,特别是@PreAuthorize和@PostAuthorize,它们分别允许您编写包含对方法参数和返回值的引用的表达式。
提示将 Web 安全性和方法安全性结合起来的情况并不少见。过滤器链提供用户体验功能,例如身份验证和重定向到登录页面等,方法安全提供更细粒度的保护。使用线程
Spring Security 基本上是线程绑定的,因为它需要使当前经过身份验证的主体可用于各种下游消费者。基本构建块是SecurityContext,它可能包含一个Authentication(并且当用户登录时,它Authentication是显式的authenticated)。您始终可以SecurityContext通过 中的静态便捷方法访问和操作SecurityContextHolder,而后者又操作ThreadLocal. 以下示例显示了这种安排:
SecurityContext context = SecurityContextHolder.getContext(); Authentication authentication = context.getAuthentication(); assert(authentication.isAuthenticated);
这是 不是 对用户应用程序代码来执行这个共同的,但它可以是有用的,如果你,比如,需要写一个自定义的验证过滤器(虽然,即使如此,也有Spring Security的基类,您可以使用,让你可以避免需要使用SecurityContextHolder)。
如果您需要访问 Web 端点中当前经过身份验证的用户,您可以在 a 中使用方法参数@RequestMapping,如下所示:
@RequestMapping("/foo") public String foo(@AuthenticationPrincipal User user) { ... // do stuff with user }
这个注解拉电流Authentication出来的SecurityContext,并调用getPrincipal()它的方法来产生方法的参数。Principalin an的类型Authentication取决于AuthenticationManager用于验证身份验证的 ,因此这是获取对用户数据的类型安全引用的有用小技巧。
如果正在使用 Spring Security,则PrincipalfromHttpServletRequest是 type Authentication,因此您也可以直接使用它:
@RequestMapping("/foo") public String foo(Principal principal) { Authentication authentication = (Authentication) principal; User = (User) authentication.getPrincipal(); ... // do stuff with user }
如果您需要编写在不使用 Spring Security 时工作的代码(您需要对加载Authentication类更加防御),这有时会很有用。
异步处理安全方法
由于SecurityContext是线程绑定的,如果您想要进行任何调用安全方法的后台处理(例如, with @Async),您需要确保传播上下文。这归结为SecurityContext用在后台执行的任务(Runnable、Callable等)包装。Spring Security 提供了一些帮助程序来使这更容易,例如包装器Runnable和Callable. 要传播SecurityContextto@Async方法,您需要提供一个AsyncConfigurer并确保Executoris 的类型正确:
@Configuration public class ApplicationConfiguration extends AsyncConfigurerSupport { @Override public Executor getAsyncExecutor() { return new DelegatingSecurityContextExecutorService(Executors.newFixedThreadPool(5)); } }
内容来源:Spring中国教育管理中心