The problem
Spring Security has a function that forces to redirect the http request to https.
The following is the example of setting of redirect:
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/**").hasRole("USER") .and() .formLogin() .and() .requiresChannel() .anyRequest().requiresSecure(); } ...
In this way, in case of protocol is http, request is forced to redirect to https.
It's quite convenient setting, but when the ELB(Elastic Load Balancing), which has a function of SSL accelerator, exists before application server(such as Tomcat, which Spring works on), it causes redirect loop. While the ssl communication is between client and ELB, ELB and Tomcat communicates with each other via http. Because of that, even if the client requests via ssl the tomcat misunderstand it as http request and try to redirect to https every time.
The approach
To fix this situation, there are 2 workarounds.
1. Cheat the decision logic of protocol of Spring Security
2. Change the decision logic of protocol of Spring Security
So, to begin with, how does Spring Security decide whether the request is via http or https?
The code below is the exact part of the logic.
An extract of a part of SecureChannelProcessor
package org.springframework.security.web.access.channel; ... public class SecureChannelProcessor implements InitializingBean, ChannelProcessor { ... public void decide(FilterInvocation invocation, Collection<ConfigAttribute> config) throws IOException, ServletException { Assert.isTrue((invocation != null) && (config != null), "Nulls cannot be provided"); for (ConfigAttribute attribute : config) { if (supports(attribute)) { if (!invocation.getHttpRequest().isSecure()) { entryPoint.commence(invocation.getRequest(), invocation.getResponse()); } } } } ...
In other words, Spring Security sees the result of HttpServletRequest#isSecure and decides if the communication is via ssl or not.
Cheat the decision logic of protocol of Spring Security
I used "cheat", but the point is, by middle-ware layer we solve this problem.
If we run the application on Tomcat, we can set the return value of HttpServletRequest#isSecure to true by setting secure="true" of the Connector element in server.xml.
<Connector port="8080" protocol="HTTP/1.1" proxyPort="443" scheme="https" secure="true" proxyName="myapp.example.com" connectionTimeout="20000" URIEncoding="UTF-8" redirectPort="8443" />
Change the decision logic of protocol of Spring Security
This pattern of workaround is to change the logic of Spring Security itself.
More specifically, we implement the subclass of SecureChannelProcessor and InsecureChannelProcessor which decide communications protocol by using X-Forwarded-Proto header.
About Elastic Load Balancing - X-Forwarded-Proto
X-Forwarded-Proto
TheX-Forwarded-Proto
request header helps you identify the protocol (HTTP or HTTPS) that a client used to connect to your server. Your server access logs contain only the protocol used between the server and the load balancer; they contain no information about the protocol used between the client and the load balancer. To determine the protocol used between the client and the load balancer, use theX-Forwarded-Proto
request header. Elastic Load Balancing stores the protocol used between the client and the load balancer in theX-Forwarded-Proto
request header and passes the header along to your server.
Example of implementing subclass of SecureChannelProcessor
public class ProxyInsecureChannelProcessor extends InsecureChannelProcessor { @Override public void decide(FilterInvocation invocation, Collection<ConfigAttribute> config) throws IOException, ServletException { if ((invocation == null) || (config == null)) { throw new IllegalArgumentException("Nulls cannot be provided"); } String forwardedProto = invocation.getHttpRequest().getHeader("X-Forwarded-Proto"); for (ConfigAttribute attribute : config) { if (supports(attribute)) { if (forwardedProto != null) { if (forwardedProto.equals("https")) { getEntryPoint().commence(invocation.getRequest(), invocation.getResponse()); } } else { if (invocation.getHttpRequest().isSecure()) { getEntryPoint().commence(invocation.getRequest(), invocation.getResponse()); } } } } } }
Example of implementing subclass of InsecureChannelProcessor
public class ProxySecureChannelProcessor extends SecureChannelProcessor { @Override public void decide(FilterInvocation invocation, Collection<ConfigAttribute> config) throws IOException, ServletException { Assert.isTrue((invocation != null) && (config != null), "Nulls cannot be provided"); String forwardedProto = invocation.getHttpRequest().getHeader("X-Forwarded-Proto"); for (ConfigAttribute attribute : config) { if (supports(attribute)) { if (forwardedProto != null) { if (!forwardedProto.equals("https")) { getEntryPoint().commence(invocation.getRequest(), invocation.getResponse()); } } else { if (!invocation.getHttpRequest().isSecure()) { getEntryPoint().commence(invocation.getRequest(), invocation.getResponse()); } } } } } }
Then we describe setting of Spring Security to use those class.
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { List<ChannelProcessor> channelProcessors = new ArrayList<>(); channelProcessors.add(new ProxySecureChannelProcessor()); channelProcessors.add(new ProxyInsecureChannelProcessor()); http .authorizeRequests() .antMatchers("/**").hasRole("USER") .and() .formLogin() .and() .requiresChannel() .channelProcessors(channelProcessors) .anyRequest().requiresSecure(); ...
▼Verification environment
Spring Security 3.2.4.RELEASE