Help

Built with Seam

You can find the full source code for this website in the Seam package in the directory /examples/wiki. It is licensed under the LGPL.

The Servlet specification defines several methods for tracking of the HTTP session between browser client and web server. Typically a Servlet container implements tracking of the session via the JSESSION (the name is mandatory) cookie, and also supports embedding of the session identifier in URLs via rewriting using the jsessionid path parameter.

How does the servlet container know which method it should use?

When a browser sends a request to the server for the first time, the server will receive neither a session cookie nor the request parameter. It does not know at this point if the client will support cookies at all and it has to rewrite all URLs in the page it serves, so that it includes the jsessionid path parameter on all links. But it will also send a cookie header with the response, so the client will have both: The HTML of a page with URLs that include a jsessionid and the JSESSIONID cookie.

The user now clicks on a link and a GET request is made with the jsessionid path parameter, and the JSESSIONID cookie will also be send to the server (if cookies are enabled in the browser). The servlet container can now decide what to use in future requests: If a cookie was received, URL rewriting is no longer necessary. If no cookie was received, the page that is served next has to be rewritten again.

The problem with search engines

The first problem you will probably encounter on any public website using a servlet container are search engines and web crawlers.

When a web crawler tries to index your website, it will send a request without a session identifier (naturally). Your servlet container will reply with a page containing rewritten URLs with a jsessionid path parameter. It will also send the session cookie, but web crawlers ignore cookies.

They do not ignore the path parameter. That is why you often see so many jsessionid URLs when you search on Google, for example. Although this has no negative consequences, it's just not very pretty.

The real problem: Seam page fragment caching

Seam supports caching of rendered page fragments, internally this is a cache of HTML strings, not the original JSF component subtree. Consider the following XHTML template:

<s:cache key="myUniqueKey" region="myPageFragments">
    <h:outputLink value="/myOtherResource.html">A plain link</h:outputLink>
    <s:link view="/myOtherPage.xhtml" value="A Seam link"/>
</s:cache>

When this template is rendered, Seam will cache the HTML output in the myPageFragments region. Both links will be rendered with the jsessionid path parameter automatically inserted by the servlet container! That means we just cached a link that was only valid and relevant for a particular client, the client that first requested the template.

Any subsequent requests that hit this cached fragment will be served with the same HTML content! This means that other clients will get the same session identifier. The worst case scenario here is that they might steal the session of the person who last forced a full non-cached rendering of the template. This is extremely dangerous and a serious bug in your application, as it allows session hijacking and of course, even if you users are not evil, it will confuse them as some of the links they click on will have a foreign session identifier and they will lose their session.

Disabling URL rewriting

So the only solution is to disable URL rewriting in the servlet container, globally. Even if it would be possible to disable URL rewriting of individual links, forgetting to do so with just one link breaks your application.

Unfortuantely, the most popular servlet container, Tomcat, does not support disabling URL rewriting globally. It does however support disabling cookies... which doesn't help at all.

The following filter disables URL rewriting:

@Startup
@Scope(ScopeType.APPLICATION)
@Name("sessionIdFilter")
@BypassInterceptors
@Filter(around ="org.jboss.seam.web.ajax4jsfFilter")
public class SessionIdFilter extends AbstractFilter {

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {

        if (!(req instanceof HttpServletRequest)) {
            chain.doFilter(req, res);
            return;
        }

        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        // Redirect requests with JSESSIONID in URL to clean version (old links 
        // bookmarked/stored by bots). This is ONLY triggered if the request did 
        // not also contain a JSESSIONID cookie! Which should be fine for bots...
        if (request.isRequestedSessionIdFromURL()) {
            String url = request.getRequestURL()
                         .append(request.getQueryString() != null ? "?"+request.getQueryString() : "")
                         .toString();
            // TODO: The url is clean, at least in Tomcat, which strips out 
            // the JSESSIONID path parameter automatically (Jetty does not?!)
            response.setHeader("Location", url);
            response.sendError(HttpServletResponse.SC_MOVED_PERMANENTLY);
            return;
        }

        // Prevent rendering of JSESSIONID in URLs for all outgoing links
        HttpServletResponseWrapper wrappedResponse =
            new HttpServletResponseWrapper(response) {
                @Override
                public String encodeRedirectUrl(String url) {
                    return url;
                }

                @Override
                public String encodeRedirectURL(String url) {
                    return url;
                }

                @Override
                public String encodeUrl(String url) {
                    return url;
                }

                @Override
                public String encodeURL(String url) {
                    return url;
                }
            };
        chain.doFilter(req, wrappedResponse);

    }
}

This filter actually solves two problems: First, it detects requests that have been made with a jsessionid in the URL. These are typically made from bookmarked URLs or by web crawlers that still have the old URLs in their database. When that happens, we redirect the client/crawler to the same URL without the jessionid and we tell the client to update the bookmark (MOVED PERMANENTLY).

The second part of the filter is disabling URL rewriting in the servlet container by wrapping the response and overriding the methods used by the container to rewrite URLs with no-ops.

You do not have to configure anything in your Seam application to use this filter, just copy/paste the class.

2 comments:
 
22. Dec 2011, 04:59 America/New_York | Link

A small sidenote:

Clients rejecting cookies will not be able to use sessions then.

 
03. Jul 2012, 13:49 America/New_York | Link

One thing I have noticed is that this filter breaks <rich:fileUpload />. When the filter is enabled and you attempt to upload files you see transfer errors within the upload component. Any idea what could be causing this behavior?