Java Spring Spring Security

Spring Security即将弃用配置类WebSecurityConfigurerAdapter

用过WebSecurityConfigurerAdapter的都知道对Spring Security十分重要,总管Spring Security的配置体系。但是马上这个类要废了,这个类将在5.7版本被@Deprecated所标记了,未来这个类将被移除。
既然马上要弃用了,总要有个过渡方案或者新玩法吧。这里把整套的替代方案再搞一遍。
版本需要Spring Security 5.4.x及以上。

HttpSecurity新旧玩法对比

旧玩法:

  1. @Configuration
  2. static class SecurityConfig extends WebSecurityConfigurerAdapter {
  3. @Override
  4. protected void configure(HttpSecurity http) throws Exception {
  5. http
  6. .antMatcher("/**")
  7. .authorizeRequests(authorize -> authorize
  8. .anyRequest().authenticated()
  9. );
  10. }
  11. }

新玩法:

  1. @Bean
  2. SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
  3. return http
  4. .antMatcher("/**")
  5. .authorizeRequests(authorize -> authorize
  6. .anyRequest().authenticated()
  7. )
  8. .build();
  9. }

WebSecurity新旧玩法对比

使用WebSecurity.ignoring()忽略某些URL请求,这些请求将被Spring Security忽略,这意味着这些URL将有受到 CSRF、XSS、Clickjacking 等攻击的可能。以下示例仅仅作为演示,请勿使用在生产环境。是不是又学到了呢?
旧玩法:

  1. @Configuration
  2. public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
  3. @Override
  4. public void configure(WebSecurity web) {
  5. // 仅仅作为演示
  6. web.ignoring().antMatchers("/ignore1", "/ignore2");
  7. }
  8. }

新玩法:

  1. @Configuration
  2. public class SecurityConfiguration {
  3. @Bean
  4. public WebSecurityCustomizer webSecurityCustomizer() {
  5. // 仅仅作为演示
  6. return (web) -> web.ignoring().antMatchers("/ignore1", "/ignore2");
  7. }
  8. }

如果需要忽略URL,请考虑通过HttpSecurity.authorizeHttpRequestspermitAll来实现。

AuthenticationManager新旧玩法对比

AuthenticationManager配置主要分为全局的(Global )、本地的(Local)。

旧玩法

  1. @Configuration
  2. public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
  3. @Override
  4. protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  5. auth.jdbcAuthentication();
  6. }
  7. }

上面是通过WebSecurityConfigurerAdapter开启的是本地配置。开启全局配置需要覆写其authenticationManagerBean()方法并标记为Bean:

  1. @Bean(name name="myAuthenticationManager")
  2. @Override
  3. public AuthenticationManager authenticationManagerBean() throws Exception {
  4. return super.authenticationManagerBean();
  5. }

新玩法

本地配置通过HttpSecurity.authenticationManager实现:

  1. @Configuration
  2. public class SecurityConfiguration {
  3. @Bean
  4. public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
  5. http
  6. .authorizeHttpRequests((authz) -> authz
  7. .anyRequest().authenticated()
  8. )
  9. .httpBasic(withDefaults())
  10. .authenticationManager(new CustomAuthenticationManager());
  11. }
  12. }

全局配置摆脱了依赖WebSecurityConfigurerAdapter.authenticationManagerBean()方法,只需要定义一个AuthenticationManager类型的Bean即可:

  1. @Bean
  2. AuthenticationManager ldapAuthenticationManager(
  3. BaseLdapPathContextSource contextSource) {
  4. LdapBindAuthenticationManagerFactory factory =
  5. new LdapBindAuthenticationManagerFactory(contextSource);
  6. factory.setUserDnPatterns("uid={0},ou=people");
  7. factory.setUserDetailsContextMapper(new PersonContextMapper());
  8. return factory.createAuthenticationManager();
  9. }

当然还可以通过自定义GlobalAuthenticationConfigurerAdapter并注入Spring IoC来修改AuthenticationManagerBuilder,不限制数量,但是要注意有排序问题。相关的思维导图:
Spring Security的配置机制 - 图1

Spring Security 5.4版本带来的新玩法

在以往Spring Security的教程中自定义配置都是声明一个配置类WebSecurityConfigurerAdapter,然后覆写(@Override)对应的几个方法就行了。然而这一切在Spring Security 5.4开始就得到了改变,从Spring Security 5.4 起不需要继承WebSecurityConfigurerAdapter就可以配置HttpSecurity 了。相关的说明原文:

  • Remove need for WebSecurityConfigurerAdapter #8805
  • Configure HTTP Security without extending WebSecurityConfigurerAdapter #8804

    新的配置方式

    旧的配置方式目前依然有效
    1. @Configuration
    2. static class SecurityConfig extends WebSecurityConfigurerAdapter {
    3. @Override
    4. protected void configure(HttpSecurity http) throws Exception {
    5. http
    6. .antMatcher("/**")
    7. .authorizeRequests(authorize -> authorize
    8. .anyRequest().authenticated()
    9. );
    10. }
    11. }
    5.4.x版本有新的选择:
    1. @Bean
    2. SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    3. return http
    4. .antMatcher("/**")
    5. .authorizeRequests(authorize -> authorize
    6. .anyRequest().authenticated()
    7. )
    8. .build();
    9. }
    这种JavaConfig的方式看起来更加清爽舒服,而且和适配器解耦了。上面filterChain方法的参数是HttpSecurity类型。熟悉@Bean注解的同学应该会意识到一定有一个HttpSecurity类型的Spring Bean。没错!就在HttpSecurityConfiguration中有一个这样的Bean
    1. @Bean({"org.springframework.security.config.annotation.web.configuration.HttpSecurityConfiguration.httpSecurity"})
    2. @Scope("prototype")
    3. HttpSecurity httpSecurity() throws Exception {
    4. // 省略掉
    5. return http;
    6. }
    初始化的内容已经忽略掉,它不是本文关注的重点。可以注意到HttpSecurity@Scope("prototype")标记。也就是这个HttpSecurity Bean不是单例的,每一次请求都会构造一个新的实例。这个设定非常方便构建多个互相没有太多关联的SecurityFilterChain,进而能在一个安全体系中构建相互隔离的安全策略。比如后端管理平台用Session模式,前台应用端用Token模式。多个SecurityFilterChain

    原理

    Spring Security 有一个名为springSecurityFilterChain默认的过滤器链类(实际位置就是上图的 Bean Filter位置),其类型为FilterChainProxy, 它代理了所有的SecurityFilterChain,关键的代理注入代码:
    1. for (SecurityFilterChain securityFilterChain : this.securityFilterChains) {
    2. this.webSecurity.addSecurityFilterChainBuilder(() -> securityFilterChain);
    3. for (Filter filter : securityFilterChain.getFilters()) {
    4. if (filter instanceof FilterSecurityInterceptor) {
    5. this.webSecurity.securityInterceptor((FilterSecurityInterceptor) filter);
    6. break;
    7. }
    8. }
    9. }
    那么this.securityFilterChains来自哪里呢?
    1. @Autowired(required = false)
    2. void setFilterChains(List<SecurityFilterChain> securityFilterChains) {
    3. securityFilterChains.sort(AnnotationAwareOrderComparator.INSTANCE);
    4. this.securityFilterChains = securityFilterChains;
    5. }
    到这里就一目了然了吧,SecurityFilterChain类型的Bean会被加载到this.securityFilterChains中。如果Spring Security 版本升级到 5.4.x,就可以尝试一下这种方式。

    Spring Security的配置机制

    Spring Security 5.4版本开始会提供一个原型范围的HttpSecurity来构建过滤器链SecurityFilterChain
    1. @Bean(HTTPSECURITY_BEAN_NAME)
    2. @Scope("prototype")
    3. HttpSecurity httpSecurity() throws Exception {
    4. WebSecurityConfigurerAdapter.LazyPasswordEncoder passwordEncoder = new WebSecurityConfigurerAdapter.LazyPasswordEncoder(
    5. this.context);
    6. AuthenticationManagerBuilder authenticationBuilder = new WebSecurityConfigurerAdapter.DefaultPasswordEncoderAuthenticationManagerBuilder(
    7. this.objectPostProcessor, passwordEncoder);
    8. authenticationBuilder.parentAuthenticationManager(authenticationManager());
    9. HttpSecurity http = new HttpSecurity(this.objectPostProcessor, authenticationBuilder, createSharedObjects());
    10. // @formatter:off
    11. http
    12. .csrf(withDefaults())
    13. .addFilter(new WebAsyncManagerIntegrationFilter())
    14. .exceptionHandling(withDefaults())
    15. .headers(withDefaults())
    16. .sessionManagement(withDefaults())
    17. .securityContext(withDefaults())
    18. .requestCache(withDefaults())
    19. .anonymous(withDefaults())
    20. .servletApi(withDefaults())
    21. .apply(new DefaultLoginPageConfigurer<>());
    22. http.logout(withDefaults());
    23. // @formatter:on
    24. return http;
    25. }
    这里会构建基于原型的HttpSecurityBean,并且初始化了一些默认配置供使用。涉及Spring Security的日常开发都是围绕这个类进行的,所以这个类是学习Spring Security的重中之重。
    基于原型(prototype)的Spring Bean的一个典型应用场景,

    基本配置

    日常使用的一些配置项如下:
方法 说明
requestMatchers() 为SecurityFilterChain提供URL拦截策略,具体还提供了antMatcher和mvcMathcer
openidLogin() 用于基于 OpenId 的验证
headers() 将安全标头添加到响应,比如说简单的 XSS 保护
cors() 配置跨域资源共享( CORS )
sessionManagement() 配置会话管理
portMapper() 配置一个PortMapper(HttpSecurity#(getSharedObject(class))),其他提供SecurityConfigurer的对象使用 PortMapper 从 HTTP 重定向到 HTTPS 或者从 HTTPS 重定向到 HTTP。默认情况下,Spring Security使用一个PortMapperImpl映射 HTTP 端口8080到 HTTPS 端口8443,HTTP 端口80到 HTTPS 端口443
jee() 配置基于容器的预认证。在这种情况下,认证由Servlet容器管理
x509() 配置基于x509的预认证
rememberMe 配置“记住我”的验证
authorizeRequests() 基于使用HttpServletRequest限制访问
requestCache() 配置请求缓存
exceptionHandling() 配置错误处理
securityContext() 在HttpServletRequests之间的SecurityContextHolder上设置SecurityContext的管理。当使用WebSecurityConfigurerAdapter时,这将自动应用
servletApi() 将HttpServletRequest方法与在其上找到的值集成到SecurityContext中。当使用WebSecurityConfigurerAdapter时,这将自动应用
csrf() 添加 CSRF 支持,使用WebSecurityConfigurerAdapter时,默认启用
logout() 添加退出登录支持。当使用WebSecurityConfigurerAdapter时,这将自动应用。默认情况是,访问URL”/ logout”,使HTTP Session无效来清除用户,清除已配置的任何#rememberMe()身份验证,清除SecurityContextHolder,然后重定向到/login?success
anonymous() 配置匿名用户的表示方法。当与WebSecurityConfigurerAdapter结合使用时,这将自动应用。默认情况下,匿名用户将使用org.springframework.security.authentication.AnonymousAuthenticationToken表示,并包含角色 ROLE_ANONYMOUS
authenticationManager() 配置AuthenticationManager
authenticationProvider() 添加AuthenticationProvider
formLogin() 指定支持基于表单的身份验证。如果未指定FormLoginConfigurer#loginPage(String),则将生成默认登录页面
oauth2Login() 根据外部OAuth 2.0或OpenID Connect 1.0提供程序配置身份验证
oauth2Client() OAuth2.0 客户端相关的配置
oauth2ResourceServer() OAuth2.0资源服务器相关的配置
requiresChannel() 配置通道安全。为了使该配置有用,必须提供至少一个到所需信道的映射
httpBasic() 配置 Http Basic 验证
addFilter() 添加一个已经在内置过滤器注册表注册过的过滤器实例或者子类
addFilterBefore() 在指定的Filter类之前添加过滤器
addFilterAt() 在指定的Filter类的位置添加过滤器
addFilterAfter() 在指定的Filter类的之后添加过滤器
and() 连接以上策略的连接器,用来组合安全策略。实际上就是”而且”的意思

高级玩法

建议先把上面的基本玩法有选择的弄明白,然后有精力的话去研究下HttpSecurity的高级玩法。

apply

这个方法用来把其它的一些配置合并到当前的配置中去,形成插件化,支持SecurityConfigurerAdapter或者SecurityConfigurer的实现。其实内置的一些配置都是以这种形式集成到HttpSecurity中去的。例如文章开头的配置中有默认登录页面相关的配置:

  1. httpSecurity.apply(new DefaultLoginPageConfigurer<>());

objectPostProcessor

配置一个自定义ObjectPostProcessorObjectPostProcessor可以改变某些配置内部的机制,这些配置往往不直接对外提供操作接口。

获取、移除配置类

getConfigurer用来获取已经apply的配置类;getConfigurers用来获取已经apply某个类型的所有配置类。

配置、获取SharedObject

SharedObject是在配置中进行共享的一些对象,HttpSecurity共享了一些非常有用的对象可以供各个配置之间共享,比如AuthenticationManager。相关的方法有setSharedObjectgetSharedObjectgetSharedObjects

获取SecurityFilterChain

HttpSecurity也提供了构建目标对象SecurityFilterChain的实例的方法。可以通过build()来对配置进行初次构建;也可以通过getObject()来获取已经构建的实例;甚至可以使用getOrBuild()来进行直接获取实例或者构建实例。
所以新的配置都是这样的:

  1. @Bean
  2. SecurityFilterChain securityFilterChain (HttpSecurity http) {
  3. http.cors();
  4. return http.build();
  5. }

记住每一个HttpSecurity只能被构建成功一次。