2

I am having a Spring MVC app which uses Spring Security for login. I am using Apache Webserver as Proxy and Tomcat. Below is my /etc/apache2/sites-enabled/example.com.conf file:

ServerAdmin admin@example.com
ServerName example.com
ServerAlias www.example.com
DocumentRoot /var/www/example.com/public_html

ProxyPreserveHost On
ProxyRequests off

ProxyPass /myapp/j_spring_security_check http://XX.YY.ZZ.WW:8080/myapp/j_spring_security_check
ProxyPassReverse /myapp/j_spring_security_check http://XX.YY.ZZ.WW:8080/myapp/j_spring_security_check

ProxyPass /myapp http://XX.YY.ZZ.WW:8080/myapp
ProxyPassReverse /myapp http://XX.YY.ZZ.WW:8080/myapp

My problem is now I have to access my site as:

www.example.com/myapp

where as I want to access it as

www.example.com

I tried playing with it but then the login didn't work properly. How should I set the ProxyPass & ProxyPassReverse for this?

masegaloeh
  • 17,978
  • 9
  • 56
  • 104
AAgg
  • 321
  • 1
  • 4
  • 11

3 Answers3

5

I've been struggling with this same problem for a few days and I might have cracked it. I'm new to Spring Security so don't take this as gospel! Others might object... I'm using Apache 2.4 (on OS X) and Spring Security 4.1.1.

Everything ran perfectly well running locally, but whenever it was deployed to run behind a reverse proxy I got 404 errors every time I logged in. After much head scratching and Googling, here's what I found:

(As I don't have enough reputation points to post more that 2 links I've had to use a space after 'http://' for the URLs!)

Suppose Apache and Tomcat are running on the same host (localhost) with Apache configured to proxy requests from www.example.com to our web app deployed under the context path '/webapp'

    ProxyPass / http://localhost:8080/webapp/
    ProxyPassReverse / http://localhost:8080/webapp/
  1. External client requests protected URL: http:// www.example.com/secret

    GET /secret HTTP/1.1
    
  2. Apache proxies this to http:// localhost:8080/webapp/secret

  3. One of Spring's security filters intervenes and responds with a redirect to /login

    HTTP/1.1 302 Found
    Location: http://www.example.com/login
    
  4. Browser fetches URL

    GET /login HTTP/1.1
    
  5. Apache proxies this to http:// localhost:8080/webapp/login

  6. Spring responds with its default login page

    HTTP/1.1 200 OK
    
  7. The interesting thing to note at this point is that the login form generated by Spring prefixes the forms action element with the context path (i.e. action="/webapp/login"). When you then click the submit button, a POST is performed to the URL /webapp/login

    POST /webapp/login HTTP/1.1
    

We now have a problem. When Apache proxies this to the backend server, the resulting URL will be http:// localhost/webapp/webapp/login. You can see this in the catalina.out log showing there is no handler that can handle the request as the context path is now appearing twice in the URL.

The problem here is that the ProxyPass and ProxyReversePass directives (mod_proxy module) only modifies the HTTP Location header, the URL is left untouched. What is needed is to strip the context path from the URL before it reaches the proxy which will add it back on. Apache's RewriteRule seems to do the trick:

RewriteRule /webapp/(.*)$ http://localhost:8080/webapp/$1 [P]

Although this solved the 404 errors and I could see Apache was now proxying to the correct URL, I was constantly getting the login page re-displayed every time I logged in. This next bit of config seems to resolve this:

ProxyPassReverseCookieDomain localhost www.example.com
ProxyPassReverseCookiePath /webapp/ /

I believe this may be because the proxying was causing the domain and path in the cookie to be set incorrectly, but I have to read up some more about that!

I hope this helps someone else out there, and people with more expertise than me in this area can comment on whether this is a fair solution...

Paul
  • 61
  • 1
  • 6
0

You could define virtualhosts. Something like this should do, I think:

<VirtualHost *:80>
    ServerAdmin for@get.it
    ProxyRequests Off
    ProxyPreserveHost On
    ProxyPass / http://localhost:8080/myapp connectiontimeout=5 timeout=30
    ProxyPassReverse / http://localhost:8080/myapp
    ServerName youname.it
</VirtualHost>

I run a setup like this with Apache 2.4 here

Marged
  • 213
  • 1
  • 2
  • 10
  • I have tried tried this already, it doesn't work. Nothing gets displayed on the browser. Are you also using Spring Security? That could be the differentiator here. – AAgg Feb 23 '15 at 09:27
0

This is possible if you define your own Filter & HttpServletRequestWrapper, with the Filter inserted before the Spring Security filter. By overriding the "getContextPath" to return the empty string, you can then set up an nginx/apache reverse proxy. (the basic concept of this comes from: http://www.lacerta.be/d7/content/keeping-real-user-ip-java-web-apps-behind-nginx-proxy, but that doesn't cover the context path)

In your dependency management software, add Servlet API:

   <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>servlet-api</artifactId>
        <version>2.5</version>
        <scope>provided</scope>
    </dependency>

(this of course assumes that you'll be running in a Tomcat/J2EE container).

And then, within your project or possibly a shared library, define two classes:

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

    public class RealIPFilter implements Filter {

         public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

             if (request instanceof HttpServletRequest) {
                chain.doFilter(new RealIPWrapper((HttpServletRequest)request), response);
             } else {
                chain.doFilter(request, response);
             }
         }

         @Override
         public void destroy() {}

         @Override
         public void init(FilterConfig config) throws ServletException {}

   }

And then the wrapper:

    public class RealIPWrapper extends HttpServletRequestWrapper {

        public RealIPWrapper(HttpServletRequest request) {
            super(request);
        }

        @Override
        public String getContextPath() {
            return "";
        }

        @Override
        public String getRemoteAddr() {
            String realIP = super.getHeader("X-Real-IP");
            return realIP != null ? realIP : super.getRemoteAddr();
        }

        @Override
        public String getRemoteHost() {
            try {
                return InetAddress.getByName(this.getRemoteAddr()).getHostName();
            } catch (UnknownHostException|NullPointerException e) {
                return getRemoteAddr();
            }
        }
    }

And then add the correct filter before the spring filter (web.xml)

<filter>
    <filter-name>RealIPFilter</filter-name>
    <filter-class>RealIPFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>RealIPFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
<filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

So then in the NGINX server block:

  location / {
     proxy_set_header X-Real-IP $remote_addr;
     proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
     proxy_set_header Host $host;
     proxy_set_header X-NginX-Proxy true;

     proxy_cookie_path /<context-path>/ /;

     proxy_pass http://localhost:8080/<context>/;
     proxy_redirect off;

  }