Spring Security 架构

       本指南是Spring Security的入门,它提供了对该框架的设计和基本模块的洞察。仅介绍了应用程序安全性的最基本知识,但是这样做可以清除开发人员在使用Spring Security所遇到的一些困惑。为此,来看看使用过滤器(通常使用方法注释)在Web应用程序中应用安全性的方式。当需要从高层次了解安全应用程序的工作方式,如何自定义它,或者仅需要学习如何考虑应用程序安全性时,请使用本指南。

       本指南不是解决最基本的问题(还有其他来源)的手册,但对于初学者和专家都可能有用。在本文中Spring Boot之所以被广泛引用,是因为它为安全的应用程序提供了一些默认行为,并且有助于理解它与整个体系结构之间的关系。所有这些原则同样适用于不使用Spring Boot的应用程序。

身份验证和访问控制

       应用程序安全性可以归结为两个独立问题:身份验证(你是谁)和授权(你可以做什么?)。有时人们会说“访问控制”而不是“授权”。Spring Security的体系结构旨在将身份验证与授权分开,并具有策略和扩展点。

认证方式

身份验证的主要策略接口AuthenticationManager只有一个方法:

public interface AuthenticationManager {

Authentication authenticate(Authentication authentication) throws AuthenticationException;

}

AuthenticationManager接口在它的authenticate()方法中可以执行以下其中之一种情况:

1.如果可以验证输入代表有效的主体,则返回一个Authentication(通常为authenticated=true)。

2.如果认为输入代表无效的主体,则抛出一个AuthenticationException。

3.如果无法决定,则返回null。

AuthenticationException是运行时异常。它通常由应用程序以通用方式处理,具体取决于应用程序的样式或目的。换句话说,通常不希望用户代码捕获并处理它。例如,返回一个页面显示身份验证失败,而后端HTTP服务将发送401响应,有没有WWW认证头取决于上下文。

最常用的实现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()方法中的参数)。一个ProviderManager在同一个应用程序通过委托给AuthenticationProviders程序链可支持多个不同的认证机制。如果一个ProviderManager不能识别特定的身份验证实例类型,该类型将被跳过。

一个ProviderManager具有可选的父级,它能查询是否所有providers都返回null。如果父级不可用,则空身份验证将导致AuthenticationException。有时,一个应用程序具有受保护资源的逻辑组(例如,与路径模式匹配的所有Web资源/api/**),并且每个组可以具有自己专用的AuthenticationManager。通常,每个都是一个ProviderManager,并且它们共享一个父级。因此,父级是一种“全局”资源,充当所有provider的后备。

具有公共父级的ProviderManager

自定义Authentication Managers

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应用程序安全性的详细信息,请参见下文)。请注意,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仅用于构建“ local” AuthenticationManager,它是全局变量的子级。在Spring Boot应用程序中,你可以@Autowired一个全局变量转换为另一个Bean,但是除非你显式公开它,否则不能对本地的Bean进行转换。

        Spring Boot提供了一个默认的全局AuthenticationManager(只有一个用户),可以自定义全局AuthenticationManager,但默认的足够安全,除非需要,可以不必过多考虑。如果你执行任何构建AuthenticationManager的配置,则通常可以在本地对要保护的资源执行该配置,而不必担心全局默认值。

授权或访问控制

       认证成功后,我们可以继续进行授权。这里的核心策略是AccessDecisionManager。框架提供了三种实现,所有这三种实现都委托给AccessDecisionVoter链。有点像ProviderManager委托给AuthenticationProviders。

AccessDecisionToverter考虑使用ConfigAttributes装饰的身份验证(表示主体)和安全对象:

boolean supports(ConfigAttribute attribute);

boolean supports(Class<?> clazz);

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

       对象在AccessDecisionManager和AccessDecisionVoter的签名中是完全通用的-它表示用户可能想要访问的任何内容(web资源或Java类中的方法是最常见的两种情况)。ConfigAttributes也是相当通用的,它表示带有一些元数据的安全对象的装饰,这些元数据决定了访问该对象所需的权限级别。ConfigAttribute是一个接口,但它只有一个非常通用的方法,并返回一个字符串,因此这些字符串以某种方式编码资源所有者的意图,表示允许谁访问它的规则。典型的ConfigAttribute是用户角色的名称(如ROLE_ADMIN或ROLE_AUDIT),它们通常具有特殊格式(如ROLE_前缀)或表示需要求值的表达式。

大多数人只使用默认的AccessDecisionManager,它是基于确认的(如果任何voters确认,则授予访问权)。任何自定义都会在voters中发生,要么添加新的,要么修改现有的工作方式。使用作为Spring表达式语言(SpEL)表达式的ConfigAttributes非常常见,例如isFullyAuthenticated() && hasRole('FOO')。AccessDecisionVoter支持此功能,它可以处理表达式并为表达式创建上下文。要扩展可以处理的表达式的范围,需要SecurityExpressionRoot的自定义实现,有时还需要SecurityExpressionHandler。

网络安全

Web层(用于UI和HTTP后端)中的Spring安全性是基于Servlet过滤器的,因此通常先看看过滤器的角色是有帮助的。下图显示了单个HTTP请求的处理程序的典型分层。


Filter chain delegating to a Servlet

       客户端向应用程序发送请求,然后容器根据请求URI的路径确定对它应用哪些过滤器和哪个servlet。一个servlet最多只能处理一个请求,但是过滤器形成一个链,因此它们是有序的,实际上,如果过滤器要处理请求本身,则可以否决链的其余部分。过滤器还可以修改下游过滤器和Servlet中使用的请求和/或响应。过滤器链的顺序非常重要,Spring Boot通过两种机制对其进行管理:一种是@Beans类型Filter可以具有@Order或实现Ordered,另一种是它们可以是FilterRegistrationBean的一部分,FilterRegistrationBean本身有一个作为其API一部分的命令。一些现成的过滤器定义了自己的常量,以帮助显示它们的相对顺序(例如.Spring会话中的SessionRepositoryFilter有一个 Integer.MIN_VALUE + 50的DEFAULT_ORDER,这告诉我们,它在链的早期,但不排除其他过滤器之前)。Spring Security作为一个单一过滤器安装在链条上,其具体类型是FilterChainProxy,原因很快就会显现出来。在Spring Boot应用程序中,安全过滤器是ApplicationContext中的一个@Bean,默认情况下会安装它,以便将其应用于每个请求。它安装在SecurityProperties.DEFAULT_FILTER_ORDER所定义的位置,该位置又由FilterRegistrationBean.REQUEST_WRAPPER_FILTER_MAX_ORDER锚定(Spring Boot应用程序在包装请求并修改其行为时希望筛选器具有的最大顺序)。不过,还有更多的内容:从容器的角度来看,Spring Security是一个单一的过滤器,但是在它内部有额外的过滤器,每个过滤器都扮演着特殊的角色。这是一张图:


Spring Security Filter

       Spring Security是一个单独的物理过滤器,但是将处理委托给一系列内部过滤器.实际上,在安全过滤器中还有一个间接层:它通常作为DelegatingFilterProxy安装在容器中,而不必是Spring@Bean。代理委托给FilterChainProxy,它始终是一个@Bean,通常固定名称为springSecurityFilterChain。它是FilterChainProxy,它包含作为过滤器链(或链)在内部排列的所有安全逻辑。所有的过滤器都有相同的API(它们都实现了来自Servlet规范的过滤器接口),并且它们都有机会否决链的其余部分。

        这里可以有多个过滤器链,所有这些都由Spring Security在同一顶级FilterChainProxy中管理,并且容器未知。Spring Security过滤器包含一个过滤器链列表,并将请求发送到与之匹配的第一个链。下图显示了基于匹配请求路径的分派(/foo/**matches before/**)。这很常见,但不是匹配请求的唯一方法。这个分派过程最重要的特性是只有一个链处理一个请求。


Security Filter Dispatch

图3。Spring Security FilterChainProxy将请求发送到匹配的第一个链。

        没有自定义安全配置的普通Spring启动应用程序有几个(称为n)过滤器链,通常n=6。第一个(n-1)链只是用来忽略静态资源模式,如/css/**和/images/**,以及错误视图/错误(路径可以由用户通过安全性。忽略从SecurityProperties配置bean)。最后一个链与catch all path/**匹配,并且更为活跃,包含用于身份验证、授权、异常处理、会话处理、头写入等的逻辑。默认情况下,此链中总共有11个筛选器,但通常用户不必关心使用的筛选器和使用的时间。

对于容器来说,Spring安全性内部的所有过滤器都是未知的,这一点很重要,特别是在Spring引导应用程序中,默认情况下,所有类型为Filter的@bean都会自动注册到容器中。因此,如果要将自定义筛选器添加到安全链中,则不需要将其设为@Bean,也不需要将其包装在显式禁用容器注册的FilterRegistrationBean中。

创建和定制过滤器链

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的应用程序可能支持基于cookie的身份验证,并重定向到UI部分的登录页,以及基于令牌的身份验证,并对API部分的未经身份验证的请求作出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执行器用于管理端点,您可能希望它们是安全的,并且默认情况下它们是安全的。事实上,只要将执行器添加到安全应用程序中,就会得到一个只应用于执行器端点的附加过滤器链。它由一个只匹配执行器端点的请求匹配器定义,其顺序为ManagementServerProperties.BASIC验证顺序它比默认的SecurityProperties回退筛选器少5个,因此在回退之前会先查询它。

如果希望应用程序安全规则应用于执行器终结点,可以添加一个比执行器早排序的筛选器链,并使用包含所有执行器终结点的请求匹配器。如果您更喜欢执行器端点的默认安全设置,那么最简单的事情是在执行器端点之后添加自己的过滤器,但要早于回退(例如. ManagementServerProperties.BASIC验证顺序+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安全来说,这只是一种不同类型的“受保护资源”。对于用户,这意味着访问规则是使用相同格式的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,它可能包含一个身份验证(当用户登录时,它将是一个经过显式身份验证的身份验证)。您始终可以通过SecurityContextHolder中的静态便利方法访问和操作SecurityContext,而SecurityContextHolder中的静态便利方法又只是操作一个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

}

此批注从SecurityContext中提取当前身份验证,并对其调用getPrincipal()方法以生成方法参数。身份验证中主体的类型取决于用于验证身份验证的AuthenticationManager,因此这是一个有用的小技巧,可用于获取对用户数据的类型安全引用。

如果使用的是Spring Security,HttpServletRequest中的主体将是身份验证类型,因此您也可以直接使用它:

@RequestMapping("/foo")

public String foo(Principal principal) {

Authentication authentication = (Authentication) principal; User = (User) authentication.getPrincipal();

... // do stuff with user

}

如果需要编写在不使用Spring Security的情况下可以工作的代码(在加载身份验证类时需要更加防御性),这有时会很有用。

异步处理安全方法

由于SecurityContext是线程绑定的,如果要执行调用安全方法的任何后台处理,例如使用@Async,则需要确保传播上下文。这可以归结为用在后台执行的任务(Runnable、Callable等)包装SecurityContext。Spring Security提供了一些帮助程序来简化这个过程,比如用于Runnable和Callable的包装器。要将SecurityContext传播到@Async方法,需要提供AsyncConfigurer并确保执行器的类型正确:

@Configuration

public class ApplicationConfiguration extends AsyncConfigurerSupport {

@Override

public Executor getAsyncExecutor() {

return new DelegatingSecurityContextExecutorService(Executors.newFixedThreadPool(5));

}

}

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