问题描述
我需要将 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