3

I've found many question about enforcing HTTPS under heroku but no response are about the java.

Here are the link I found :

Scala : http://www.andersen-gott.com/2012/03/using-unfiltered-and-https-on-heroku.html

Rails : Rails - How to Redirect from http://example.com to https://www.example.com

Please note that I am using Spring MVC 3.1 so I would prefer a solution based on WebMvcConfigurerAdapter.addInterceptors(InterceptorRegistry registry)

Community
  • 1
  • 1
Christophe Blin
  • 1,687
  • 1
  • 22
  • 40

4 Answers4

6

Here is a Servlet Filter that redirects all non-https requests to https:

package com.jamesward;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class HttpsEnforcer implements Filter {

    private FilterConfig filterConfig;

    public static final String X_FORWARDED_PROTO = "x-forwarded-proto";

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        this.filterConfig = filterConfig;
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        if (request.getHeader(X_FORWARDED_PROTO) != null) {
            if (request.getHeader(X_FORWARDED_PROTO).indexOf("https") != 0) {
                response.sendRedirect("https://" + request.getServerName() + (request.getPathInfo() == null ? "" : request.getPathInfo()));
                return;
            }
        }

        filterChain.doFilter(request, response);
    }

    @Override
    public void destroy() {
        // nothing
    }
}

I did this as a Servlet Filter because I couldn't get a Spring Interceptor to intercept the static asset (resource) requests.

Full source: https://github.com/jamesward/springmvc-https-enforcer

Andy
  • 8,749
  • 5
  • 34
  • 59
James Ward
  • 29,283
  • 9
  • 49
  • 85
  • 2
    request.getPathInfo() may return 'null', resulting in a string like this: 'https://yourdomain.comnull', so you might want to check for that before appending it to your redirect URL – Erik Jul 24 '15 at 20:22
  • Edited per Erik's comment. I've been fighting this for a while now, and the answer was right under my nose! Thanks. – Andy Jun 21 '16 at 01:16
2

Late answer, but an alternative, configuration-driven approach is to use Spring Security with requires-channel="https" on the various <sec:intercept-url> tags, in conjunction with Tomcat RemoteIpValve, which handles the same proxy headers (x-forwarded-proto, etc.) mentioned in the prior answer. The valve changes the behavior of HttpServletRequest.isSecure() based on the proxy headers to reflect the protocol reported by the proxy, not the protocol the connector actually received - so any frameworks like Spring that rely on isSecure() to Do the Right Thing will behave properly when SSL is offloaded.

SSL redirects in Heroku has instruction on configuring the valve for use with webapp-runner and Heroku.

Additionally, I suggest using PropertyPlaceholderConfigurer to let the value be environment over-ridable. E.g., in your Spring Security configuration:

<sec:intercept-url pattern="/api/**" requires-channel="${REQUIRES_CHANNEL}" access="..."/>

The relevant application-config.xml would include something like this:

<bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
  <property name="systemPropertiesModeName" value="SYSTEM_PROPERTIES_MODE_OVERRIDE" />
  <property name="properties" ref="applicationProperties"/>
</bean>

<bean id="applicationProperties" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
  <property name="locations">
    <list>
      <value>classpath:/application-config.properties</value>
    </list>
  </property>
</bean>

The application-config.properties file would include REQUIRES_CHANNEL=http as a default, then when deployed to Heroku, you set heroku config:set REQUIRES_CHANNEL=https to override.

Community
  • 1
  • 1
bimsapi
  • 4,985
  • 2
  • 19
  • 27
0

A bit optimized version of James's filter

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class HttpsFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpReq = (HttpServletRequest) request;
        HttpServletResponse httpResp = (HttpServletResponse) response;
        if ("http".equalsIgnoreCase(httpReq.getHeader("x-forwarded-proto"))) {
            StringBuffer requestURL = httpReq.getRequestURL();
            httpResp.sendRedirect("https" + requestURL.substring(requestURL.indexOf(":")));
        } else {
            chain.doFilter(request, response);
        }
    }

    @Override
    public void destroy() {
    }
}

Gist

elmot
  • 71
  • 1
  • 4
-1

Here is hte solution based on spring mvc :

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

public class ForceHerokuSsl extends HandlerInterceptorAdapter {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
         String proto = request.getHeader("x-forwarded-proto");
         return proto == null || "https".equalsIgnoreCase(proto);
    }
}

Then in the configuration :

@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {

   public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new ForceHerokuSslInterceptor());
   }
}
Christophe Blin
  • 1,687
  • 1
  • 22
  • 40