Spring Boot SAML 2 身份验证对象为空

问题描述

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

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

SAML 身份验证效果很好,问题是当用户重定向回成功 URL 时,即 https://my-domain/as/auth/login 然后身份验证对象为空,因为在成功身份验证后 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("/");
  }

解决方法

终于发现问题了。

在 Zuul 中,sensitiveHeaders 的默认值为 Cookie,Set-Cookie,Authorization

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

必须将sensitiveHeaders 值设置为空,以便将cookie 传递给服务。 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

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...