盒子
盒子
文章目录
  1. Spring Security Architecture
    1. Authentication和Access Control
      1. Authentication
      2. Customizing Authentication Managers
      3. Authorization or Access Control
  2. Web Security
    1. Creating and Customizing Filter Chains
  • Request Matching for Dispatch and Authorization
    1. Combining Application Security Rules with Actuator Rules
  • Method Security
  • Working with Threads
    1. Processing Secure Methods Asynchronously
  • Spring Security Architecture

    Spring Security Architecture

    Authentication和Access Control

    应用安全或多或少归结为两个独立的问题:认证(你是谁?)和授权(你能干啥?)。Spring Security就是针对这两个问题设计出来的框架,而且具有良好的拓展性。

    Authentication

    认证的主要接口是AuthenticationManager,这个接口只有一个方法:

    1
    2
    3
    4
    5
    6
    public interface AuthenticationManager {

    Authentication authenticate(Authentication authentication)
    throws AuthenticationException;

    }

    AuthenticationManagerauthenticate()中做了以下3件事中的一件:

    1. 如果输入的验证信息正确,那么返回一个Authentication对象(通常设置其属性authenticated=true
    2. 若果验证信息不正确,就直接抛出AuthenticationException异常
    3. 若果不能判断信息正确与否就返回null

    AuthenticationException是一个运行时异常。它通常由应用默认的一个通用方法处理掉,这取决于应用的风格和目的。换句话说就是开发者一般不应该在代码里直接捕获和处理它,直接抛出。

    最常见的AuthenticationManager实现是ProviderManager,这个实现类利用一系列的AuthenticationProvider来进行认证。AuthenticationProvider 接口有点像AuthenticationManager,但是它有一个额外的方法可供调用,这个方法用来检查给定的认证方式是否支持:

    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>。通过委托一系列的AuthenticationProviders,应用里的一个ProviderManager可以支持多种不同的认证方式。若果ProviderManager不能认证某个特殊的Authentication实例,那么它就会被跳过。

    ProviderManager有一个可选的父类,若果子类的所有providers都返回null,那么就到父类中进行验证。若果没有父类,并且认证失败的话将会抛出AuthenticationException

    有时候,一个应用会有关于某种资源的集合(譬如所有匹配/api/**URI的页面资源),每种集合都会有属于它自己的AuthenticationManager。有时候,这些集合的AuthenticationManager的实现类是一个ProviderManager,并且它们共享同一个父类。这个父类相当于一种“全局”资源,用作所有子类providers都认证失败时的一种替补验证方案。

    ProviderManagers with a common parent

    Customizing Authentication Managers

    Spring Security提供一些配置helper来快速配置认证manager。最常见的helper就是AuthenticationManagerBuilder,适合用来创建in-memory,JDBC或者LDAP的UserDetails,或者是添加自定义的UserDetailsService。下面的例子展示了怎么配置一个全局的AuthenticationManager,即是上面提及的父类manager:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @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的应用场景不止如此。注意,这里的AuthenticationManagerBuilder是被用@Autowired注入到@Bean的方法中,这样可以让builder创建一个全局的父类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 void configure(AuthenticationManagerBuilder builder) {
    builder.jdbcAuthentication().dataSource(dataSource).withUser("dave")
    .password("secret").roles("USER");
    }

    }

    (通过@Override父类的方法)那么AuthenticationManagerBuilder只会创建一个局部的AuthenticationManager,它是全局AuthenticationManager的一个子类。在Spring boot应用中可以@Autowired全局AuthenticationManager到其它bean中,但是不能够@Autowird一个局部的,除非把它暴露出来。

    Spring Boot提供一个默认的全局 AuthenticationManager ,除非自定义一个全局的AuthenticationManagerBean来替换它。一般情况下,默认的全局AuthenticationManager足够安全,无须过于担心其安全性,除非自定义一个全局AuthenticationManager

    Authorization or Access Control

    一旦认证成功,我们就能进行下一步:授权。授权的核心是AccessDecisionManager。框架提供了三种实现类,这三种实现类都是委托一系列的AccessDecisionVoter来确定是不是要授权,有点像ProviderManager委托AuthenticationProviders

    一个AccessDecisionVoter需要一个Authentication(表示当前认证了的用户)和一个被ConfigAttributes描述的受保护对象来确定是否授权:

    1
    2
    3
    4
    5
    6
    boolean supports(ConfigAttribute attribute);

    boolean supports(Class<?> clazz);

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

    Object完全可以在AccessDecisionManagerAccessDecisionVoter之间进行传递。它代表一个用户想要访问的东西(最常见的是一个web资源或者一个Java类中的方法)。ConfigAttributes也可以在AccessDecisionManagerAccessDecisionVoter之间进行传递,它携带了一些信息,这些信息代表了这个受保护的Object的授权规则。ConfigAttribute是一个接口,但是它只有一个返回字符串的方法,这些字符串表示了受保护资源的授权规则,并且通常有特殊格式(譬如ROLE_前缀)或者是一个需要计算的表达式。

    大多开发者都只使用默认的AccessDecisionManager,具体实现类是AffirmativeBased(这个实现类的策略是只要有一个voter通过,就会被授权)。自定义是否授权,要么就是在voter里新增授权规则,要么就是修改原有的授权规则。

    使用Spring Expression Language(SpEL)表达式来确定是否授权很常见,如isFullyAuthenticated() && hasRole('FOO')。若果需要自定义SpEL表达式,那么需要实现SecurityExpressionRoot 或者 SecurityExpressionHandler


    Web Security

    Spring Security 是基于Servlet的Filters,所以先看看Filters会很大有裨益。下图是一次HTTP请求从发起到被处理过程中所经过的handlers的层级图。

    Filter chain delegating to a Servlet

    客户端发送一个请求到APP,然后container基于请求的URI路径决定哪个过滤器和哪个servlet会处理这次请求。一次请求最多能被一个servlet处理,但是过滤器则是组成一条有序链,请求可以在链中被顺序地处理,并且一个过滤器可以拦截请求,不让后续的过滤器处理。一个过滤器也可以修改传递给下游过滤器和servlet的请求和(或者)响应。过滤器链的顺序非常重要,而且Spring Boot通过2个机制来管理链的顺序:一个是有@Order注解或者实现了Ordered接口的Filter@Bean,另外一种是把过滤器修饰成FilterRegistrationBean,这个FilterRegistrationBean有一个order的成员变量决定调用的顺序。一些现成的过滤器定义了它们自己的值来表明它们想要相对于彼此的顺序位置(例如来自Spring Session的SessionRepositoryFilter有一个值为Integer.MIN_VALUE + 50DEFAULT_ORDER,这说明它想在链中早点被处理,同时不妨碍比它靠前的过滤器被处理)。

    Spring Security本质上就是这条链中的一个Filter,具体类型是FilterChainProxy

    下文将会提及这个类。在一个Spring Boot app中,这个Spring Security的过滤器是ApplicationContext中的一个@Bean,而且默认是被添加到链中,所以它可以处理每一次请求。它在链中的位置由SecurityProperties.DEFAULT_FILTER_ORDER定义,该位置由FilterRegistrationBean.REQUEST_WRAPPER_FILTER_MAX_ORDER锚定。不仅如此,从container的角度看,Spring Security就只是一个过滤器,但是从Spring Security角度看,它自身内部包含了很多额外的过滤器,每一个都有特定的作用。如下图:

    实际上,Spring Security的过滤器中甚至还有一层间接层:这个间接层在container中是以DelegatingFilterProxy形式存在,并且不是一个Spring @Bean。这个DelegatingFilterProxy在容器中代表着FilterChainProxy,这个FilterChainProxy有着一个固定的名字springSecurityFilterChain

    FilterChainProxy包含了所有的安全逻辑,并且在内部通过过滤器链来实现。链中的过滤器都有相同的API接口(从Servlet角度看它们都实现了Filter接口),并且可以拦截请求,不让后续的过滤器处理。

    在Spring Security中,顶层的FilterChainProxy管理着多条的过滤器链,而且container不知道这些链的存在。Spring Security filter包含了一个过滤器链的列表,并且把请求分派给第一个匹配的过滤器链。下图展示了基于请求路径的分派例子(/foo/ 会比 /**先匹配)。这是最常见的但不是唯一的匹配方式。最重要的一点就是只有一条过滤器链处理一个请求。

    Security Filter Dispatch

    没有自定义安全配置的Spring Boot应用有n条过滤器链,一般n=6。第n - 1条链会忽略静态资源匹配,如/css/**/images/**,以及错误页面/error(用户能通过配置SecurityPropertiessecurity.ignored来配置这些路径)。最后一条过滤器链匹配全路径/**,而且更加灵活,这条链包含了认证,授权,异常处理,会话处理,头写入等等的逻辑。这条链默认一共有11个过滤器,但是用户一般不需要关心用到了哪些过滤器,以及什么时候用到。

    1
    Spring Security内所有的过滤器对于container来说都是未知的,这一点很重要,特别是在一个Spring Boot应用中,所有的`Filter`类型的`@Beans`都是自动被container注册。所以若果想在安全链中添加一个自定义的过滤器,就要不声明为`@Bean`,或者封装到`FilterRegistrationBean`中并且显式禁止container注册。
    Creating and Customizing Filter Chains

    在Spring Boot app中,默认备用的过滤器链(即匹配/**请求的链)有一个SecurityProperties.BASIC_AUTH_ORDER的预定义顺序。通过设置security.basic.enabled=false,或者设置一个lower的顺序就可以关掉这个预定义顺序。要这样做,只需添加一个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/**")
    ...;
    }
    }

    这个bean会导致Spring Security增加一条新的过滤器链并且放在备用链前。

    许多应用对于不同的资源,都有不同的访问规则。每一个资源都有它自己的WebSecurityConfigurerAdapter,这个adapter有唯一的顺序,并且有自己的请求matcher。若果匹配规则有重叠,顺序靠前的过滤器将优先。

    Request Matching for Dispatch and Authorization

    一条安全的过滤器链(等价于一个WebSecurityCOnfigurerAdapter)有一个用于请求的匹配器,这个匹配器决定这条过滤器链应用到这个HTTP请求。一旦一条过滤器链被应用,其它的就不会被应用了。但是在过滤器链中你能够通过在HttpSecurity中配置额外的匹配器来进行更细粒度的控制:

    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();
    }
    }

    最常见的错误是忘记这些匹配器是应用于不同的处理过程,一个是匹配整个过滤器链(antMatcher(“/foo/**”)),另外一个只是设置访问规则(antMatchers(“/foo/bar”).hasRole(“BAR”))。

    Combining Application Security Rules with Actuator Rules

    若果使用Spring Boot Actuator来管理端点,那就希望这些端点都是安全的,并且默认就是如此。事实上只要添加Actuator到一个安全应用,就会得到一条额外的只应用到actuator端点的过滤器链。它定义了一个只匹配actuator端点的请求匹配器,并且有着一个值为5的ManagementServerProperties.BASIC_AUTH_ORDER,这个顺序要小于SecurityProperties的备用链,所以会在备用链前被调用。

    若果需要应用安全规则到这些actuator端点,可以添加一个顺序要比默认actuator端点的链更早的,匹配所有端点的过滤器链。若果想用默认的配置,那么最简单的方式是添加一个顺序比默认actuator后,但比备用早(如ManagementServerProperties.BASIC_AUTH_ORDER + 1的过滤器,如

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

    Method Security

    正如支持安全web应用那样,Spring Security也为安全访问Java方法提供支持。对于Spring Security来说这不过是一种不同“保护资源“。对于用户来说,这意味着访问规则使用相同格式的ConfigAttribute字符串(如角色或者表达式),但是编写代码的位置就不一样了。第一步是启动method security,例如在应用的顶层配置:

    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";
    }

    }

    这个一个有着安全方法的service。若果Spring创建一个这样类型的@Bean,那么它会被代理,并且调用者将会被安全拦截器进行权限验证,通过后方法才能执行。若果验证失败,那么调用者将会得到AccessDeniedException而不是方法的执行结果。

    还有其它用于方法的,可以增强安全性的注解,譬如@PreAuthorize@PostAuthorize,分别能让你写一些关于方法参数和返回结果的表达式。

    1
    组合Web安全和方法安全的例子并不少见。过滤器链为用户提供常见的功能,如认证和重定向到登录页面等等,而方法安全提供更具颗粒度的保护。

    Working with Threads

    Spring Security从根本上来说是线程绑定的,因为它需要确保当前的认证用户信息可以被下游的消费者获取。基本构件是SecurityContext,它可能包含了一个Authenticaiton(当一个用户登录后Authentication将会被明确地标记为authenticated)。通过SecurityContextHolder静态方法,你可以总是访问和操作SecurityContext

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

    在user应用代码中这样做不常见,但是要自定义认证过滤器的话,这会很有用(尽管Spring Security中有基类可以实现相同功能,避免使用SecurityContextHolder)。

    若果在一个web端点中访问当前认证用户,可以在@RequestMapping中传入参数:

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

    这个注解从SecurityCOntext中拉取当前Authentication,并且调用getPrincipal()方法得到参数。Authentication中的Principal的类型依赖于进行认证AuthenticationManager,因此这是一个获取安全用户数据引用的小技巧。

    若果使用Spring Security,那么来自HttpServletRequestPrincipal会是Authentication类型,那么可以直接这样定义:

    1
    2
    3
    4
    5
    6
    @RequestMapping("/foo")
    public String foo(Principal principal) {
    Authentication authentication = (Authentication) principal;
    User = (User) authentication.getPrincipal();
    ... // do stuff with user
    }

    有时候需要代码在没用Spring Security情况下正常工作,这些样就会很有用(你将需要更加安全地加载Authentication类)。

    Processing Secure Methods Asynchronously

    因为SecurityContext是线程绑定的,若果想在后台处理中调用安全方法,如带有@Async,那么需要确保上下文被传递。这可以归结为用后台任务(RunnableCallable等)封装SecurityContext。Spring Security提供一些helpers来简化这个操作,例如使用RunnableCallable的wrapper。为了传递SecurityContext@Async方法,需要提供一个AsyncConfigurer并且确保Executor是正确类型:

    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));
    }

    }