Avoid back button on JSF web application

40,898

Solution 1

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:

Solution 2

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");
    }
} 
Share:
40,898
Tony
Author by

Tony

Software Engineer. Mobile development. Ruby On Rails.

Updated on July 09, 2022

Comments

  • Tony
    Tony almost 2 years

    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
    BalusC over 10 years
    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.
  • Jan
    Jan about 10 years
    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
    Jan about 10 years
    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.
  • snabel
    snabel over 8 years
    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."
  • bluelabel
    bluelabel over 8 years
    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.
  • BalusC
    BalusC over 8 years
    @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.
  • bluelabel
    bluelabel over 8 years
    @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,
  • BalusC
    BalusC over 8 years
    @bluelabel: the keyword you're looking for is "autofill". See also a.o. stackoverflow.com/q/9930900
  • Talib
    Talib over 6 years
    @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.
  • BalusC
    BalusC over 6 years
    @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.
  • Talib
    Talib over 6 years
    @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
    Talib over 6 years
    @BalusC I have posted my question here also in case you want to have a look . stackoverflow.com/questions/47204246/…
  • 10101101
    10101101 about 4 years
    @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
  • BalusC
    BalusC about 4 years
    @10101101: Bug in Safari. Check stackoverflow.com/q/3602887
  • 10101101
    10101101 about 4 years
    @BalusC Yep Thanks Sir :) Only difference is that it needs page resfresh, firefox/chrome on pc dont need that.. but its not a problem.