认证和授权

应用安全性大致归结为两个独立的问题:认证(Authentication)和授权(Authorization)。认证相当于“你是谁”,授权相当于“你被允许做什么”。授权还有另外一个叫法,访问控制(Access Control)。Spring Security的架构设计将这两部分区分开来,并且两者都有自己的策略以及拓展点。

认证

认证主要的策略接口是AuthenticationManager

1
2
3
4
5
6
public interface AuthenticationManager {

Authentication authenticate(Authentication authentication)
throws AuthenticationException;

}

authenticate()**方法返回一个Authentication时代表认证通过,认证不通过时抛出 AuthenticationException,无法判定时返回null。AuthenticationException 是一个运行时异常。抛出异常时,一般前台页面会提示认证失败并且后端服务会返回401并且响应头中会有名为WWW-Authenticate的header。
**AuthenticationManager
的默认实现是ProviderManager,它内部实现是委托一组AuthenticationProvider的实例完成authenticate方法逻辑。AuthenticationProvider接口类似于AuthenticationManager接口,它提供一个额外的support方法来表示自己支持哪种Authentication的认证。

1
2
3
4
5
6
7
8
public interface AuthenticationProvider {

Authentication authenticate(Authentication authentication)
throws AuthenticationException;

boolean supports(Class<?> authentication);

}

supports()方法中的Class<?> 参数实际上是 Class<? extends Authentication>ProviderManager通过委托一组AuthenticationProvider实现在同一个应用中支持多种不同的认证机制。如果某一个Authentication无法被ProviderManager识别,那么这个Authentication将被跳过。
一个
ProviderManager有一个可选的parent,当所有的providers返回null时,它可以咨询他的parent。如果没有配parent,当所有providers返回null时,ProviderManager会抛出AuthenticationException
有时,一个应用中受保护资源具有逻辑组关系时(比如所有的web资源匹配/api/
),每个组都可以拥有他们各自的 AuthenticationManager。这些 ProviderManager拥有同一个parent,这个parent代表一些全局的资源,作为所有providers的fallback。

自定义Authentication Managers

Spring Security 提供了一些configuration helpers以快速帮你配置一些常用的authentication manager功能。最常用的helper是AuthenticationManagerBuilder,它可以创建内存、JDBC或者LDAP userdetails,或者添加自定义的UserDetailsService。下面这个例子是配置一个global(parent) AuthenticationManager

1
2
3
4
5
6
7
8
9
10
11
12
@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");
}

}

注意:AuthenticationManagerBuilder是被*@Autowired到一个@Bean*的方法中。这个AuthenticationManagerBuilder是配置global(parent)的AuthenticationManager
相反:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@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");
}

}

重写Configurer的方法,此时的AuthenticationManagerBuilder配置的是local的AuthenticationManager,它是globalAuthenticationManager的子级。在Spring Boot 应用中,可以 @Autowired global的AuthenticationManager到某个bean中,但是local的AuthenticationManager不可以这样做。通常情况下,只需要配置localAuthenticationManager就可以。

授权(Access Control)

授权的核心策略接口是AccessDecisionManager。框架提供了三种实现,并且它们都委托一组AccessDecisionVoter,就像ProviderManager委托AuthenticationProviders一样。

1
2
3
4
5
6
7
8
public interface AccessDecisionVoter<S> {
boolean supports(ConfigAttribute attribute);

boolean supports(Class<?> clazz);

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

Authentication代表一个用户凭证(同时也是授权逻辑的调用者),object是被保护的对象(用户想要访问的资源,可以是一个web资源也可以是一个java方法),attributes是与object相关联的配置属性(例如,角色名)。

Web 安全性

Spring Security在web层(UI和接口层)是基于Servlet Filters。Spring Security仅仅是Filter链中的一个Filter,具体的类型是FilterChainProxy。在Spring Boot应用中,security filter是上下文中的一个@Bean,SecurityProperties.DEFAULT_FILTER_ORDER是它的默认顺序。在web容器的角度看,Spring Security是一个单独的Filter。但是从内部看,它包含很多不同角色的Filter。

事实上,security filter和filter chain中还会有一个中间层,通常是DelegatingFilterProxy,它可以不是上下文中的一个bean。DelegatingFilterProxy委托FilterChainProxy(上下文中的一个bean,有固定的名字:springSecurityFilterChain),FilterChainProxy包含所有spring security filter。所有的spring security filter都实现servlet规范中的filter接口。一个FilterChainProxy可以包含多个filter chain,他们被spring security管理,web容器无法感知他们的存在。

创建和自定义Filter Chains

Spring Boot应用中默认的fallback filter chain 匹配url /** ,可以通过security.basic.enabled=false关闭它,或者可以把它作为一个fallback,用更低的order定义自己的规则(添加一组filter chain):添加一个类型为 WebSecurityConfigurerAdapter (或者 WebSecurityConfigurer)的@Bean,并在类上注明order。

1
2
3
4
5
6
7
8
9
@Configuration
@Order(SecurityProperties.BASIC_AUTH_ORDER - 10)
public class ApplicationConfigurerAdapter extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/foo/**")
...;
}
}

分发和授权的请求匹配

一个security filter chain(或者说WebSecurityConfigurerAdapter)有一个request matcher用来决定是否匹配某个HTTP请求,一旦匹配,其他的security filter chain不会再匹配这个HTTP请求。但是在一个filter chain内部,可以通过向HttpSecurity添加额外的matchers实现更细粒度的授权控制。

1
2
3
4
5
6
7
8
9
10
11
12
@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();
}
}

注意:方法中第一行和第三、四行方法名是不同的。第一行是整个filter chain的request matcher。

方法安全

Spring Security同样可以保护Java方法的调用。通过如下方法开启:

1
2
3
4
@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SampleSecureApplication {
}

然后可以直接在要被保护的方法上添加注释:

1
2
3
4
5
6
7
8
9
@Service
public class MyService {

@Secured("ROLE_USER")
public String secure() {
return "Hello Security";
}

}

线程相关

Spring Security是与线程绑定的。想要在代码中手动获取Authentication可以通过以下方式:

1
2
3
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
assert(authentication.isAuthenticated);

controller层:

1
2
3
4
@RequestMapping("/foo")
public String foo(@AuthenticationPrincipal User user) {
... // do stuff with user
}

异步处理Secure Methods

因为SecurityContext是线程绑定的,所以任何对Secure Methods异步的调用,需要主要上文的传递。这就需要将SecurityContext包装进任务中(Runnable, Callable等等)。Spring Security提供了一些helpers。

1
2
3
4
5
6
7
8
9
@Configuration
public class ApplicationConfiguration extends AsyncConfigurerSupport {

@Override
public Executor getAsyncExecutor() {
return new DelegatingSecurityContextExecutorService(Executors.newFixedThreadPool(5));
}

}