49

I am showing VERY sensitive data. After the user logs out from my server I don't want another user to be able to see the data hitting the Back button of the browser.

How can I achieve this?

BalusC
  • 1,082,665
  • 372
  • 3,610
  • 3,555
Tony
  • 10,088
  • 20
  • 85
  • 139

2 Answers2

83

By default, the browser's back button does not send a HTTP request to the server at all. Instead, it retrieves the page from the browser cache. This is essentially harmless, but indeed confusing to the enduser, because s/he incorrectly thinks that it's really coming from the server.

All you need to do is to instruct the browser to not cache the restricted pages. You can do this with a simple servlet filter which sets the appropriate response headers:

@WebFilter
public class NoCacheFilter implements Filter {

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        if (!request.getRequestURI().startsWith(request.getContextPath() + ResourceHandler.RESOURCE_IDENTIFIER)) { // Skip JSF resources (CSS/JS/Images/etc)
            response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); // HTTP 1.1.
            response.setHeader("Pragma", "no-cache"); // HTTP 1.0.
            response.setDateHeader("Expires", 0); // Proxies.
        }

        chain.doFilter(req, res);
    }

    // ...
}

(do note that this filter skips JSF resource requests, whose caching actually needs to be configured separately)

To get it to run on every JSF request, set the following annotation on the filter class, assuming that the value of the <servlet-name> of the FacesServlet in your webapp's web.xml is facesServlet:

@WebFilter(servletNames={"facesServlet"})

Or, to get it to run on a specific URL pattern only, such the one matching the restricted pages, e.g. /app/*, /private/*, /secured/*, or so, set the following annotation on the filter class:

@WebFilter("/app/*")

You could even do the very same job in a filter which checks the logged-in user, if you already have one.

If you happen to use JSF utility library OmniFaces, then you could also just grab its CacheControlFilter. This also transparently takes JSF resources into account.

See also:

Community
  • 1
  • 1
BalusC
  • 1,082,665
  • 372
  • 3,610
  • 3,555
  • I have tried this - and when I run it in the debugger it stops inside the noCacheFilter: doFilter method upon page loading. Then when I click the back button in Firefox (testing locally) it again runs through the function. Yet ends up showing the previous page. What should I look out for ? – Jan Mar 05 '14 at 11:44
  • The other error that happens in Firefox is that every third time or so you click on Back it will tell you that the page has expired. When you then click o the reload button (provided by the error page) it loads the previous page anyway. – Jan Mar 05 '14 at 12:20
  • I know this kind of old question, however, this is not working for Chrome. You could still see form field entered values when back button is clicked in chrome. – bluelabel Dec 01 '15 at 00:03
  • @bluelabel: that's just the browser autofill feature, which is an entirely different problem. The answer definitely works for Chrome, one won't be able to access restricted pages anymore via back button. Try reformulating your search query in order to find the right answers. – BalusC Dec 01 '15 at 08:02
  • @BalusC, in my scenario, i have a form with jsf input fields(not ajaxed), and i added above filter, after filling the form in chrome, i clicked different URL and clicked browser back button. I could see some fields still have values (not all), when i check it in dev tools, i could see, browser cache is not available instead it tries to get it from server. But the server response does not have the values i added to those fields. So I think chrome is trying to be smart here by caching form fields some hwere. However, this does not happen in firefox or IE. And my comment was not to offend you, – bluelabel Dec 01 '15 at 21:00
  • 1
    @bluelabel: the keyword you're looking for is "autofill". See also a.o. http://stackoverflow.com/q/9930900 – BalusC Dec 01 '15 at 21:03
  • @BalusC shouldn't the "response" object be passed in chain.doFilter(req, res) instead of "res" ? because its not working for me, no idea why not. – Talib Nov 12 '17 at 06:44
  • 1
    @Talib: they both refer exactly the same object. It's Java, an OO language, not PHP orso. If it's not working for you, then either those headers are being overriden at a later moment, or old pages are still in browser cache, or you've simply misobserved the back button behavior (i.e. it did actually hit the server, but there's in turn some bug in server side code which made it to look like cached). In a completely blank web application, the above answer works. – BalusC Nov 12 '17 at 11:14
  • @BalusC thank you very much for the informative reply. I actually checked everything , cleared the firefox history, checked the response headers from the developer tools and the response headers contains the cached headers : Cache-Control must-revalidate,no-cache,no-store,max-age=0 Pragma no-cache Expires Thu, 01 Jan 1970 00:00:00 GMT Can you please suggest something which might causing the issue ? – Talib Nov 12 '17 at 12:30
  • @BalusC I have posted my question here also in case you want to have a look . https://stackoverflow.com/questions/47204246/avoid-going-on-back-page-after-logout-in-jsf-application?noredirect=1#comment81363345_47204246 – Talib Nov 12 '17 at 12:31
  • @BalusC Thank You very much, it works perfectly on Firefox and Chrome for me, but unfortunately on Safari in iPad mini, it doesnt.. :( I cant get it to work on Safari – 10101101 Mar 20 '20 at 11:04
  • @10101101: Bug in Safari. Check https://stackoverflow.com/q/3602887 – BalusC Mar 23 '20 at 12:41
  • @BalusC Yep Thanks Sir :) Only difference is that it needs page resfresh, firefox/chrome on pc dont need that.. but its not a problem. – 10101101 Mar 28 '20 at 13:16
10

I also found another good solution.

In faces-config.xml add

<lifecycle>
    <phase-listener id="nocache">client.security.CacheControlPhaseListener</phase-listener>
</lifecycle>

And implement the following class:

package client.security;

import javax.faces.context.FacesContext;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;
import javax.servlet.http.HttpServletResponse;

@SuppressWarnings("serial")
public class CacheControlPhaseListener implements PhaseListener
{
    public PhaseId getPhaseId()
    {
        return PhaseId.RENDER_RESPONSE;
    }

    public void afterPhase(PhaseEvent event)        
    {
    }

    public void beforePhase(PhaseEvent event)
    {
       FacesContext facesContext = event.getFacesContext();
       HttpServletResponse response = (HttpServletResponse) facesContext
                .getExternalContext().getResponse();
       response.addHeader("Pragma", "no-cache");
       response.addHeader("Cache-Control", "no-cache");
       // Stronger according to blog comment below that references HTTP spec
       response.addHeader("Cache-Control", "no-store");
       response.addHeader("Cache-Control", "must-revalidate");
       // some date in the past
       response.addHeader("Expires", "Mon, 8 Aug 2006 10:00:00 GMT");
    }
} 
Tony
  • 10,088
  • 20
  • 85
  • 139
  • 8
    Whilst that may work, using a JSF phase listener while the same functionality is also possible with a servlet filter is like as using a hammer instead of a screw driver to get a screw down. – BalusC Sep 04 '13 at 12:38
  • what do you mean? "Whilst that may work, using a JSF phase listener while the same functionality is also possible with a servlet filter is like as using a hammer instead of a screw driver to get a screw down." – snabel Sep 17 '15 at 07:46