原文css
也能够在个人博客上查看前端
虽然这篇文章也有其余的翻译,可是看起来像是机翻的,因此我本身翻译了一下,而且加上本身的理解。若是有理解错误的地方或者错别字,还望指出。java
本文是 Spring Security 的入门指南,深刻解析了 Spring Security 框架的设计和基础模块。咱们仅涉及程序安全性的基础知识,可是这能够帮助使用 Spring Security 的开发者解开一些疑惑。为此,咱们如何使用 filter 和方法注解来实践 web 应用的安全。若是你想在更高层次上理解如何保障应用安全性,或者想要定制应用安全,又或者你只是想了解设计应用安全的思路,那么本指南就很适合你。web
本指南不是解决最基本问题以外的用户手册(这样文章已经有不少了),可是本文对初学者和高手都有必定的帮助。Spring Boot 在本文中常常被说起,由于它为 Spring Security 提供了一些默认配置而且这有助于理解 Spring Security 是如何适应整个架构的。而全部这些原则对不使用 Spring Boot 的应用一样适用。spring
应用安全总结起来就是两大问题:认证(authentication,你是谁?)和受权(authorization,容许你作什么?)有时候也会用访问控制(access control)这个名词来代替受权,这会让咱们一些困惑,但以这种方式思考可能会有帮助:“受权”在其余地方已经实现了。Spring Security 的架构旨在将认证从受权中分离出来,并也有适用于二者的策略和可扩展的设计。后端
用于认证的主要接口是AuthenticationManager
,它只有一个方法:api
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
复制代码
一个 AuthenticationManager
的authenticate()
方法中有三种状况:安全
Authentication
(authenticated=true
),若是验证输入是合法的Principal
)。AuthenticationException
异常,若是输入不合法。null
。AuthenticationException
是一个运行时异常,一般被应用程序以常规的方式的处理,这取决于应用的母的和代码风格。换句话说,代码中通常不会捕捉和处理这个异常。好比,可使得网页显示认证失败,后端返回 401 HTTP 状态码,响应头中的WWW-Authenticate
有无视状况而定。cookie
AuthenticationManager
最广泛的实现是ProviderManager
,ProviderManager
将认证委托给一系列的AuthenticationProvider
实例 。AuthenticationProvider
和 AuthenticationManager
很相似,可是它有一个额外的方法容许查询它支持的Authentication
方式:session
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
boolean supports(Class<?> authentication);
}
复制代码
supports
方法的Class<?> authentication
参数实际上是Class<? extends Authentication>
类型的。一个ProviderManager
在一个应用中能支持多种不一样的认证机制,经过将认证委托给一系列的AuthenticationProvider
。ProviderManager
没有识别出的认证类型,将会被忽略。
每一个ProviderManager
能够有一个父类,若是全部AuthenticationProvider
都返回null
,那么就交给父类去认证。若是父类也不可用,则抛出AuthenticationException
异常。
有时应用的资源会有逻辑分组(好比全部网站资源都匹配URL/api/**
),而且每一个组都有本身的AuthenticationManager
,一般是一个ProviderManager
,它们之间有共同的父类认证器。那么父类就是一种全局资源,充当全部认证器的 fallback。
图1 ProviderManager 的继承关系
AuthenticationManager
Spring Security 提供了一些配置方式帮助你快速的配置通用的AuthenticationManager
。最多见的是AuthenticationManagerBuilder
,它可使用内存方式(in-memory)、JDBC 或 LDAP、或自定义的UserDetailService
来认证用户。下面是设置全局认证器的例子:
@Configuration
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {
... // web stuff here
@Autowired
public initialize(AuthenticationManagerBuilder builder, DataSource dataSource) {
builder.jdbcAuthentication().dataSource(dataSource).withUser("dave")
.password("secret").roles("USER");
}
}
复制代码
虽然这个例子仅仅设计一个 web 应用,可是AuthenticationManagerBuilder
的用处大为广阔(详细状况请看[Web 安全](#Web 安全)是如何实现的)。请注意AuthenticationManagerBuilder
是经过@AutoWired
注入到被@Bean
注解的一个方法中的,这使得它成为一个全局AuthenticationManager
。相反的,若是咱们这样写:
@Configuration
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {
@Autowired
DataSource dataSource;
... // web stuff here
@Override
public configure(AuthenticationManagerBuilder builder) {
builder.jdbcAuthentication().dataSource(dataSource).withUser("dave")
.password("secret").roles("USER");
}
}
复制代码
重写configure(AuthenticationManagerBuilder builder)
方法,那么AuthenticationManagerBuilder
仅会构造一个“本地”的AuthenticationManager
,只是全局认证器的一个子实现。在 Spring Boot 应用中你可使用@Autowired
注入全局的AuthenticationManager
,可是你不能注入“本地”的,除非你本身公开暴露它。
Spring Boot 提供默认的全局AuthenticationManager
,除非你提供本身的全局AuthenticationManager
。不用担忧,默认的已经足够安全了,除非你真的须要一个自定义的全局AuthenticationManager
。通常的,你只需只用“本地”的AuthenticationManagerBuilder
来配置,而不须要担忧全局的。
一旦认证成功,咱们就能够进行受权了,它核心的策略就是AccessDecisionManager
。它提供三个方法而且所有委托给AccessDecisionVoter
,这有点像ProviderManager
将认证委托给AuthenticationProvider
。
一个AccessDecisionVoter
考虑一个Authentication
(表明一个Principal
)和一个被ConfigAttributes
装饰的安全对象:
boolean supports(ConfigAttribute attribute);
boolean supports(Class<?> clazz);
int vote(Authentication authentication, S object, Collection<ConfigAttribute> attributes);
复制代码
AccessDecisionVoter
和AccessDecisionManager
方法中的object
参数是彻底泛型化的,它表明任何用户想要访问(web 资源或 Java 方法是最多见的两种状况)。ConfigAttributes
也是至关泛型化的,它表示一个被装饰的安全对象并带有访问权限级别的元数据。ConfigAttributes
是一个接口,仅有一个返回String
的方法,返回的字符串中包含资源全部者,解释了访问资源的规则。常见的ConfigAttributes
是用户的角色(好比ROLE_ADMIN
和ROLE_AUDIT
),它们一般有必定的格式(好比以ROLE_
做为前缀)或者是可计算的表达式。
大部分人使用默认的AccessDecisionManager
,即AffirmativeBased
(若是没有 voters 返回那么该访问将被受权)。任何自定义的行为最好放在 voter 中,不乱世添加一个新的 voter 仍是修改已有的 voter。
使用 Spring Expression Language(SpEL)表达式的ConfigAttributes
是很常见的,好比isFullyAuthenticated() && hasRole('FOO')
。解析表达式和加载表达式由AccessDecisionVoter
实现。要扩展可处理的表达式的范围,须要自定义SecurityExpressionRoot
,有时候也须要SecurityExpressionHandler
。
Web 层中的 Spring Security 基于 Servlet 的Filter
。因此先来看下Filter
在 web 安全中所扮演的角色。下图展现了处理单个 HTTP 请求的经典分层结构。
客户端向应用发送请求,而后容器根据 URI 来决定哪一个 filter(过滤器) 和哪一个 Servlet 适用于它。一个 servlet 最多处理一个请求,过滤器是链式的,它们是有顺序的。事实上一个过滤器能够否决接下来的过滤器,若是它想独自处理这个请求的话。一个过滤器也能够对下流的过滤器和 servlet 修改响应和请求。因此过滤器的顺序十分重要,Spring Boot 提供管理过滤器的两种机制:一个是被@Bean
注解的Filter
能够用@Order
注解或实现Ordered
接口;另外一个是过滤器是FilterRegistrationBean
的一部分,它自己就有一个顺序。一些现有的过滤器定义了本身的常量来表示顺序,以帮助代表他们相对于彼此的顺序(好比 Spring Session 中的SessionRepositoryFilter
的DEFAULT_ORDER
的值为Integer.MIN_VALUE + 50
,它表示这个过滤器相对的在过滤链的前端,可是也不排斥在它以前的过滤器,前面还剩下50个位置)。
Spring Security 在过滤链中表现为一个Filter
,其类型是FilterChainProxy
,缘由你很快就会知道。在一个 Spring Boot 应用中安全过滤器是ApplicationContext
中的一个Bean
,而且它是默认配置的,因此在每次请求中都会存在。而它在过滤链中的位置由SecurityProperties.DEFAULT_FILTER_ORDER
决定,而该位置又由FilterRegistrationBean.REQUEST_WRAPPER_FILTER_MAX_ORDER
(值为0,是 Spring Boot 中改变请求行为的过滤器的顺序的最大值)锚定。(译者注:Order
的值越小,越在过滤链的前端)。除此以外,从容器的角度来看 Spring Security 是一个单独的过滤器,可是其中包含了额外过滤器,每一个过滤器都发挥特殊的做用,以下图所示:
图2 Spring Security 是一个单独的物理过滤器,可是它将请求委托一系列的内部过滤器
事实上内部的安全过滤器不止一个层次结构:它们一般是DelegatingFilterProxy
,不须要是一个 Spring Bean
。代理委托给FilterChainProxy
,而它是一个Bean
,bean 的名字一般是springSecurityFilterChain
。FilterChainProxy
包含了全部内部安全过滤器,而且以必定顺序排列成过滤链。其中全部的过滤器都有相同的 API(它们都实现了Servlet规范的Filter
接口),它们都有机会否决过滤链的下流部分。
Spring Security 能够在同一顶层FilterChainProxy
中管理多个过滤器链,而且对容器来讲都是未知的。Spring Security Filter 包含了一系列的过滤链,而且向这些链分发匹配它们的请求。下图展现了根据请求路径来分发(/foo/**
在/**
以前匹配)。这是一种常见但不是惟一的分发方式。最重要的特征是,分发过程当中,只有一条过滤链只处理该请求。
图3 Spring Security
FilterChainProxy
分发请求给首先匹配的过滤链。
一个纯净的(没有自定义安全配置的) Spring Boot 应用一般有 n 条过滤链,n = 6。第一条链(n-1)是忽略静态资源的,好比/css/**
和/images/**
,和错误页面/error
(这些路径能够在SecurityProperties
中的security.ignored
里配置)。最后一条链匹配全部路径/**
,而且包含认证逻辑、受权、异常处理、session 处理,响应头处理等。这条过滤链中默认一共有 11 个过滤器,咱们通常不关心使用哪一个过滤器以及在什么时候使用他们。
注意:意识到 Spring Security 的内部过滤器对容器是透明的这是很重要的,全部的
Filter
都以@Bean
的方式自动注册到容器中。因此若是你想在安全过滤链中添加过滤器,你不须要使用@Bean
注解或将其包裹在显示禁用容器注册的FilterRegistrationBean
中。
Spring Boot 中默认的 fallback 过滤链(使用/**
匹配的过滤链)有一个预约义的顺序SecurityProperties.BASIC_AUTH_ORDER
。你可使用security.base.enabled=false
关闭它,或者你能够定义一个更低的顺序值(译者注:越低的值表示顺序更前,因此它的顺序在默认的 fallback 以前)。只要添加一个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 在默认的 fallback 以前添加一个过滤链。
许多应用中对一组资源的和另外一组资源的访问规则可能大不相同。好比一个有前端页面和后端 API 的应用支持基于 cookie 的认证将用户重定向到登陆界面,同时也支持基于 token 的认证,认证失败将返回 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 最容易犯的一个错误就是忘记匹配规则能够应用在不一样的范围中,一个是整条过滤链,另外一个是应用于过滤链匹配规则中的规则。
略,我没用过 Actuator,因此就没翻译
Spring Security 在支持 web 安全的同时,也提供了对 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 建立了MyService
Bean,那么它将被代理,调用者必须在方法调用以前经过一个安全拦截器。若是访问被拒绝,调用者会抛出一个AccessDeniedException
而不是执行这个方法的结果。
还有其余可用于强制执行安全约束的方法注解,特别是@PreAuthorize
和 @PostAuthorize
, 它们容许你在其中写 SpEL 表达式并能够引用方法的参数和返回值。
提示: 把 web 安全和方法安全放在一块儿并不突兀。过滤链提供了用户体验特性,好比认证和重定向到登陆界面。而方法安全在更细粒度级别上提供了保护。
Spring Security是线程绑定的,由于它须要保证当前的已认证的用户(authenticated principal)对下流的消费者可用。基本构建块是SecurityContext
,它可能包含Authentication
(当一个用户登录后,authenticated
确定是 true
)。你老是能够从SecurityContextHolder
中的静态方法获得SecurityContext
,它内部使用了ThreadLocal
进行管理。
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
assert(authentication.isAuthenticated);
复制代码
这种操做并不常见,可是它可能对你有帮助。好比,你须要写一个自定义的认证过滤器(尽管如此,Spring Security 中还有一些基类可用于避免使用SecurityContextHolder
的地方)。
若是须要访问 web endpoint(译者注:对应响应的 URL) 中通过身份验证的用户,则能够在@RequestMapping
中使用方法参数注解。例如:
@RequestMapping("/foo")
public String foo(@AuthenticationPrincipal User user) {
... // do stuff with user
}
复制代码
这个注解至关于从SecurityContext
中得到当前Authentication
,并调用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
用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));
}
}
复制代码