Spring boot SAML 2 身份验证对象 null

Posted

技术标签:

【中文标题】Spring boot SAML 2 身份验证对象 null【英文标题】:Spring boot SAML 2 authentication object null 【发布时间】:2021-03-31 13:56:47 【问题描述】:

我需要将 SAML 身份验证与 REST API 集成,以便我可以使我的 REST 服务无状态,我采取的方法如下

在 zuul 代理后面开发了一个身份验证服务,该服务在 AWS ALB 后面运行 用户尝试通过端点 https://my-domain/as/auth/login 生成令牌 由于用户未登录,因此他被重定向到他进行身份验证的 IDP 身份验证后,IDP 将用户重定向回我的服务,即 URL https://my-domain/as/auth/login 我检查用户身份验证主体,如果用户通过身份验证,则生成 JWT 令牌并将其返回给用户

SAML 身份验证运行良好,问题是当用户被重定向回成功 URL 即 https://my-domain/as/auth/login 时,身份验证对象为 null,因为 SecurityContextHolder 在身份验证成功后被清除,并且 401 处理程序启动并将用户重定向到循环中的 IDP在 SAML 断言失败之前,请指出我错在哪里

我的 Zuul 代理配置如下所示

zuul:
   ribbon:
      eager-load:
         enabled: true
   host:
      connect-timeout-millis: 5000000
      socket-timeout-millis: 5000000
   ignoredServices: ""
   ignoredPatterns:
      - /as/*
   routes:
      sensitiveHeaders: Cookie,Set-Cookie
      import-data-service:
         path: /ids/*
         serviceId: IDS
         stripPrefix: true
      authentication-service:
         path: /as/*
         serviceId: AS
         stripPrefix: true
         customSensitiveHeaders: false

我的 SAML 安全配置如下所示

@Configuration
public class SamlSecurityConfig extends WebSecurityConfigurerAdapter 

  @Bean
  public WebSSOProfileOptions defaultWebSSOProfileOptions() 
    WebSSOProfileOptions webSSOProfileOptions = new WebSSOProfileOptions();
    webSSOProfileOptions.setIncludeScoping(false);
    webSSOProfileOptions.setBinding(SAMLConstants.SAML2_POST_BINDING_URI);
    return webSSOProfileOptions;
  

  @Bean
  public SAMLEntryPoint samlEntryPoint() 
    SAMLEntryPoint samlEntryPoint = new SAMLEntryPoint();
    samlEntryPoint.setDefaultProfileOptions(defaultWebSSOProfileOptions());
    return samlEntryPoint;
  

  @Bean
  public MetadataDisplayFilter metadataDisplayFilter() 
    return new MetadataDisplayFilter();
  

  @Bean
  public SimpleUrlAuthenticationFailureHandler authenticationFailureHandler() 
    return new SimpleUrlAuthenticationFailureHandler();
  


  @Bean
  public SavedRequestAwareAuthenticationSuccessHandler successRedirectHandler() 
    SavedRequestAwareAuthenticationSuccessHandler successRedirectHandler =
        new SavedRequestAwareAuthenticationSuccessHandler();
    successRedirectHandler.setDefaultTargetUrl("https://my-domain/as/saml/SSO");
    return successRedirectHandler;
  

  @Bean
  public SessionRepositoryFilter sessionFilter() 
    HttpSessionStrategy cookieStrategy = new CookieHttpSessionStrategy();
    MapSessionRepository repository = new MapSessionRepository();
    ((CookieHttpSessionStrategy) cookieStrategy).setCookieName("JSESSIONID");
    SessionRepositoryFilter sessionRepositoryFilter = new SessionRepositoryFilter(repository);
    sessionRepositoryFilter.setHttpSessionStrategy(cookieStrategy);
    return sessionRepositoryFilter;
  


  @Bean
  public SAMLProcessingFilter samlWebSSOProcessingFilter() throws Exception 
    SAMLProcessingFilter samlWebSSOProcessingFilter = new SAMLProcessingFilter();
    samlWebSSOProcessingFilter.setAuthenticationManager(authenticationManager());
    samlWebSSOProcessingFilter.setAuthenticationSuccessHandler(successRedirectHandler());
    samlWebSSOProcessingFilter.setAuthenticationFailureHandler(authenticationFailureHandler());
    return samlWebSSOProcessingFilter;
  


  @Bean
  public HttpStatusReturningLogoutSuccessHandler successLogoutHandler() 
    return new HttpStatusReturningLogoutSuccessHandler();
  

  @Bean
  public SecurityContextLogoutHandler logoutHandler() 
    SecurityContextLogoutHandler logoutHandler = new SecurityContextLogoutHandler();
    logoutHandler.setInvalidateHttpSession(true);
    logoutHandler.setClearAuthentication(true);
    return logoutHandler;
  

  @Bean
  public SAMLLogoutFilter samlLogoutFilter() 
    return new SAMLLogoutFilter(successLogoutHandler(), new LogoutHandler[] logoutHandler(),
        new LogoutHandler[] logoutHandler());
  

  @Bean
  public SAMLLogoutProcessingFilter samlLogoutProcessingFilter() 
    return new SAMLLogoutProcessingFilter(successLogoutHandler(), logoutHandler());
  

  @Bean
  public MetadataGeneratorFilter metadataGeneratorFilter() 
    return new MetadataGeneratorFilter(metadataGenerator());
  

  @Bean
  public MetadataGenerator metadataGenerator() 
    MetadataGenerator metadataGenerator = new MetadataGenerator();
    metadataGenerator.setEntityId("entityUniqueIdenifier");
    metadataGenerator.setExtendedMetadata(extendedMetadata());
    metadataGenerator.setIncludeDiscoveryExtension(false);
    metadataGenerator.setRequestSigned(true);
    metadataGenerator.setKeyManager(keyManager());
    metadataGenerator.setEntityBaseURL("https://my-domain/as");
    return metadataGenerator;
  

  @Bean
  public KeyManager keyManager() 
    ClassPathResource storeFile = new ClassPathResource("/saml-keystore.jks");
    String storePass = "samlstorepass";
    Map<String, String> passwords = new HashMap<>();
    passwords.put("mykeyalias", "mykeypass");
    return new JKSKeyManager(storeFile, storePass, passwords, "mykeyalias");
  

  @Bean
  public ExtendedMetadata extendedMetadata() 
    ExtendedMetadata extendedMetadata = new ExtendedMetadata();
    extendedMetadata.setIdpDiscoveryEnabled(false);
    extendedMetadata.setSignMetadata(false);
    extendedMetadata.setSigningKey("mykeyalias");
    extendedMetadata.setEncryptionKey("mykeyalias");
    return extendedMetadata;
  

  @Bean
  public FilterChainProxy samlFilter() throws Exception 
    List<SecurityFilterChain> chains = new ArrayList<>();

    chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/metadata/**"),
        metadataDisplayFilter()));

    chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/login/**"),
        samlEntryPoint()));

    chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SSO/**"),
        samlWebSSOProcessingFilter()));

    chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SSOHoK/**"),
        samlWebSSOHoKProcessingFilter()));

    chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/logout/**"),
        samlLogoutFilter()));

    chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SingleLogout/**"),
        samlLogoutProcessingFilter()));

    return new FilterChainProxy(chains);
  

  @Bean
  public TLSProtocolConfigurer tlsProtocolConfigurer() 
    return new TLSProtocolConfigurer();
  

  @Bean
  public ProtocolSocketFactory socketFactory() 
    return new TLSProtocolSocketFactory(keyManager(), null, "default");
  

  @Bean
  public Protocol socketFactoryProtocol() 
    return new Protocol("https", socketFactory(), 443);
  

  @Bean
  public MethodInvokingFactoryBean socketFactoryInitialization() 
    MethodInvokingFactoryBean methodInvokingFactoryBean = new MethodInvokingFactoryBean();
    methodInvokingFactoryBean.setTargetClass(Protocol.class);
    methodInvokingFactoryBean.setTargetMethod("registerProtocol");
    Object[] args = "https", socketFactoryProtocol();
    methodInvokingFactoryBean.setArguments(args);
    return methodInvokingFactoryBean;
  

  @Bean
  public VelocityEngine velocityEngine() 
    return VelocityFactory.getEngine();
  

  @Bean(initMethod = "initialize")
  public StaticBasicParserPool parserPool() 
    return new StaticBasicParserPool();
  

  @Bean(name = "parserPoolHolder")
  public ParserPoolHolder parserPoolHolder() 
    return new ParserPoolHolder();
  

  @Bean
  public HTTPPostBinding httpPostBinding() 
    return new HTTPPostBinding(parserPool(), velocityEngine());
  

  @Bean
  public HTTPRedirectDeflateBinding httpRedirectDeflateBinding() 
    return new HTTPRedirectDeflateBinding(parserPool());
  

  @Bean
  public SAMLProcessorImpl processor() 
    Collection<SAMLBinding> bindings = new ArrayList<>();
    bindings.add(httpRedirectDeflateBinding());
    bindings.add(httpPostBinding());
    return new SAMLProcessorImpl(bindings);
  

  @Bean
  public HttpClient httpClient() 
    return new HttpClient(multiThreadedHttpConnectionManager());
  

  @Bean
  public MultiThreadedHttpConnectionManager multiThreadedHttpConnectionManager() 
    return new MultiThreadedHttpConnectionManager();
  

  @Bean
  public static SAMLBootstrap sAMLBootstrap() 
    return new CustomSamlBootStrap();
  

  @Bean
  public SAMLDefaultLogger samlLogger() 
    SAMLDefaultLogger samlDefaultLogger = new SAMLDefaultLogger();
    samlDefaultLogger.setLogAllMessages(true);
    samlDefaultLogger.setLogErrors(true);
    return samlDefaultLogger;
  

  @Bean
  public SAMLContextProviderImpl contextProvider() 
    SAMLContextProviderLB samlContextProviderLB = new SAMLContextProviderLB();
    samlContextProviderLB.setServerName("my-domain/as");
    samlContextProviderLB.setScheme("https");
    samlContextProviderLB.setServerPort(443);
    samlContextProviderLB.setIncludeServerPortInRequestURL(false);
    samlContextProviderLB.setContextPath("");
    // samlContextProviderLB.setStorageFactory(new EmptyStorageFactory());
    return samlContextProviderLB;
  

  // SAML 2.0 WebSSO Assertion Consumer
  @Bean
  public WebSSOProfileConsumer webSSOprofileConsumer() 
    return new WebSSOProfileConsumerImpl();
  

  // SAML 2.0 Web SSO profile
  @Bean
  public WebSSOProfile webSSOprofile() 
    return new WebSSOProfileImpl();
  

  // not used but autowired...
  // SAML 2.0 Holder-of-Key WebSSO Assertion Consumer
  @Bean
  public WebSSOProfileConsumerHoKImpl hokWebSSOprofileConsumer() 
    return new WebSSOProfileConsumerHoKImpl();
  

  // not used but autowired...
  // SAML 2.0 Holder-of-Key Web SSO profile
  @Bean
  public WebSSOProfileConsumerHoKImpl hokWebSSOProfile() 
    return new WebSSOProfileConsumerHoKImpl();
  

  @Bean
  public SingleLogoutProfile logoutprofile() 
    return new SingleLogoutProfileImpl();
  

  @Bean
  public ExtendedMetadataDelegate idpMetadata()
      throws MetadataProviderException, ResourceException 

    Timer backgroundTaskTimer = new Timer(true);

    ResourceBackedMetadataProvider resourceBackedMetadataProvider =
        new ResourceBackedMetadataProvider(backgroundTaskTimer,
            new ClasspathResource("/IDP-metadata.xml"));

    resourceBackedMetadataProvider.setParserPool(parserPool());

    ExtendedMetadataDelegate extendedMetadataDelegate =
        new ExtendedMetadataDelegate(resourceBackedMetadataProvider, extendedMetadata());
    extendedMetadataDelegate.setMetadataTrustCheck(true);
    extendedMetadataDelegate.setMetadataRequireSignature(false);
    return extendedMetadataDelegate;
  

  @Bean
  @Qualifier("metadata")
  public CachingMetadataManager metadata() throws MetadataProviderException, ResourceException 
    List<MetadataProvider> providers = new ArrayList<>();
    providers.add(idpMetadata());
    return new CachingMetadataManager(providers);
  

  @Bean
  public SAMLUserDetailsService samlUserDetailsService() 
    return new SamlUserDetailsServiceImpl();
  

 @Bean
  public SAMLWebSSOHoKProcessingFilter samlWebSSOHoKProcessingFilter() throws Exception 
    final SAMLWebSSOHoKProcessingFilter filter = new SAMLWebSSOHoKProcessingFilter();
    filter.setAuthenticationManager(authenticationManager());
    filter.setAuthenticationSuccessHandler(successRedirectHandler());
    filter.setAuthenticationFailureHandler(authenticationFailureHandler());
    return filter;
  


  @Bean
  public SAMLAuthenticationProvider samlAuthenticationProvider() 
    SAMLAuthenticationProvider samlAuthenticationProvider = new SAMLAuthenticationProvider();
    samlAuthenticationProvider.setUserDetails(samlUserDetailsService());
    samlAuthenticationProvider.setForcePrincipalAsString(false);
    return samlAuthenticationProvider;
  

  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception 
    auth.authenticationProvider(samlAuthenticationProvider());
  

  @Override
  protected void configure(HttpSecurity http) throws Exception 

    http.exceptionHandling().authenticationEntryPoint(samlEntryPoint());
    http.csrf().disable();
    http.addFilterBefore(metadataGeneratorFilter(), ChannelProcessingFilter.class)
        .addFilterAfter(samlFilter(), BasicAuthenticationFilter.class);

    http.authorizeRequests().antMatchers("/error").permitAll().antMatchers("/saml/**").permitAll()
        .anyRequest().authenticated();

    http.logout().logoutSuccessUrl("/");
  

【问题讨论】:

如果您提供mvce,您将有更大的机会收到答复 @s7vr 我已尝试将我尝试使其工作的所有配置都放入其中,因此希望在所有上下文中提供更好的理解 【参考方案1】:

终于找到问题了。

在 Zuul 中,sensitiveHeaders 有一个默认值Cookie,Set-Cookie,Authorization

现在,如果我们不设置属性本身,那么这些标头将被视为敏感的,并且不会向下游传输到我们的服务。

必须将sensitiveHeaders 值设置为空,以便将cookies 传递给服务。 Cookie 包含我们的 JSESSIONID,它标识了会话。

zuul:
   ribbon:
      eager-load:
         enabled: true
   host:
      connect-timeout-millis: 5000000
      socket-timeout-millis: 5000000
   ignoredServices: ""
   sensitiveHeaders: 
   ignoredPatterns:
      - /as/*
   routes:
      import-data-service:
         path: /ids/*
         serviceId: IDS
         stripPrefix: true
      authentication-service:
         path: /as/*
         serviceId: AS
         stripPrefix: true
         customSensitiveHeaders: false

【讨论】:

以上是关于Spring boot SAML 2 身份验证对象 null的主要内容,如果未能解决你的问题,请参考以下文章

基于 SAML 的 SSO 用于身份验证和 LDAP 用于授权 - Spring Boot Security

即使在 IDP 使用 SAML 成功登录后,获取身份验证对象仍为空

如何在 Spring Boot 中从 Azure SAML 2.0 应用程序获取角色或组

没有 Spring Boot 的 Spring Security SAML 身份元数据

Spring Saml FilterChainProxy 清除上下文 - 空身份验证

使用 Spring Security 在一个应用程序中结合数据库和 SAML 身份验证