做者:before31
https://my.oschina.net/xuezi/...
本指南是Spring Security的入门,它提供了对该框架的设计和基本构建的看法。咱们仅介绍了应用程序安全性的最基本知识,可是这样作能够解除使用Spring Security的开发人员所遇到的一些困惑。为此,咱们会看一下使用过滤器(更一般是使用方法注释)在Web应用程序中应用安全性的方式。当你须要从高层次了解安全应用程序的工做方式,如何自定义它,或者仅须要学习如何考虑应用程序安全性时,请使用本指南。css
本指南并非解决最基本问题的手册(对于基本问题,有不少其余可参考的资料),但对于初学者和专家均可能有用。 Spring Boot也被说起了不少次,由于它为安全的应用程序提供了一些默认行为,而且理解它与整个体系结构之间的关系会对你颇有帮助。全部这些原则一样适用于不使用Spring Boot的应用程序。web
应用程序安全性差很少能够归结为两个独立的问题:身份认证(你是谁)和受权(authorization)(你能够作什么?)。 有时人们会说“访问控制”而不是“受权”,这可能会形成困惑,可是以这种方式思考可能会有所帮助,由于“受权”在其余地方又有其余含义。 Spring Security的体系结构旨在将身份认证与受权分开,而且具备许多策略和扩展点。spring
认证的主要策略接口是AuthenticationManager
,该接口只有一个方法:segmentfault
public interface AuthenticationManager { Authentication authenticate(Authentication authentication) throws AuthenticationException; }
在AuthenticationManager接口的authenticate()方法中能够有3种处理状况:后端
AuthenticationException
是运行时异常。它一般由应用程序以通用方式处理,具体取决于应用程序的风格或目的。 换句话说,一般不但愿用户代码捕获并处理它。 例如,一个Web UI将呈现一个页面,该页面说明身份验证失败,然后端HTTP服务将发送401响应,是否携带WWW-Authenticate标头则取决于上下文。api
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链,ProviderManager能够在同一应用程序中支持多种不一样的身份验证机制。 若是ProviderManager没法识别特定的身份验证明例类型,则将跳过该类型。cookie
ProviderManager具备可选的父级,若是全部提供程序都返回null,则能够咨询该父级。 若是父级不可用,则空的Authentication将致使AuthenticationException。架构
有时,应用程序具备逻辑组的受保护资源(例如,与路径模式/api/**
匹配的全部Web资源),而且每一个组能够具备本身的专用AuthenticationManager。 一般,每个都是ProviderManager,它们共享一个父级。 所以,父级是一种“全局”资源,充当全部providers的后备。app
使用ProviderManager的AuthenticationManager层次结构
Spring Security提供了一些配置助手,能够快速获取在应用程序中设置的通用Authentication Managers功能。 最经常使用的帮助程序是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应用程序安全性的详细信息,请参见下文)。注意,AuthenticationManagerBuilder是@Autowired
到@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),那么AuthenticationManagerBuilder仅用于构建“本地” AuthenticationManager,它是全局管理器的子级。 在Spring Boot应用程序中,你可使用@Autowired将全局的管理器注入到另外一个bean,可是除非你本身显式公开“本地”管理器,不然不能对本地管理器执行此操做。
Spring Boot提供了一个默认的全局AuthenticationManager(只有一个用户),除非你经过提供本身的AuthenticationManager类型的bean来抢占它。 除非你主动须要自定义全局AuthenticationManager,不然默认值自己就足够安全,你没必要担忧太多。 若是执行任何构建AuthenticationManager的配置,则一般能够在“本地”对要保护的资源进行配置,而不要去关心全局的默认值。
身份认证成功之后,咱们接下来讨论受权,这里的核心策略是AccessDecisionManager。 该框架提供了三种实现,全部这三种实现都委托给AccessDecisionVoter链,有点像ProviderManager委托给AuthenticationProviders。
AccessDecisionVoter会考虑Authentication(表示一个主体)和被ConfigAttributes修饰的安全Object:
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的,即任何选民(voter)返回容许,则将授予访问权限。 任何定制都倾向于在选民中发生,要么增长新选民,要么修改现有选民的工做方式。
使用SpEL(Spring表达式语言)表达式的ConfigAttribute很是常见,例如isFullyAuthenticated()&& hasRole('FOO')。 AccessDecisionVoter支持此功能,能够处理表达式并为其建立上下文。 为了扩展能够处理的表达式的范围,须要SecurityExpressionRoot的自定义实现,有时还须要实现SecurityExpressionHandler。
Web层(用于UI和HTTP后端)中的Spring Security基于Servlet过滤器,所以一般首先了解过滤器的做用会颇有帮助。 下图显示了单个HTTP请求的处理程序的典型分层。
客户端向应用程序发送请求,而后容器根据请求URI的路径肯定对它应用哪些过滤器和哪一个servlet。一个servlet最多只能处理一个请求,可是过滤器造成一个链,所以它们是有序的,实际上,若是某个过滤器要本身处理该请求,则其能够否决链的其他部分。过滤器还能够修改下游过滤器和Servlet中使用的请求和/或响应。过滤器链的顺序很是重要,Spring Boot经过两种机制对其进行管理:一种是Filter类型的@Bean能够具备@Order或实现Ordered,另外一种是它们能够成为FilterRegistrationBean自己的一部分,该Bean自己就维持了顺序。一些现成的过滤器定义了本身的常量,以帮助代表它们相对彼此的顺序(例如,Spring Session中的SessionRepositoryFilter拥有的DEFAULT_ORDER值为Integer.MIN_VALUE + 50,这告诉咱们此过滤器须要在过滤器链中比较靠前,但也并不阻止其余过滤器更靠前一些)。
Spring Security是做为链中的单个Filter安装的,其隐秘类型为FilterChainProxy,缘由很快就会变得显而易见。 在Spring Boot应用程序中,安全过滤器是ApplicationContext中的@Bean,默认状况下会安装它,以便将其应用于每一个请求。 它安装在SecurityProperties.DEFAULT_FILTER_ORDER定义的位置,该位置又由FilterRegistrationBean.REQUEST_WRAPPER_FILTER_MAX_ORDER锚定(Spring Boot应用程序但愿过滤器包装请求并修改其行为时指望的最大顺序)。 可是,还有更多要说的:从容器的角度来看,Spring Security是一个过滤器,可是在内部有其余过滤器,每一个过滤器都扮演着特殊的角色。 这是一张图:
Spring Security是单一Filter,但将处理逻辑委托给一系列内部过滤器
实际上,安全过滤器中甚至还有一层间接层:它一般做为DelegatingFilterProxy安装在容器中,而没必要是Spring @Bean。代理委托给一个始终为@Bean的FilterChainProxy,一般使用固定名称springSecurityFilterChain。它是FilterChainProxy,它包含全部内部安全逻辑,这些安全逻辑在内部排列为一个或多个过滤器链。全部过滤器都具备相同的API(它们都实现了Servlet规范中的Filter接口),而且它们都有机会否决该链的其他部分。
在同一顶级FilterChainProxy中能够有多个由Spring Security管理的过滤器链,而对于容器来讲都是未知的。 Spring Security过滤器包含一个过滤器链列表,并向与其匹配的第一个链调度(dispatch)一个请求。下图显示了基于匹配请求路径(/foo/*在/*以前匹配)进行的调度。这是很常见的,但不是匹配请求的惟一方法。此调度过程的最重要特征是,只有一个链能够处理请求。
Spring SecurityFilterChainProxy将请求调度到匹配的第一个链
一个Spring Boot应用程序,若是没有自定义安全配置,会拥有多个(称为n)过滤器链,一般n = 6。 前(n-1)个链仅用于忽略静态资源,例如/css/和/images/,以及错误视图/error(路径能够由SecurityProperties配置bean中的security.ignored控制)。 最后一个链与全部路径/**匹配,而且更活跃,包含用于身份认证、受权、异常处理、会话处理、标头写入等逻辑。默认状况下,该链中共有11个过滤器,但一般状况下用户没必要关心使用了哪一个过滤器以及什么时候使用。
注意:容器不知道Spring Security内部的全部过滤器这一事实很重要,尤为是在Spring Boot应用程序中,默认状况下,全部Filter类型的@Beans都会自动向容器注册。 所以,若是要向安全链中添加自定义过滤器,请不要使其成为@Bean,或者干脆将其包装在FilterRegistrationBean中,在该Bean中显式禁用了容器注册的。
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("/foo/**") ...; } }
这个bean将使Spring Security添加一个新的过滤器链并将其放置在后背过滤器链以前。
许多应用程序对一组资源的访问规则可能彻底不一样于另一组。 例如,对于一个承载了UI而且也支持API的应用程序,可能其UI部分须要支持基于cookie(cookid-based)的身份认证以及对登陆页面的重定向,而其API部分则须要支持基于令牌(token-based)的身份认证以及对未经身份认证的请求的401响应。 每组资源都有其本身的WebSecurityConfigurerAdapter以及惟一的顺序和本身的请求匹配器。 若是匹配规则重叠,则最先的有序过滤器链将获胜。
安全过滤器链(或等效的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("/foo/**") .authorizeRequests() .antMatchers("/foo/bar").hasRole("BAR") .antMatchers("/foo/spam").hasRole("SPAM") .anyRequest().isAuthenticated(); } }
配置Spring Security时最容易犯的一个错误是忘记这些匹配器适用于不一样的流程,一个是整个过滤器链的请求匹配器,另外一个是仅选择要应用的访问规则。
若是你使用了Spring Boot执行器(Actuator),则可能但愿它的端点(endpoint)是安全的,默认状况下确实将是安全的。 实际上,将执行器添加到安全应用程序后,你会得到一条仅适用于执行器端点的附加过滤器链。 它由仅匹配执行器端点的请求匹配器来定义,而且其顺序为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的servlet应用程序。
除了支持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,则它将被代理,而且在实际执行该方法以前,调用者必须通过安全拦截器。 若是访问被拒绝,则调用者将获得AccessDeniedException异常,而不是实际的方法结果。
方法上还可使用其余注解来强制执行安全性约束,特别是@PreAuthorize和@PostAuthorize,它们可使你编写分别包含对方法参数和返回值的引用的表达式。
小贴士:结合使用Web安全性和方法安全性并很多见。 过滤器链提供了用户体验功能,例如身份认证和重定向到登陆页面等,而方法安全性在更精细的级别上提供了保护。
Spring Security从根本上讲是线程绑定的,由于它须要使当前通过身份验证的主体可供各类下游使用者使用。 基本构件是SecurityContext,它能够包含一个Authentication(当用户登陆成功之后,它将被显式注明为authenticated)。 你始终能够经过SecurityContextHolder中的静态方法访问和操做SecurityContext,而该方法里面实际上是简单地操做TheadLocal,例如
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 }
该注解将当前的Authentication从SecurityContext中取出,并调用其getPrincipal()方法以产生方法参数。 Authentication中的主体(Principal)的类型取决于用于身份认证的AuthenticationManager,所以这也是一个得到对用户数据的类型安全引用的有用的小技巧。
若是使用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类时更具防护性)。
因为SecurityContext是线程绑定的,所以,若是要执行任何调用安全方法的后台处理,例如使用@Async,你须要确保传播该SecurityContext。 简单归结起来就是,要将SecurityContext与在后台执行的任务(Runnable,Callable等)包装在一块儿。 Spring Security提供了一些帮助程序,例如Runnable和Callable的包装器,可使上述操做变得简单。 要将SecurityContext传播到@Async方法,你须要提供AsyncConfigurer并确保Executor具备正确的类型:
@Configuration public class ApplicationConfiguration extends AsyncConfigurerSupport { @Override public Executor getAsyncExecutor() { return new DelegatingSecurityContextExecutorService(Executors.newFixedThreadPool(5)); } }
关注公众号《架构文摘》,天天一篇架构领域重磅好文,涉及一线互联网公司应用架构(高可用、高性能、高稳定)、大数据、机器学习、Java架构等各个热门领域。