本文主要翻译Spring的官方文档,主要介绍SS的基本概念和基本接口和原理。
Spring Security中的两个核心的概念:Authentication(验证);Authorization(授权); Spring Security可以用来实现诸如,OAuth、OpenID、LDAP、CAS、JAAS等系统间的认证很授权。如我们可以使用Spring Security配合OAuth实现单点登录。
Authentication主要的接口是AuthenticationManager public interface AuthenticationManager { Authentication authenticate(Authentication authentication) throws AuthenticationException; }
authenticate()方法调用有三种情况:
public interface AuthenticationProvider { Authentication authenticate(Authentication authentication) throws AuthenticationException; boolean supports(Class<?> authentication); } 复制代码
ProviderManager将验证委托给一系列AuthenticationProvider,实际是一个AuthenticationProvider列表,所以他可以在一个应用中支持多种验证策略。在验证的时候会遍历所有的AuthenticationProvider逐个验证,调用supports方法判断是否验证该类型的Authentication,实则验证否则跳过。如果所有的AUthenticationProvider都无法决定返回了null,那就调用父级的AuthenticationManager验证(如果父级不为空)。 有时在应用中有验证的逻辑分组,每个分组有自己的AuthenticationManager(通常就是ProviderManager),这些分组共享一个父级AuthenticationManager。
SpringSecurity提供了一些配置帮助类来快去获取AuthenticationManager的功能,最常用的就是AuthenticationManagerBuilder,它可以以in-memory、JDBC、LDAP方式配置user details,或者添加自定义UserDetailService。eg.
@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"); } } 复制代码
??? spring官方给的代码实例有问题吧,configure方法没有返回类型
Authorization授权的主要接口是AccessDecisionManager,框架提供了3个实现类:UnanimousBased(全部通过)、ConsensusBased(少数服从多数)、AffirmativeBased(无人发对则通过),所有这些实现类都是把授权委托给一系列的AccessDecisionVoter,这有点像ProviderManager样验证委托给AuthenticationProvider。此处AccessDecisionVoter就是抽象的投票人。
public interface AccessDecisionVoter<S> { boolean supports(ConfigAttribute attribute); boolean supports(Class<?> clazz); int vote(Authentication authentication, S object, Collection<ConfigAttribute> attributes); } 复制代码
一个AccessDecisionVoter会考虑验证主体Authentication和一个被ConfigAttributes修饰的安全对象Object。 Object在AccessDecisionManager和AccessDecisionVoter的签名中是完全通用的,Object代表一切用户可能要访问的东西(通常情况是网络资源或者Java类的方法调用这两种情况)。ConfigAttributes也是通用的,用一些元数据来表示对安全对象的修饰,这些元数据决定访问安全对象所需要的权限级别。ConfigAttribute是一个接口,它只有一个非常通用的方法,并返回String,因此这些字符串以某种方式编码了资源所有者的意图,表达关于谁被允许访问安全对象的规则。一个典型的ConfigAttribute就是用户的角色名(如 ROLE_ADMIN 或者 ROLE_AUDIT),并且它们通常会有特殊的格式(如 ROLE_ 前缀)或者表示需要评估的表达式。 大部分人只是用AccessDecisionManager的默认实现类AffirmativeBased(无人反对则允许通过),任何定制都会发生在投票者身上,要么添加新的,要么修改现有的工作方式。 使用EL表达式的ConfigAttribute是非常常见的,例如 isFullyAuthenticated() && hasRole('FOO')
, 这是有一个可以处理表达式并为他们创建上下文的AccessDecisionVoter支持的。要扩展可处理的表达式的范围,需要自定义SecurityExpressionRoot来实现,有时还需要SecurityExpressionHandler。
Spring Security在web层的使用是基于Servlet过滤器的,下图是单个HTTP请求处理程序的典型分层。
当客户端向应用发送一个请求,应用容器会根据请求的URI路径来觉得应用那个过滤器和Servlet。最多只能有一个Servlet处理请求,但是过滤器是链式的,所以过滤器处理是有顺序的,实际上一个过滤器如果想要自己处理请求可以断开过滤链。过滤器也可以修改下流过滤器使用的Request和Response。过滤器链的顺序非常重要,Spring boot为其提供了2中机制:第一个是使用@Order注解或着实现Ordered接口,另一个是作为FilterRegistrationBean的一部分,FilterRegistrationBean本身的API是有顺序的。一些现成的过滤器通过定义常数,来表示他们系统彼此之间的相对顺序(例如,SessionRepositoryFilter中的常数DEFAULT_ORDER = Integer.MIN_VALUE + 50,这告诉我们它喜欢在链的早期,但是它不排除其他过滤器在它之前)。 Spring Security是过滤器链上一个节点作为Spring Security接入的入口,该过滤器就是FilterChainProxy,如下图:
Spring Security在请求处理过滤器链中插入了一个过滤器节点FilterChainProxy,看名字就知道是个代理过滤器链,它过处理过程委托给内部的过滤器(这里就进入Spring Security的内部,下文称安全过滤器)。事实上,在安全过滤器中甚至还有一层DelegatingFilterProxy,它通常在标准顾虑器和安全过滤器之间做代理,它将委托给FilterChainProxy,它没有必要必须是Spring的bean,而FilterChainProxy是Spring的bean,也就是说一般FilterChainProxy的生命周期交给Spring容器来管理。 FilterChainProxy中包含了一个过滤器链列表,会将请求分发到第一个匹配的过滤器链中去,下图显示了基于匹配请求路径的调度,这很常见,但不是匹配请求的唯一方式。这个调度过程最重要的特征是只有一个链处理一个请求。
Spring Boot应用中的默认的回退过滤器链(匹配/**)预先定义了顺序(SecurityProperties.BASIC_AUTH_ORDER),你可以通过security.basic.enabled=false来关闭它,或者你可以使用它作为回退过滤器你只定义较低顺序的匹配其他规则的过滤器。为此,只需要添加一个WebSecurityConfigurerAdapter(或者WebSecurityConfigurer)类型的@Bean,并且使用@Order注解修饰这个Class。例如:
@Configuration @Order(SecurityProperties.BASIC_AUTH_ORDER - 10) public class ApplicationConfigurerAdapter extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.antMatcher("/foo/**") ...; } } 复制代码
安全过滤器链(或等同于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内容,暂且略过
除了支持web应用,Spring Security还支持将访问规则应用到Java方法执行。对于Sping 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,它们允许您分别编写包含对方法参数和返回值的引用的表达式。
Spring Security从根本上说是线程绑定的,因为它需要使当前经过身份验证的主体对各种各样的下游消费者可用,基本的构建模块是SecurityContext,它可能包含Authentication(当用户登录时,它将是一个验证通过的Authentication)。您始终可以通过SecurityContextHolder里的静态方便方法访问和操作SecurityContext。例如:
SecurityContext context = SecurityContextHolder.getContext(); Authentication authentication = context.getAuthentication(); assert(authentication.isAuthenticated); 复制代码
如果您需要访问网络端点中当前经过身份验证的用户,可以在@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 } 复制代码
由于SecurityContext是线程绑定的,如果您想要执行任何调用安全方法的后台处理,例如使用@Async,您需要确保上下文得到传播。这可以归结为用任务(Runnable、Callable等)包装SecurityContext,在后台执行。Spring Security提供了一些助手来简化这一过程,比如Runnable和Callable的包装器。要将SecurityContext传播到@Async方法,您需要提供一个AsyncConfigurer,并确Executor的类型正确:
@Configuration public class ApplicationConfiguration extends AsyncConfigurerSupport { @Override public Executor getAsyncExecutor() { return new DelegatingSecurityContextExecutorService(Executors.newFixedThreadPool(5)); } } 复制代码