2019-07-24 Spring Security核心流程

Spring Security

学习 Spring Security 用法,架构、设计模式等。

核心组件

主要介绍 Spring Security 中常见核心 Java 类以及他们之间的依赖关系,以及整个架构的设计原理。

SecurityContextHolder

SecurityContextHolder 用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保存在 SecurityContextHolder中。SecurityContextHolder 默认使用 ThreadLocal 策略来存储认证信息。看到 ThreadLocal 也就意味着,这是一种与线程绑定的策略。Spring Security 在用户登录时自动绑定认证信息到当前线程,在用户退出时,自动清除当前线程的认证信息。但这一切的前提,是你在 web 场景下使用 Spring Security

获取当前用户的信息

因为身份信息是与线程绑定的,所以可以在程序的任何地方使用静态方法获取用户信息。一个例子如下:

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();

if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}

getAuthentication() 返回了认证信息,再次 getPrincipal() 返回了身份信息,UserDetails便是 Spring 对身份信息封装的一个接口。AuthenticationUserDetails 的介绍在下面的小节具体讲解,本节重要的内容是介绍 SecurityContextHolder 这个容器。

Authentication

直接上源码:

package org.springframework.security.core;// <1>

public interface Authentication extends Principal, Serializable { // <1>
    Collection<? extends GrantedAuthority> getAuthorities(); // <2>

    Object getCredentials();// <2>

    Object getDetails();// <2>

    Object getPrincipal();// <2>

    boolean isAuthenticated();// <2>

    void setAuthenticated(boolean var1) throws IllegalArgumentException;
}

<1> Authentication 是 spring security 包中的接口,直接继承自 Principal 类,而 Principal 是位于 java.security 包中的。可以见得,Authentication 在 spring security 中是最高级别的身份/认证的抽象。

<2> 由这个顶级接口,我们可以得到用户拥有的权限信息列表,密码,用户细节信息,用户身份信息,认证信息。

还记得1.1节中,authentication.getPrincipal()返回了一个 Object,我们将 Principal 强转成了 Spring Security 中最常用的UserDetails,这在 Spring Security 中非常常见,接口返回 Object,使用 instanceof 判断类型,强转成对应的具体实现类。接口详细解读如下:

  • getAuthorities(),权限信息列表,默认是 GrantedAuthority 接口的一些实现类,通常是代表权限信息的一系列字符串。
  • getCredentials(),密码信息,用户输入的密码字符串,在认证过后通常会被移除,用于保障安全。
  • getDetails(),细节信息,web 应用中的实现接口通常为 WebAuthenticationDetails,它记录了访问者的 ip 地址和 sessionId 的值。
  • getPrincipal(),敲黑板!!!最重要的身份信息,大部分情况下返回的是 UserDetails 接口的实现类,也是框架中的常用接口之一。UserDetails 接口将会在下面的小节重点介绍。

Spring Security是如何完成身份认证的?

  1. 用户名和密码被过滤器获取到,封装成 Authentication ,通常情况下是 UsernamePasswordAuthenticationToken 这个实现类。
  2. AuthenticationManager 身份管理器负责验证这个 Authentication
  3. 认证成功后,AuthenticationManager 身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,身份信息,细节信息,但密码通常会被移除)Authentication 实例。
  4. SecurityContextHolder 安全上下文容器将第3步填充了信息的 Authentication,通过 SecurityContextHolder.getContext().setAuthentication(…) 方法,设置到其中。

这是一个抽象的认证流程,而整个过程中,如果不纠结于细节,其实只剩下一个 AuthenticationManager 是我们没有接触过的了,这个身份管理器我们在后面的小节介绍。

AuthenticationManager

初次接触 Spring Security 的朋友相信会被 AuthenticationManagerProviderManagerAuthenticationProvider …这么多相似的 Spring 认证类搞得晕头转向,但只要稍微梳理一下就可以理解清楚它们的联系和设计者的用意。AuthenticationManager(接口)是认证相关的核心接口,也是发起认证的出发点,因为在实际需求中,我们可能会允许用户使用用户名+密码登录,同时允许用户使用邮箱+密码,手机号码+密码登录,甚至,可能允许用户使用指纹登录(还有这样的操作?没想到吧),所以说 AuthenticationManager 一般不直接认证,AuthenticationManager 接口的常用实现类 ProviderManager 内部会维护一个 List<AuthenticationProvider> 列表,存放多种认证方式,实际上这是委托者模式的应用(Delegate)。也就是说,核心的认证入口始终只有一个:AuthenticationManager,不同的认证方式:用户名+密码(UsernamePasswordAuthenticationToken),邮箱+密码,手机号码+密码登录则对应了三个 AuthenticationProvider。这样一来四不四就好理解多了?熟悉 shiro 的朋友可以把AuthenticationProvider 理解成 Realm。在默认策略下,只需要通过一个 AuthenticationProvider 的认证,即可被认为是登录成功。

ProviderManager 关键部分源码

public class ProviderManager implements AuthenticationManager, MessageSourceAware,
        InitializingBean {

    // 维护一个AuthenticationProvider列表
    private List<AuthenticationProvider> providers = Collections.emptyList();

    public Authentication authenticate(Authentication authentication)
          throws AuthenticationException {
       Class<? extends Authentication> toTest = authentication.getClass();
       AuthenticationException lastException = null;
       Authentication result = null;

       // 依次认证
       for (AuthenticationProvider provider : getProviders()) {
          if (!provider.supports(toTest)) {
             continue;
          }
          try {
             result = provider.authenticate(authentication);

             if (result != null) {
                copyDetails(authentication, result);
                break;
             }
          }
          ...
          catch (AuthenticationException e) {
             lastException = e;
          }
       }
       // 如果有Authentication信息,则直接返回
       if (result != null) {
            if (eraseCredentialsAfterAuthentication
                    && (result instanceof CredentialsContainer)) {
                 //移除密码
                ((CredentialsContainer) result).eraseCredentials();
            }
             //发布登录成功事件
            eventPublisher.publishAuthenticationSuccess(result);
            return result;
       }
       ...
       //执行到此,说明没有认证成功,包装异常信息
       if (lastException == null) {
          lastException = new ProviderNotFoundException(messages.getMessage(
                "ProviderManager.providerNotFound",
                new Object[] { toTest.getName() },
                "No AuthenticationProvider found for {0}"));
       }
       prepareException(lastException, authentication);
       throw lastException;
    }
}

ProviderManager 中的 List ,会依照次序去认证,认证成功则立即返回,若认证失败则返回 null,下一个 AuthenticationProvider 会继续尝试认证,如果所有认证器都无法认证成功,则 ProviderManager 会抛出一个ProviderNotFoundException 异常。

以上已经把 Spring Security 的整个认证流程都讲述了一遍,简单小结如下:身份信息的存放容器 SecurityContextHolder ,身份信息的抽象 Authentication ,身份认证器 AuthenticationManager 及其认证流程。姑且在这里做一个分隔线。下面来介绍下 AuthenticationProvider 接口的具体实现。

DaoAuthenticationProvider

AuthenticationProvider 最最最常用的一个实现便是 DaoAuthenticationProvider 。顾名思义,Dao 正是数据访问层的缩写,也暗示了这个身份认证器的实现思路。由于本文是一个 Overview ,姑且只给出其 UML 类图:

按照我们最直观的思路,怎么去认证一个用户呢?用户前台提交了用户名和密码,而数据库中保存了用户名和密码,认证便是负责比对同一个用户名,提交的密码和保存的密码是否相同便是了。在Spring Security 中。提交的用户名和密码,被封装成了UsernamePasswordAuthenticationToken ,而根据用户名加载用户的任务则是交给了 UserDetailsService ,在DaoAuthenticationProvider 中,对应的方法便是 retrieveUser ,虽然有两个参数,但是 retrieveUser 只有第一个参数起主要作,返回一个 UserDetails。还需要完成 UsernamePasswordAuthenticationTokenUserDetails 密码的比对,这便是交给 additionalAuthenticationChecks 方法完成的,如果这个 void 方法没有抛异常,则认为比对成功。比对密码的过程,用到了PasswordEncoderSaltSource ,密码加密和盐的概念相信不用我赘述了,它们为保障安全而设计,都是比较基础的概念。

如果你已经被这些概念搞得晕头转向了,不妨这么理解 DaoAuthenticationProvider :它获取用户提交的用户名和密码,比对其正确性,如果正确,返回一个数据库中的用户信息(假设用户信息被保存在数据库中)。

UserDetails 与 UserDetailsService

上面不断提到了 UserDetails 这个接口,它代表了最详细的用户信息,这个接口涵盖了一些必要的用户信息字段,具体的实现类对它进行了扩展。

public interface UserDetails extends Serializable {

   Collection<? extends GrantedAuthority> getAuthorities();

   String getPassword();

   String getUsername();

   boolean isAccountNonExpired();

   boolean isAccountNonLocked();

   boolean isCredentialsNonExpired();

   boolean isEnabled();
}

它和 Authentication 接口很类似,比如它们都拥有 usernameauthorities ,区分他们也是本文的重点内容之一。AuthenticationgetCredentials()UserDetails 中的 getPassword() 需要被区分对待,前者是用户提交的密码凭证,后者是用户正确的密码,认证器其实就是对这两者的比对。Authentication 中的 getAuthorities() 实际是由 UserDetailsgetAuthorities() 传递而形成的。还记得 Authentication 接口中的 getUserDetails() 方法吗?其中的 UserDetails 用户详细信息便是经过了 AuthenticationProvider 之后被填充的。

public interface UserDetailsService {
   UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

UserDetailsServiceAuthenticationProvider 两者的职责常常被人们搞混,关于他们的问题在文档的 FAQissues 中屡见不鲜。记住一点即可,敲黑板!!!UserDetailsService 只负责从特定的地方(通常是数据库)加载用户信息,仅此而已,记住这一点,可以避免走很多弯路。UserDetailsService 常见的实现类有 JdbcDaoImplInMemoryUserDetailsManager,前者从数据库加载用户,后者从内存中加载用户,也可以自己实现 UserDetailsService,通常这更加灵活。

架构概览图

为了更加形象的理解上述我介绍的这些核心类,附上一张按照我的理解,所画出 Spring Security 的一张非典型的 UML

整个Spring Security 都是围绕以上这张架构图展开的,最顶层为最核心最抽象的 AuthenticationManager 接口, ProviderManagerAuthenticationManager 的一个具体实现,功能如其名字他的作用是管理 Provider 的, AuthenticationProvider 才是真正认证的接口,因此我们在实践中要实现我们自己的认证方式,也就是 AuthenticationProvider 的一个具体实现。当然可以实现多个,如果有多种认证方式,现实中往往也是有多种认证方式。

注意 AuthenticationProvider 接口中的 supports 方法,ProviderManager 里面其实是维护了一个 AuthenticationProviderList 因此到底使用哪个 Provider 来做验证,可以使用 Filter 返回的 Authentication 实现类来限定,部分代码如下:

//Filter 代码
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException, IOException, ServletException {
            ......
        return getAuthenticationManager().authenticate(new JwtAuthenticationToken(token));
    }
//Provider 代码
    @Override
    public boolean supports(Class<?> authentication) {
        return (JwtAuthenticationToken.class.isAssignableFrom(authentication));
    } 
//在 ProviderManager 中验证时有如下代码
for (AuthenticationProvider provider : getProviders()) {
   if (!provider.supports(toTest)) {
      continue;
   }

如果对Spring Security 的这些概念感到理解不能,不用担心,因为这是 Architecture First 导致的必然结果,先过个眼熟。后续的文章会秉持 Code First 的理念,陆续详细地讲解这些实现类的使用场景,源码分析,以及最基本的:如何配置 Spring Security ,在后面可以时不时往回看一看,找到具体的类在整个架构中所处的位置。另外,一些 Spring Security的过滤器还未囊括在架构概览中,如将表单信息包装成 UsernamePasswordAuthenticationToken 的过滤器,考虑到这些虽然也是架构的一部分,但是真正重写他们的可能性较小,所以打算放到后面讲解。

配置介绍

Security 安全核心配置例子:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/", "/home").permitAll()
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .loginPage("/login").permitAll()
                .and()
            .logout()
                .permitAll();
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth
            .inMemoryAuthentication()
                .withUser("admin").password("admin").roles("USER");
    }
}

当配置了上述的javaconfig之后,我们的应用便具备了如下的功能:

  • 除了“/”,”/home”(首页),”/login”(登录),”/logout”(注销),之外,其他路径都需要认证。
  • 指定“/login”该路径为登录页面,当未认证的用户尝试访问任何受保护的资源时,都会跳转到“/login”。
  • 默认指定“/logout”为注销页面
  • 配置一个内存中的用户认证器,使用admin/admin作为用户名和密码,具有USER角色
  • 防止CSRF攻击
  • Session Fixation protection
  • Security Header(添加一系列和Header相关的控制)
    • HTTP Strict Transport Security for secure requests
    • 集成X-Content-Type-Options
    • 缓存控制
    • 集成X-XSS-Protection.aspx)
    • X-Frame-Options integration to help prevent Clickjacking(iframe被默认禁止使用)
  • 为Servlet API集成了如下的几个方法
    • HttpServletRequest#getRemoteUser())
    • HttpServletRequest.html#getUserPrincipal())
    • HttpServletRequest.html#isUserInRole(java.lang.String))
    • HttpServletRequest.html#login(java.lang.String, java.lang.String))
    • HttpServletRequest.html#logout())

一个 Restful 配置

动态从数据库加载权限,这边因为 Spring Security 的机制是在模块启动的时候进行加载的,如果想要动态 reload 权限,调查来看Spring 并没有提供相关的接口,需要动态 reload Spring 相关的 bean 是一种比较危险暴力的做法,需要多加注意。

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        List<String> permitAllEndpointList = Arrays.asList(AUTHENTICATION_URL,"/api/*/webjars/**","/api/*/swagger**","/api/*/swagger-resources/**","/api/*/v2/api-docs");
        //load Authorities from DB
        ResponseData<List<Authority>> authorityRules = authorityService.getAuthority();
        
        try {
            for (AuthorityModel rule : authorityRules.getData()) {
                //这里因为 Restful API url 有相同的情况,因此需要URL和方法名组合来区分,注意一种特殊情况,
                //因为是使用Ant语法来匹配URL,如果出现请求方法相同且URL是匹配子集关系时,要把最具体的URL放在
                //前面,比如同是 GET 方法,Api1:/api/user/paged 和 Api2:/api/user/{id},在Ant语法里面
                //Api2是能匹配Api1的,如果用户有Api2的权限而没有Api1的权限,在如下初始化权限时Api2初始化的
                //顺序在Api1的前面,就会导致用户即使没有Api1的权限也能访问,因此要确保数据库加载的时候Api1在Api2
                //前面
                if (HttpMethod.POST.name().equals(rule.getMethod().name())) {
                    http
                            .authorizeRequests()
                            .antMatchers(HttpMethod.POST, rule.getPattern()).hasAuthority(rule.getSystemName());
                } else if (HttpMethod.GET.name().equals(rule.getMethod().name())) {
                    http
                            .authorizeRequests()
                            .antMatchers(HttpMethod.GET, rule.getPattern()).hasAuthority(rule.getSystemName());
                } else if (HttpMethod.PUT.name().equals(rule.getMethod().name())) {
                    http
                            .authorizeRequests()
                            .antMatchers(HttpMethod.PUT, rule.getPattern()).hasAuthority(rule.getSystemName());
                } else if (HttpMethod.DELETE.name().equals(rule.getMethod().name())) {
                    http
                            .authorizeRequests()
                            .antMatchers(HttpMethod.DELETE, rule.getPattern()).hasAuthority(rule.getSystemName());
                }
            }
        } catch (Exception e) {
            log.error("", e);
        }

        http
            .csrf().disable() // We don't need CSRF for JWT based authentication
            .exceptionHandling().authenticationEntryPoint(this.authenticationEntryPoint)
                //自定义没有权限时的处理,一般根据业务封装自定义的返回结果
                .accessDeniedHandler(ajaxAccessDeniedHandler)

            .and()
                //Restful API 完全无状态,使用JWT token
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
                //定义哪些API不需要认证,比如获取 Token 的接口
                .authorizeRequests()
                .antMatchers(permitAllEndpointList.toArray(new String[permitAllEndpointList.size()])).permitAll()
            .and()
                // Protected API End-points
                .authorizeRequests()
                .antMatchers(API_ROOT_URL, API_ATTACHMENT_URL).authenticated()
            .and()
                //处理跨域过滤器
                .addFilterBefore(new CustomCorsFilter(), UsernamePasswordAuthenticationFilter.class)
                //处理登录获取token的过滤器
                .addFilterBefore(buildAjaxLoginProcessingFilter(AUTHENTICATION_URL), UsernamePasswordAuthenticationFilter.class)
                //验证用户token,以及用户是否有接口访问权限的过滤器
                .addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(permitAllEndpointList, Arrays.asList(API_ROOT_URL,API_ATTACHMENT_URL)), UsernamePasswordAuthenticationFilter.class);
    }

具体解释见代码上的注释

@EnableWebSecurity

我们自己定义的配置类 WebSecurityConfig 加上了 @EnableWebSecurity 注解,同时继承了 WebSecurityConfigurerAdapter。你可能会在想谁的作用大一点,毫无疑问 @EnableWebSecurity起到决定性的配置作用,它其实是个组合注解。

@Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME)
@Target(value = { java.lang.annotation.ElementType.TYPE })
@Documented
@Import({ WebSecurityConfiguration.class,// <2>
        SpringWebMvcImportSelector.class })// <1>
@EnableGlobalAuthentication // <3>
@Configuration
public @interface EnableWebSecurity {
    boolean debug() default false;
}
  • <1> @Importspringboot 提供的用于引入外部的配置的注解,可以理解为:@EnableWebSecurity 注解激活了 @Import 注解中包含的配置类。
  • <2> WebSecurityConfiguration 顾名思义,是用来配置 web 安全的,下面的小节会详细介绍。
  • <3> @EnableGlobalAuthentication 注解的源码如下:
@Import(AuthenticationConfiguration.class)
@Configuration
public @interface EnableGlobalAuthentication {
}

注意点同样在 @Import 之中,它实际上激活了 AuthenticationConfiguration 这样的一个配置类,用来配置认证相关的核心类。

也就是说:@EnableWebSecurity完成的工作便是加载了WebSecurityConfigurationAuthenticationConfiguration 这两个核心配置类,也就此将 spring security 的职责划分为了配置安全信息,配置认证信息两部分。

WebSecurityConfiguration

在这个配置类中,有一个非常重要的Bean被注册了。

@Configuration
public class WebSecurityConfiguration {
    //DEFAULT_FILTER_NAME = "springSecurityFilterChain"
    @Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
    public Filter springSecurityFilterChain() throws Exception {
        ...
    }
 }

在未使用springboot之前,大多数人都应该对springSecurityFilterChain这个名词不会陌生,他是spring security的核心过滤器,是整个认证的入口。在曾经的XML配置中,想要启用spring security,需要在web.xml中进行如下配置:

<!-- Spring Security -->
   <filter>
       <filter-name>springSecurityFilterChain</filter-name>
       <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
   </filter>

   <filter-mapping>
       <filter-name>springSecurityFilterChain</filter-name>
       <url-pattern>/*</url-pattern>
   </filter-mapping>

而在springboot集成之后,这样的XMLjava配置取代。WebSecurityConfiguration中完成了声明springSecurityFilterChain的作用,并且最终交给DelegatingFilterProxy这个代理类,负责拦截请求(注意DelegatingFilterProxy这个类不是spring security包中的,而是存在于web包中,spring使用了代理模式来实现安全过滤的解耦)。

AuthenticationConfiguration

@Configuration
@Import(ObjectPostProcessorConfiguration.class)
public class AuthenticationConfiguration {
    @Bean
    public AuthenticationManagerBuilder authenticationManagerBuilder(
            ObjectPostProcessor<Object> objectPostProcessor) {
        return new AuthenticationManagerBuilder(objectPostProcessor);
    }
    public AuthenticationManager getAuthenticationManager() throws Exception {
        ...
    }
}

AuthenticationConfiguration 的主要任务,便是负责生成全局的身份认证管理者 AuthenticationManager。还记得在《初识 Spring Security 篇一》中,介绍了 Spring Security 的认证体系,AuthenticationManager 便是最核心的身份认证管理器。

WebSecurityConfigurerAdapter

适配器模式在 spring 中被广泛的使用,在配置中使用 Adapter 的好处便是,我们可以选择性的配置想要修改的那一部分配置,而不用覆盖其他不相关的配置。WebSecurityConfigurerAdapter 中我们可以选择自己想要修改的内容,来进行重写,而其提供了三个 configure 重载方法,是我们主要关心的:

    /**WebSecurityConfigurerAdapter**/

    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        this.disableLocalConfigureAuthenticationBldr = true;
    }
    ...
        /**
     * Override this method to configure {@link WebSecurity}. For example, if you wish to
     * ignore certain requests.
     */
    public void configure(WebSecurity web) throws Exception {
    }
    /**
     * Override this method to configure the {@link HttpSecurity}. Typically subclasses
     * should not invoke this method by calling super as it may override their
     * configuration. The default configuration is:
     *
     * <pre>
     * http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();
     * </pre>
     *
     * @param http the {@link HttpSecurity} to modify
     * @throws Exception if an error occurs
     */
    // @formatter:off
    protected void configure(HttpSecurity http) throws Exception {
        logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");

        http
            .authorizeRequests()
                .anyRequest().authenticated()
                .and()
            .formLogin().and()
            .httpBasic();
    }

由参数就可以知道,分别是对 AuthenticationManagerBuilderWebSecurityHttpSecurity进行个性化的配置。

HttpSecurity常用配置

@Configuration
@EnableWebSecurity
public class CustomWebSecurityConfig extends WebSecurityConfigurerAdapter {
  
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/resources/**", "/signup", "/about").permitAll()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")
                .anyRequest().authenticated()
                .and()
            .formLogin()
                .usernameParameter("username")
                .passwordParameter("password")
                .failureForwardUrl("/login?error")
                .loginPage("/login")
                .permitAll()
                .and()
            .logout()
                .logoutUrl("/logout")
                .logoutSuccessUrl("/index")
                .permitAll()
                .and()
            .httpBasic()
                .disable();
    }
}

上述是一个使用 Java Configuration 配置 HttpSecurity 的典型配置,其中 http 作为根开始配置,每一个 and() 对应了一个模块的配置(等同于xml配置中的结束标签),并且 and() 返回了 HttpSecurity 本身,于是可以连续进行配置。他们配置的含义也非常容易通过变量本身来推测

  • authorizeRequests()配置路径拦截,表明路径访问所对应的权限,角色,认证信息。
  • formLogin()对应表单认证相关的配置
  • logout()对应了注销相关的配置
  • httpBasic()可以配置basic登录

他们分别代表了 http 请求相关的安全配置,这些配置项无一例外的返回了 Configurer 类,而所有的 http 相关配置可以通过查看HttpSecurity的主要方法得知:

    public LogoutConfigurer<HttpSecurity> logout() throws Exception {
        return getOrApply(new LogoutConfigurer<HttpSecurity>());
    }
    ......
        public CsrfConfigurer<HttpSecurity> csrf() throws Exception {
        ApplicationContext context = getContext();
        return getOrApply(new CsrfConfigurer<HttpSecurity>(context));
    }

需要对 http 协议有一定的了解才能完全掌握所有的配置,不过,springbootspring security的自动配置已经足够使用了。其中每一项 Configurer(e.g.FormLoginConfigurer,CsrfConfigurer)都是 HttpConfigurer 的细化配置项。

WebSecurityBuilder

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    public void configure(WebSecurity web) throws Exception {
        web
            .ignoring()
            .antMatchers("/resources/**");
    }
}

以笔者的经验,这个配置中并不会出现太多的配置信息。

AuthenticationManagerBuilder

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
            .inMemoryAuthentication()
            .withUser("admin").password("admin").roles("USER");
    }
}

想要在 WebSecurityConfigurerAdapter 中进行认证相关的配置,可以使用 configure(AuthenticationManagerBuilder auth) 暴露一个 AuthenticationManager 的建造器:AuthenticationManagerBuilder 。如上所示,我们便完成了内存中用户的配置。

细心的朋友会发现,在前面的文章中我们配置内存中的用户时,似乎不是这么配置的,而是:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth
            .inMemoryAuthentication()
                .withUser("admin").password("admin").roles("USER");
    }
}

如果你的应用只有唯一一个 WebSecurityConfigurerAdapter ,那么他们之间的差距可以被忽略,从方法名可以看出两者的区别:使用@Autowired注入的 AuthenticationManagerBuilder 是全局的身份认证器,作用域可以跨越多个WebSecurityConfigurerAdapter,以及影响到基于Method的安全控制;而 protected configure()的方式则类似于一个匿名内部类,它的作用域局限于一个WebSecurityConfigurerAdapter内部。关于这一点的区别,可以参考我曾经提出的issuespring-security#issues4571。官方文档中,也给出了配置多个WebSecurityConfigurerAdapter的场景以及demo,将在该系列的后续文章中解读

原文链接:http://blog.didispace.com/xjf-spring-security-4/

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

推荐阅读更多精彩内容