Wednesday, February 22, 2012

Convincing IE to not remove window.location.hash on a redirect

Our system has to work well with the most used browsers. This is usually not a big deal as longas you don't check your page on IE. Unfortunately it is still among the most used ones, so any serious web page has to consider it.

Lately we faced the problem, that whenever spring security has decided that the login is not valid anymore, and redirected the user to the login page the query string and the hash are removed (only in IE) and the user looses the reference to the page he/she was before.

There are two solutions to this problem. The first is just a parameter in the authentication entry point of your website

<bean id="authenticationEntryPoint"
class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint" >
<constructor-arg name="loginFormUrl" value="/login.jsf" / >
<property name="useForward" value="true"/ >
</bean>

Forwarding instead of redirecting leaves the URL unchanged when the page displays the login page, so IE has no change to remove the parameters. Unfortunately this solutions shows the login page probably on every page of your website. If the user uses a password inserting tool based on URLs this will not work anymore, and of course it is not transparent for the user to enter its credentials once on the login page and than on another page.

The second solution I found on the Internet (so its not really my Idea). Basically it consists of adding a new filter in the filter chain on first position
<custom-filter ref="retainAnchorFilter" position="FIRST" />
This filter is than defined like this

<bean id="retainAnchorFilter" class="it.unibz.ict.utils.RetainAnchorFilter">
 <constructor-arg name="storeUrlPattern" value="${local.url}/login.*" />
<constructor-arg name="restoreUrlPattern" value=".*/${local.appname}/.*" />
<constructor-arg name="cookieName" value="TARGETANCHOR" />
</bean>
This filter will than store the hash when the URL matches the storeUrlPattern and restore it on the restoreUrlPattern. Actually we don't use the restore feature, because on the login page we read the parameters into a hidden field and send it using the normal form submission which fits better into our architecture, but for this sample I preferred to have it complete.

public class RetainAnchorFilter extends GenericFilterBean {
private final String storeUrlPattern;
private final String restoreUrlPattern;
private final String cookieName;
public RetainAnchorFilter(String storeUrlPattern, String restoreUrlPattern, String cookieName) {
this.storeUrlPattern = storeUrlPattern;
this.restoreUrlPattern = restoreUrlPattern;
this.cookieName = cookieName;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException,
ServletException {
if (response instanceof HttpServletResponse) {
response = new RedirectResponseWrapper((HttpServletResponse) response);
}
chain.doFilter(request, response);
}
/**
* HttpServletResponseWrapper that replaces the redirect by appropriate Javascript code.
*/
private class RedirectResponseWrapper extends HttpServletResponseWrapper {
public RedirectResponseWrapper(HttpServletResponse response) {
super(response);
}
@Override
public void sendRedirect(String location) throws IOException {
HttpServletResponse response = (HttpServletResponse) getResponse();
String redirectPageHtml = "";
if (location.matches(storeUrlPattern)) {
redirectPageHtml = generateStoreAnchorRedirectPageHtml(location);
} else if (location.matches(restoreUrlPattern)) {
redirectPageHtml = generateRestoreAnchorRedirectPageHtml(location);
} else {
super.sendRedirect(location);
return;
}
response.setContentType("text/html;charset=UTF-8");
response.setContentLength(redirectPageHtml.length());
response.getWriter().write(redirectPageHtml);
}
private String generateStoreAnchorRedirectPageHtml(String location) {
StringBuilder sb = new StringBuilder();
sb.append("<html><head><title>Redirect Page</title>\n");
sb.append("<script type=\"text/javascript\">\n");
// store anchor
sb.append("document.cookie = '" + cookieName + "=' + window.location.hash + '; path=/';\n");
// redirect
sb.append("window.location = '" + location + "' + window.location.hash;\n");
sb.append("</script>\n</head>\n");
sb.append("<body><h1>Redirect Page (Store Anchor)</h1>\n");
sb.append("Should redirect to " + location + "\n");
sb.append("</body></html>\n");
return sb.toString();
}
@SuppressWarnings("unused")
private String generateRestoreAnchorRedirectPageHtml(String location) {
StringBuilder sb = new StringBuilder();
sb.append("<html><head><title>Redirect Page</title>\n");
sb.append("<script type=\"text/javascript\">\n");
// generic Javascript function to get cookie value
sb.append("function getCookie(name) {\n");
sb.append("var cookies = document.cookie;\n");
sb.append("if (cookies.indexOf(name + '=') != -1) {\n");
sb.append("var startpos = cookies.indexOf(name)+name.length+1;\n");
sb.append("var endpos = cookies.indexOf(\";\",startpos)-1;\n");
sb.append("if (endpos == -2) endpos = cookies.length;\n");
sb.append("return unescape(cookies.substring(startpos,endpos));\n");
sb.append("} else {\n");
sb.append("return false;\n");
sb.append("}}\n");
// get anchor from cookie
sb.append("var targetAnchor = getCookie('" + cookieName + "');\n");
// append to URL and redirect
sb.append("if (targetAnchor) {\n");
sb.append("window.location = '" + location + "' + targetAnchor;\n");
sb.append("} else {\n");
sb.append("window.location = '" + location + "';\n");
sb.append("}\n");
sb.append("</script></head>\n");
sb.append("<body><h1>Redirect Page (Restore Anchor)</h1>\n");
sb.append("Should redirect to " + location + "\n");
sb.append("</body></html>\n");
return sb.toString();
}
}
}

The latter is for sure the better solution regarding transparency and in our case necessary because we have to redirect to CAS which would not be possible with the former.


No comments: