The problem
Spring Security には 通信を HTTPS に強制的にリダイレクトする機能があります。
以下はその設定例。
@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(); } ...
このように設定することでプロトコルが HTTP の場合に HTTPS にリダイレクトしてくれるようになります。
便利な設定ですが、Spring が動作する Tomcat 等の前段に ELB (Elasitc Load Balancing) などの SSL アクセラレータ機能のあるロードバランサが存在すると、クライアント <-> ELB 間は SSL 通信ですが、ELB <-> Tomcat 間は HTTP 通信になり、クライアントからの通信が SSL であったとしても HTTP と誤認してしまい、常に HTTPS にリダイレクトしようとしてしまいます。
The approach
このような状況を解決するために、次のようなアプローチが考えられます。
1. Spring Security のプロトコル判定ロジックをだます
2. Spring Security のプロトコル判定ロジックを変更する
ではそもそも Spring Security はどのようなロジックで通信が HTTP なのか SSL なのかを判断しているのでしょうか。
次のコードがその部分にあたります。
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()); } } } } ...
つまり Spring Security は HttpServletRequest#isSecure の結果をみて SSL 通信かどうかを判定しています。
Spring Security のプロトコル判定ロジックをだます
要はミドルウェア層で解決しようというもの。
もし、Tomcat で動作させている場合は、Tomcat の Server.xml の Connector 設定で secure="true" とすれば、HttpServletRequest#isSecure の戻り値を true にできる。
<Connector port="8080" protocol="HTTP/1.1" proxyPort="443" scheme="https" secure="true" proxyName="myapp.example.com" connectionTimeout="20000" URIEncoding="UTF-8" redirectPort="8443" />
Spring Security のプロトコル判定ロジックを変更する
Spring Security の判定ロジックそものを変更してしまおうといもの。
具体的には、X-Forwarded-Proto ヘッダを使って通信プロコトルを判断する、
SecureChannelProcessor と InsecureChannelProcessor のサブクラスを実装します。
Elastic Load Balancing - X-Forwarded-Proto について
X-Forwarded-Proto
X-Forwarded-Proto
リクエストヘッダーを使用すると、クライアントがサーバーへの接続に使用したプロトコル(HTTP または HTTPS)を識別することができます。サーバーアクセスログには、サーバーとロードバランサーの間で使用されたプロトコルのみが含まれ、クライアントとロードバランサーの間で使用されたプロトコルに関する情報は含まれません。クライアントとロードバランサーの間で使用されたプロトコルを判別するには、X-Forwarded-Proto
リクエストヘッダーを使用します。Elastic Load Balancing は、クライアントとロードバランサーの間で使用されたプロトコルをX-Forwarded-Proto
リクエストヘッダーに格納し、このヘッダーをサーバーに渡します。
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()); } } } } } }
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()); } } } } } }
上記2つのクラスを使用するように、Spring Security の設定を下記のように記述します。
@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(); ...
▼検証環境
Spring Security 3.2.4.RELEASE