浏览代码

Added initial integration of SEC-29 "Save POST request parameters before redirect" for peer review. See http://opensource2.atlassian.com/projects/spring/browse/SEC-29 for more info.

Scott McCrory 20 年之前
父节点
当前提交
a8ad9231ab

+ 308 - 0
sandbox/src/main/java/org/acegisecurity/intercept/web/SandboxSecurityEnforcementFilter.java

@@ -0,0 +1,308 @@
+/* Copyright 2004, 2005 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.sf.acegisecurity.intercept.web;
+
+import java.io.IOException;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import net.sf.acegisecurity.AccessDeniedException;
+import net.sf.acegisecurity.AuthenticationException;
+import net.sf.acegisecurity.AuthenticationTrustResolver;
+import net.sf.acegisecurity.AuthenticationTrustResolverImpl;
+import net.sf.acegisecurity.InsufficientAuthenticationException;
+import net.sf.acegisecurity.context.SecurityContextHolder;
+import net.sf.acegisecurity.ui.AbstractProcessingFilter;
+import net.sf.acegisecurity.util.PortResolver;
+import net.sf.acegisecurity.util.PortResolverImpl;
+import net.sf.acegisecurity.wrapper.redirect.SavedHttpServletRequest;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.util.Assert;
+
+/**
+ * Wraps requests to the {@link FilterSecurityInterceptor}.
+ *
+ * <p>
+ * This filter is necessary because it provides the bridge between incoming
+ * requests and the <code>FilterSecurityInterceptor</code> instance.
+ * </p>
+ *
+ * <p>
+ * If an {@link AuthenticationException} is detected, the filter will launch
+ * the <code>authenticationEntryPoint</code>. This allows common handling of
+ * authentication failures originating from any subclass of {@link
+ * net.sf.acegisecurity.intercept.AbstractSecurityInterceptor}.
+ * </p>
+ *
+ * <p>
+ * If an {@link AccessDeniedException} is detected, the filter will determine
+ * whether or not the user is an anonymous user. If they are an anonymous
+ * user, the <code>authenticationEntryPoint</code> will be launched. If they
+ * are not an anonymous user, the filter will respond with a
+ * <code>HttpServletResponse.SC_FORBIDDEN</code> (403 error).  In addition,
+ * the <code>AccessDeniedException</code> itself will be placed in the
+ * <code>HttpSession</code> attribute keyed against {@link
+ * #ACEGI_SECURITY_ACCESS_DENIED_EXCEPTION_KEY} (to allow access to the stack
+ * trace etc). Again, this allows common access denied handling irrespective
+ * of the originating security interceptor.
+ * </p>
+ *
+ * <p>
+ * To use this filter, it is necessary to specify the following properties:
+ * </p>
+ *
+ * <ul>
+ * <li>
+ * <code>filterSecurityInterceptor</code> indicates the
+ * <code>FilterSecurityInterceptor</code> to delegate HTTP security decisions
+ * to.
+ * </li>
+ * <li>
+ * <code>authenticationEntryPoint</code> indicates the handler that should
+ * commence the authentication process if an
+ * <code>AuthenticationException</code> is detected. Note that this may also
+ * switch the current protocol from http to https for an SSL login.
+ * </li>
+ * <li>
+ * <code>portResolver</code> is used to determine the "real" port that a
+ * request was received on.
+ * </li>
+ * </ul>
+ *
+ * <P>
+ * <B>Do not use this class directly.</B> Instead configure
+ * <code>web.xml</code> to use the {@link
+ * net.sf.acegisecurity.util.FilterToBeanProxy}.
+ * </p>
+ *
+ * @author Ben Alex
+ * @author colin sampaleanu
+ * @author Andrey Grebnev <a href="mailto:andrey.grebnev@blandware.com">&lt;andrey.grebnev@blandware.com&gt;</a>
+ * @version $Id$
+ */
+public class SandboxSecurityEnforcementFilter implements Filter, InitializingBean {
+	
+    private static final Log logger = LogFactory.getLog(SecurityEnforcementFilter.class);
+    
+    public static final String ACEGI_SECURITY_ACCESS_DENIED_EXCEPTION_KEY = "ACEGI_SECURITY_403_EXCEPTION";
+    public static final String SAVED_REQUEST_SESSION_ATTRIBUTE = "net.sf.acegisecurity.intercept.web.SAVED_REQUEST_SESSION_ATTRIBUTE";
+    
+    private AuthenticationEntryPoint authenticationEntryPoint;
+    private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl();
+    private FilterSecurityInterceptor filterSecurityInterceptor;
+    private PortResolver portResolver = new PortResolverImpl();
+    private boolean createSessionAllowed = true;
+
+    /**
+     * Sets the AuthenticationEntryPoint.
+     * @param authenticationEntryPoint The authentication entry point.
+     */
+    public void setAuthenticationEntryPoint(
+        AuthenticationEntryPoint authenticationEntryPoint) {
+        this.authenticationEntryPoint = authenticationEntryPoint;
+    }
+
+    /**
+     * Returns the AuthenticationEntryPoint.
+     * @return The authentication entry point.
+     */
+    public AuthenticationEntryPoint getAuthenticationEntryPoint() {
+        return authenticationEntryPoint;
+    }
+
+    /**
+     * Sets the AuthenticationTrustResolver.
+     * @param authenticationTrustResolver The AuthenticationTrustResolver.
+     */
+    public void setAuthenticationTrustResolver(
+        AuthenticationTrustResolver authenticationTrustResolver) {
+        this.authenticationTrustResolver = authenticationTrustResolver;
+    }
+
+    /**
+     * If <code>true</code>, indicates that <code>SecurityEnforcementFilter</code> is permitted
+     * to store the target URL and exception information in the <code>HttpSession</code> (the
+     * default). In situations where you do not wish to unnecessarily create <code>HttpSession</code>s
+     * - because the user agent will know the failed URL, such as with BASIC or Digest authentication
+     * - you may wish to set this property to <code>false</code>. Remember to also set the
+     * {@link net.sf.acegisecurity.context.HttpSessionContextIntegrationFilter#allowSessionCreation}
+     * to <code>false</code> if you set this property to <code>false</code>.
+     *
+     * @return <code>true</code> if the <code>HttpSession</code> will be used to store information
+     * about the failed request, <code>false</code> if the <code>HttpSession</code> will not be
+     * used
+     */
+    public boolean isCreateSessionAllowed() {
+        return createSessionAllowed;
+    }
+
+    public void setCreateSessionAllowed(boolean createSessionAllowed) {
+        this.createSessionAllowed = createSessionAllowed;
+    }
+
+    public AuthenticationTrustResolver getAuthenticationTrustResolver() {
+        return authenticationTrustResolver;
+    }
+
+    public void setFilterSecurityInterceptor(
+        FilterSecurityInterceptor filterSecurityInterceptor) {
+        this.filterSecurityInterceptor = filterSecurityInterceptor;
+    }
+
+    public FilterSecurityInterceptor getFilterSecurityInterceptor() {
+        return filterSecurityInterceptor;
+    }
+
+    public void setPortResolver(PortResolver portResolver) {
+        this.portResolver = portResolver;
+    }
+
+    public PortResolver getPortResolver() {
+        return portResolver;
+    }
+
+    public void afterPropertiesSet() throws Exception {
+        Assert.notNull(authenticationEntryPoint,
+            "authenticationEntryPoint must be specified");
+        Assert.notNull(filterSecurityInterceptor,
+            "filterSecurityInterceptor must be specified");
+        Assert.notNull(portResolver, "portResolver must be specified");
+        Assert.notNull(authenticationTrustResolver,
+            "authenticationTrustResolver must be specified");
+    }
+
+    public void destroy() {
+    }
+
+    public void doFilter(ServletRequest request, ServletResponse response,
+        FilterChain chain) throws IOException, ServletException {
+        if (!(request instanceof HttpServletRequest)) {
+            throw new ServletException("HttpServletRequest required");
+        }
+
+        if (!(response instanceof HttpServletResponse)) {
+            throw new ServletException("HttpServletResponse required");
+        }
+
+        FilterInvocation fi = new FilterInvocation(request, response, chain);
+
+        try {
+            filterSecurityInterceptor.invoke(fi);
+
+            if (logger.isDebugEnabled()) {
+                logger.debug("Chain processed normally");
+            }
+        } catch (AuthenticationException authentication) {
+            if (logger.isDebugEnabled()) {
+                logger.debug("Authentication exception occurred; redirecting to authentication entry point",
+                    authentication);
+            }
+
+            sendStartAuthentication(fi, authentication);
+        } catch (AccessDeniedException accessDenied) {
+            if (authenticationTrustResolver.isAnonymous(
+                        SecurityContextHolder.getContext().getAuthentication())) {
+                if (logger.isDebugEnabled()) {
+                    logger.debug("Access is denied (user is anonymous); redirecting to authentication entry point",
+                        accessDenied);
+                }
+
+                sendStartAuthentication(fi,
+                    new InsufficientAuthenticationException(
+                        "Full authentication is required to access this resource"));
+            } else {
+                if (logger.isDebugEnabled()) {
+                    logger.debug("Access is denied (user is not anonymous); sending back forbidden response",
+                        accessDenied);
+                }
+
+                sendAccessDeniedError(fi, accessDenied);
+            }
+        } catch (ServletException e) {
+            throw e;
+        } catch (IOException e) {
+            throw e;
+        } catch (Throwable otherException) {
+            throw new ServletException(otherException);
+        }
+    }
+
+    public void init(FilterConfig filterConfig) throws ServletException {
+    }
+
+    protected void sendAccessDeniedError(FilterInvocation fi,
+        AccessDeniedException accessDenied)
+        throws ServletException, IOException {
+        if (createSessionAllowed) {
+            ((HttpServletRequest) fi.getRequest()).getSession().setAttribute(ACEGI_SECURITY_ACCESS_DENIED_EXCEPTION_KEY,
+                accessDenied);
+        }
+
+        ((HttpServletResponse) fi.getResponse()).sendError(HttpServletResponse.SC_FORBIDDEN,
+            accessDenied.getMessage()); // 403
+    }
+
+    protected void sendStartAuthentication(FilterInvocation fi,
+        AuthenticationException reason) throws ServletException, IOException {
+    	
+    	// Get the HttpServletRequest
+        HttpServletRequest request = (HttpServletRequest) fi.getRequest();
+
+        // Save this original request on the session in case
+        // SecurityContextHolderAwareRequestWrapper has to resume it after (re)authentication.
+        request.getSession().setAttribute(SAVED_REQUEST_SESSION_ATTRIBUTE, SavedHttpServletRequest.saveRequest(request));
+
+        int port = portResolver.getServerPort(request);
+        boolean includePort = true;
+
+        if ("http".equals(request.getScheme().toLowerCase()) && (port == 80)) {
+            includePort = false;
+        }
+
+        if ("https".equals(request.getScheme().toLowerCase()) && (port == 443)) {
+            includePort = false;
+        }
+
+        // Build the target URL from the request
+        String targetUrl = request.getScheme() + "://" +
+            request.getServerName() + ((includePort) ? (":" + port) : "") +
+            request.getContextPath() + fi.getRequestUrl();
+
+        if (logger.isDebugEnabled()) {
+            logger.debug(
+                "Authentication entry point being called; target URL added to Session: " +
+                targetUrl);
+        }
+
+        if (createSessionAllowed) {
+            ((HttpServletRequest) request).getSession().setAttribute(AbstractProcessingFilter.ACEGI_SECURITY_TARGET_URL_KEY,
+                targetUrl);
+        }
+
+        authenticationEntryPoint.commence(request,
+            (HttpServletResponse) fi.getResponse(), reason);
+    }
+    
+}

+ 475 - 0
sandbox/src/main/java/org/acegisecurity/wrapper/SandboxSecurityContextHolderAwareRequestWrapper.java

@@ -0,0 +1,475 @@
+/* Copyright 2004, 2005 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.wrapper;
+
+import java.security.Principal;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.TimeZone;
+
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import javax.servlet.http.HttpSession;
+
+import net.sf.acegisecurity.Authentication;
+import net.sf.acegisecurity.AuthenticationTrustResolver;
+import net.sf.acegisecurity.AuthenticationTrustResolverImpl;
+import net.sf.acegisecurity.GrantedAuthority;
+import net.sf.acegisecurity.UserDetails;
+import net.sf.acegisecurity.context.SecurityContextHolder;
+import net.sf.acegisecurity.intercept.web.SandboxSecurityEnforcementFilter;
+import net.sf.acegisecurity.wrapper.redirect.Enumerator;
+import net.sf.acegisecurity.wrapper.redirect.FastHttpDateFormat;
+import net.sf.acegisecurity.wrapper.redirect.SavedHttpServletRequest;
+
+/**
+ * An Acegi Security-aware <code>HttpServletRequestWrapper</code>, which uses
+ * the <code>SecurityContext</code>-defined <code>Authentication</code>
+ * object for
+ * {@link SecurityContextHolderAwareRequestWrapper#isUserInRole(java.lang.String)}
+ * and {@link javax.servlet.http.HttpServletRequestWrapper#getRemoteUser()}
+ * responses.
+ * <p>
+ * Provides request parameters, headers, cookies from original requrest or saved request.
+ * </p>
+ * 
+ * @author Orlando Garcia Carmona
+ * @author Ben Alex
+ * @author Andrey Grebnev <a href="mailto:andrey.grebnev@blandware.com">&lt;andrey.grebnev@blandware.com&gt;</a>
+ * @version $Id$
+ */
+public class SandboxSecurityContextHolderAwareRequestWrapper extends
+		HttpServletRequestWrapper {
+
+	// ~ Static fields ========================================================
+
+	protected static final TimeZone GMT_ZONE = TimeZone.getTimeZone("GMT");
+
+	/**
+	 * The default Locale if none are specified.
+	 */
+	protected static Locale defaultLocale = Locale.getDefault();
+
+	// ~ Instance fields
+	// ========================================================
+
+	/**
+	 * Authentication trust resolver.
+	 */
+	private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl();
+
+	/**
+	 * The set of SimpleDateFormat formats to use in getDateHeader().
+	 * 
+	 * Notice that because SimpleDateFormat is not thread-safe, we can't declare
+	 * formats[] as a static variable.
+	 */
+	protected SimpleDateFormat formats[] = new SimpleDateFormat[3];
+
+	/**
+	 * Saved request (to be resumed after authentication)
+	 */
+	protected SavedHttpServletRequest savedRequest = null;
+
+	// ~ Constructors
+	// ===========================================================
+
+	/**
+	 * The class' primary constructor.
+	 * 
+	 * @param request HttpServletRequest
+	 */
+	public SandboxSecurityContextHolderAwareRequestWrapper(HttpServletRequest request) {
+
+		// First do what the parent class needs to.
+		super(request);
+
+		// Return if there isn't an existing HttpSession
+		HttpSession session = request.getSession(false);
+		if (session != null) {
+
+			// We know there's an existing HttpSession, so see if it has a
+			// saved request (placed there by SecurityEnforcementFilter).
+			SavedHttpServletRequest saved = (SavedHttpServletRequest) session
+					.getAttribute(SandboxSecurityEnforcementFilter.SAVED_REQUEST_SESSION_ATTRIBUTE);
+			if (saved != null) {
+
+				// We know there's a saved request, so see if it has a
+				// saved "root" request URI to forward to.
+				String requestURI = saved.getRequestURI();
+				if (requestURI != null) {
+
+					// We know there's a saved "root" request URI, so see if
+					// it's the
+					// same one specified by this request.
+					if (requestURI.equals(request.getRequestURI())) {
+
+						// They're the same "root" request URIs, so get the
+						// saved request and remove it from the HttpSession
+						// since we only want to process it once.
+						savedRequest = saved;
+						session
+								.removeAttribute(SandboxSecurityEnforcementFilter.SAVED_REQUEST_SESSION_ATTRIBUTE);
+
+						formats[0] = new SimpleDateFormat(
+								"EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US);
+						formats[1] = new SimpleDateFormat(
+								"EEEEEE, dd-MMM-yy HH:mm:ss zzz", Locale.US);
+						formats[2] = new SimpleDateFormat(
+								"EEE MMMM d HH:mm:ss yyyy", Locale.US);
+
+						formats[0].setTimeZone(GMT_ZONE);
+						formats[1].setTimeZone(GMT_ZONE);
+						formats[2].setTimeZone(GMT_ZONE);
+					}
+
+				}
+
+			}
+
+		}
+
+		return;
+
+	}
+
+	// ~ Methods
+	// ================================================================
+
+	/**
+	 * Returns the principal's name, as obtained from the
+	 * <code>SecurityContextHolder</code>. Properly handles both
+	 * <code>String</code>-based and <code>UserDetails</code>-based
+	 * principals.
+	 * 
+	 * @return the username or <code>null</code> if unavailable
+	 */
+	public String getRemoteUser() {
+		Authentication auth = getAuthentication();
+
+		if ((auth == null) || (auth.getPrincipal() == null)) {
+			return null;
+		}
+
+		if (auth.getPrincipal() instanceof UserDetails) {
+			return ((UserDetails) auth.getPrincipal()).getUsername();
+		}
+
+		return auth.getPrincipal().toString();
+	}
+
+	/**
+	 * Simple searches for an exactly matching {@link
+	 * GrantedAuthority#getAuthority()}.
+	 * 
+	 * <p>
+	 * Will always return <code>false</code> if the
+	 * <code>SecurityContextHolder</code> contains an
+	 * <code>Authentication</code> with
+	 * <code>null</code><code>principal</code> and/or
+	 * <code>GrantedAuthority[]</code> objects.
+	 * </p>
+	 * 
+	 * @param role the <code>GrantedAuthority</code><code>String</code> representation to check for.
+	 * @return <code>true</code> if an <b>exact</b> (case sensitive) matching granted authority is located, <code>false</code> otherwise.
+	 */
+	public boolean isUserInRole(String role) {
+		return isGranted(role);
+	}
+
+	/**
+	 * Returns the <code>Authentication</code> (which is a subclass of
+	 * <code>Principal</code>), or <code>null</code> if unavailable.
+	 * 
+	 * <p>
+	 * Note: Override this method in order to workaround the problem in Sun Java
+	 * System Application Server 8.1 PE
+	 * </p>
+	 * 
+	 * @return the <code>Authentication</code>, or <code>null</code>
+	 */
+	public Principal getUserPrincipal() {
+		Authentication auth = getAuthentication();
+
+		if ((auth == null) || (auth.getPrincipal() == null)) {
+			return null;
+		}
+
+		return auth;
+	}
+
+	/**
+	 * Obtain the current active <code>Authentication</code>
+	 * 
+	 * @return the authentication object or <code>null</code>
+	 */
+	private Authentication getAuthentication() {
+		Authentication auth = SecurityContextHolder.getContext()
+				.getAuthentication();
+
+		if (!authenticationTrustResolver.isAnonymous(auth)) {
+			return auth;
+		}
+
+		return null;
+	}
+
+	/**
+	 * Determines if principal has been granted a given role.
+	 * 
+	 * @param role The role being tested.
+	 * @return True if principal has been granted the given role.
+	 */
+	private boolean isGranted(String role) {
+		Authentication auth = getAuthentication();
+
+		if ((auth == null) || (auth.getPrincipal() == null)
+				|| (auth.getAuthorities() == null)) {
+			return false;
+		}
+
+		for (int i = 0; i < auth.getAuthorities().length; i++) {
+			if (role.equals(auth.getAuthorities()[i].getAuthority())) {
+				return true;
+			}
+		}
+
+		return false;
+	}
+
+	/**
+	 * The default behavior of this method is to return getMethod() on the
+	 * wrapped request object.
+	 */
+	public String getMethod() {
+		if (savedRequest == null) {
+			return super.getMethod();
+		} else {
+			return savedRequest.getMethod();
+		}
+	}
+
+	/**
+	 * The default behavior of this method is to return getHeader(String name)
+	 * on the wrapped request object.
+	 */
+	public String getHeader(String name) {
+		if (savedRequest == null) {
+			return super.getHeader(name);
+		} else {
+			String header = null;
+			Iterator iterator = savedRequest.getHeaderValues(name);
+			while (iterator.hasNext()) {
+				header = (String) iterator.next();
+				break;
+			}
+			return header;
+		}
+	}
+
+	/**
+	 * The default behavior of this method is to return getIntHeader(String
+	 * name) on the wrapped request object.
+	 */
+	public int getIntHeader(String name) {
+		if (savedRequest == null) {
+			return super.getIntHeader(name);
+		} else {
+			String value = getHeader(name);
+			if (value == null) {
+				return (-1);
+			} else {
+				return (Integer.parseInt(value));
+			}
+		}
+	}
+
+	/**
+	 * The default behavior of this method is to return getDateHeader(String
+	 * name) on the wrapped request object.
+	 */
+	public long getDateHeader(String name) {
+		if (savedRequest == null) {
+			return super.getDateHeader(name);
+		} else {
+			String value = getHeader(name);
+			if (value == null)
+				return (-1L);
+
+			// Attempt to convert the date header in a variety of formats
+			long result = FastHttpDateFormat.parseDate(value, formats);
+			if (result != (-1L)) {
+				return result;
+			}
+			throw new IllegalArgumentException(value);
+		}
+	}
+
+	/**
+	 * The default behavior of this method is to return getHeaderNames() on the
+	 * wrapped request object.
+	 */
+	public Enumeration getHeaderNames() {
+		if (savedRequest == null) {
+			return super.getHeaderNames();
+		} else {
+			return new Enumerator(savedRequest.getHeaderNames());
+		}
+	}
+
+	/**
+	 * The default behavior of this method is to return getHeaders(String name)
+	 * on the wrapped request object.
+	 */
+	public Enumeration getHeaders(String name) {
+		if (savedRequest == null) {
+			return super.getHeaders(name);
+		} else {
+			return new Enumerator(savedRequest.getHeaderValues(name));
+		}
+	}
+
+	/**
+	 * The default behavior of this method is to return getCookies() on the
+	 * wrapped request object.
+	 */
+	public Cookie[] getCookies() {
+		if (savedRequest == null) {
+			return super.getCookies();
+		} else {
+			List cookies = savedRequest.getCookies();
+			return (Cookie[]) cookies.toArray(new Cookie[cookies.size()]);
+		}
+	}
+
+	/**
+	 * The default behavior of this method is to return
+	 * getParameterValues(String name) on the wrapped request object.
+	 */
+	public String[] getParameterValues(String name) {
+		if (savedRequest == null) {
+			return super.getParameterValues(name);
+		} else {
+			return savedRequest.getParameterValues(name);
+		}
+	}
+
+	/**
+	 * The default behavior of this method is to return getParameterNames() on
+	 * the wrapped request object.
+	 */
+	public Enumeration getParameterNames() {
+		if (savedRequest == null) {
+			return super.getParameterNames();
+		} else {
+			return new Enumerator(savedRequest.getParameterNames());
+		}
+	}
+
+	/**
+	 * The default behavior of this method is to return getParameterMap() on the
+	 * wrapped request object.
+	 */
+	public Map getParameterMap() {
+		if (savedRequest == null) {
+			return super.getParameterMap();
+		} else {
+			return savedRequest.getParameterMap();
+		}
+	}
+
+	/**
+	 * The default behavior of this method is to return getParameter(String
+	 * name) on the wrapped request object.
+	 */
+	public String getParameter(String name) {
+
+		/*
+		 * if (savedRequest == null) { return super.getParameter(name); } else {
+		 * String value = null; String[] values =
+		 * savedRequest.getParameterValues(name); if (values == null) return
+		 * null; for (int i = 0; i < values.length; i++) { value = values[i];
+		 * break; } return value; }
+		 */
+
+		// We do not get value from super.getParameter because
+		// of a bug in Jetty servlet-container.
+		String value = null;
+		String[] values = null;
+		if (savedRequest == null) {
+			values = super.getParameterValues(name);
+		} else {
+			values = savedRequest.getParameterValues(name);
+		}
+
+		if (values == null)
+			return null;
+		for (int i = 0; i < values.length; i++) {
+			value = values[i];
+			break;
+		}
+		return value;
+
+	}
+
+	/**
+	 * The default behavior of this method is to return getLocales() on the
+	 * wrapped request object.
+	 */
+	public Enumeration getLocales() {
+		if (savedRequest == null) {
+			return super.getLocales();
+		} else {
+			Iterator iterator = savedRequest.getLocales();
+			if (iterator.hasNext()) {
+				return new Enumerator(iterator);
+			} else {
+				ArrayList results = new ArrayList();
+				results.add(defaultLocale);
+				return new Enumerator(results.iterator());
+			}
+		}
+	}
+
+	/**
+	 * The default behavior of this method is to return getLocale() on the
+	 * wrapped request object.
+	 */
+	public Locale getLocale() {
+		if (savedRequest == null) {
+			return super.getLocale();
+		} else {
+			Locale locale = null;
+			Iterator iterator = savedRequest.getLocales();
+			while (iterator.hasNext()) {
+				locale = (Locale) iterator.next();
+				break;
+			}
+			if (locale == null) {
+				return defaultLocale;
+			} else {
+				return locale;
+			}
+		}
+	}
+
+}

+ 143 - 0
sandbox/src/main/java/org/acegisecurity/wrapper/redirect/Enumerator.java

@@ -0,0 +1,143 @@
+/* Copyright 2004, 2005 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.wrapper.redirect;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+
+/**
+ * <p>Adapter class that wraps an <code>Enumeration</code> around a Java2
+ * collection classes object <code>Iterator</code> so that existing APIs
+ * returning Enumerations can easily run on top of the new collections.
+ * Constructors are provided to easliy create such wrappers.</p>
+ * <p>The source code is taken from Apache Tomcat</p>
+ *
+ * <p><a href="Enumerator.java.html"><i>View Source</i></a></p>
+ *
+ * @author Craig R. McClanahan
+ * @author Andrey Grebnev <a href="mailto:andrey.grebnev@blandware.com">&lt;andrey.grebnev@blandware.com&gt;</a>
+ * @version $Revision$ $Date$
+ */
+public class Enumerator implements Enumeration {
+
+    // ----------------------------------------------------------- Constructors
+
+    /**
+     * The <code>Iterator</code> over which the <code>Enumeration</code>
+     * represented by this class actually operates.
+     */
+    private Iterator iterator = null;
+
+    /**
+     * Return an Enumeration over the values of the specified Collection.
+     *
+     * @param collection Collection whose values should be enumerated
+     */
+    public Enumerator(Collection collection) {
+        this(collection.iterator());
+    }
+
+    /**
+     * Return an Enumeration over the values of the specified Collection.
+     *
+     * @param collection Collection whose values should be enumerated
+     * @param clone true to clone iterator
+     */
+    public Enumerator(Collection collection, boolean clone) {
+        this(collection.iterator(), clone);
+    }
+
+    /**
+     * Return an Enumeration over the values returned by the
+     * specified Iterator.
+     *
+     * @param iterator Iterator to be wrapped
+     */
+    public Enumerator(Iterator iterator) {
+        super();
+        this.iterator = iterator;
+    }
+
+    /**
+     * Return an Enumeration over the values returned by the
+     * specified Iterator.
+     *
+     * @param iterator Iterator to be wrapped
+     * @param clone true to clone iterator
+     */
+    public Enumerator(Iterator iterator, boolean clone) {
+
+        super();
+        if (!clone) {
+            this.iterator = iterator;
+        } else {
+            List list = new ArrayList();
+            while (iterator.hasNext()) {
+                list.add(iterator.next());
+            }
+            this.iterator = list.iterator();
+        }
+
+    }
+
+    /**
+     * Return an Enumeration over the values of the specified Map.
+     *
+     * @param map Map whose values should be enumerated
+     */
+    public Enumerator(Map map) {
+        this(map.values().iterator());
+    }
+
+    /**
+     * Return an Enumeration over the values of the specified Map.
+     *
+     * @param map Map whose values should be enumerated
+     * @param clone true to clone iterator
+     */
+    public Enumerator(Map map, boolean clone) {
+        this(map.values().iterator(), clone);
+    }
+
+    /**
+     * Tests if this enumeration contains more elements.
+     *
+     * @return <code>true</code> if and only if this enumeration object
+     *  contains at least one more element to provide, <code>false</code>
+     *  otherwise
+     */
+    public boolean hasMoreElements() {
+        return (iterator.hasNext());
+    }
+
+    /**
+     * Returns the next element of this enumeration if this enumeration
+     * has at least one more element to provide.
+     *
+     * @return the next element of this enumeration
+     *
+     * @exception NoSuchElementException if no more elements exist
+     */
+    public Object nextElement() throws NoSuchElementException {
+        return (iterator.next());
+    }
+
+}

+ 226 - 0
sandbox/src/main/java/org/acegisecurity/wrapper/redirect/FastHttpDateFormat.java

@@ -0,0 +1,226 @@
+/* Copyright 2004, 2005 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.wrapper.redirect;
+
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.TimeZone;
+
+/**
+ * <p>Utility class to generate HTTP dates.</p>
+ * <p><a href="FastHttpDateFormat.java.html"><i>View Source</i></a></p>
+ * <p>This source code is taken from Tomcat Apache</p>
+ *
+ * @author Remy Maucherat
+ * @author Andrey Grebnev <a href="mailto:andrey.grebnev@blandware.com">&lt;andrey.grebnev@blandware.com&gt;</a>
+ * @version $Revision$ $Date$
+ */
+public class FastHttpDateFormat {
+
+    /**
+     * Current formatted date.
+     */
+    protected static String currentDate = null;
+
+    /**
+     * Instant on which the currentDate object was generated.
+     */
+    protected static long currentDateGenerated = 0L;
+
+    /**
+     * HTTP date format.
+     */
+    protected static final SimpleDateFormat format =
+        new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US);
+
+    /**
+     * Formatter cache.
+     */
+    protected static final HashMap formatCache = new HashMap();
+
+    /**
+     * The set of SimpleDateFormat formats to use in <code>getDateHeader()</code>.
+     */
+    protected static final SimpleDateFormat formats[] = {
+        new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US),
+        new SimpleDateFormat("EEEEEE, dd-MMM-yy HH:mm:ss zzz", Locale.US),
+        new SimpleDateFormat("EEE MMMM d HH:mm:ss yyyy", Locale.US)
+    };
+
+    /**
+     * GMT timezone - all HTTP dates are on GMT
+     */
+    protected final static TimeZone gmtZone = TimeZone.getTimeZone("GMT");
+
+    /**
+     * Parser cache.
+     */
+    protected static final HashMap parseCache = new HashMap();
+
+    static {
+
+        format.setTimeZone(gmtZone);
+
+        formats[0].setTimeZone(gmtZone);
+        formats[1].setTimeZone(gmtZone);
+        formats[2].setTimeZone(gmtZone);
+
+    }
+
+    /**
+     * Formats a specified date to HTTP format. If local format is not
+     * <code>null</code>, it's used instead.
+     *
+     * @param value Date value to format
+     * @param threadLocalformat The format to use (or <code>null</code> -- then
+     *                          HTTP format will be used)
+     * @return Formatted date
+     */
+    public static final String formatDate(long value,
+                                          DateFormat threadLocalformat) {
+
+        String cachedDate = null;
+        Long longValue = new Long(value);
+        try {
+            cachedDate = (String) formatCache.get(longValue);
+        } catch (Exception e) {
+        }
+        if (cachedDate != null)
+            return cachedDate;
+
+        String newDate = null;
+        Date dateValue = new Date(value);
+        if (threadLocalformat != null) {
+            newDate = threadLocalformat.format(dateValue);
+            synchronized (formatCache) {
+                updateCache(formatCache, longValue, newDate);
+            }
+        } else {
+            synchronized (formatCache) {
+                newDate = format.format(dateValue);
+                updateCache(formatCache, longValue, newDate);
+            }
+        }
+        return newDate;
+
+    }
+
+    /**
+     * Gets the current date in HTTP format.
+     *
+     * @return Current date in HTTP format
+     */
+    public static final String getCurrentDate() {
+
+        long now = System.currentTimeMillis();
+        if ((now - currentDateGenerated) > 1000) {
+            synchronized (format) {
+                if ((now - currentDateGenerated) > 1000) {
+                    currentDateGenerated = now;
+                    currentDate = format.format(new Date(now));
+                }
+            }
+        }
+        return currentDate;
+
+    }
+
+    /**
+     * Parses date with given formatters.
+     *
+     * @param value The string to parse
+     * @param formats Array of formats to use
+     * @return Parsed date (or <code>null</code> if no formatter mached)
+     */
+    private static final Long internalParseDate
+        (String value, DateFormat[] formats) {
+        Date date = null;
+        for (int i = 0; (date == null) && (i < formats.length); i++) {
+            try {
+                date = formats[i].parse(value);
+            } catch (ParseException e) {
+                ;
+            }
+        }
+        if (date == null) {
+            return null;
+        }
+        return new Long(date.getTime());
+    }
+
+    /**
+     * Tries to parse the given date as an HTTP date. If local format list is not
+     * <code>null</code>, it's used instead.
+     *
+     * @param value The string to parse
+     * @param threadLocalformats Array of formats to use for parsing.
+     *                           If <code>null</code>, HTTP formats are used.
+     * @return Parsed date (or -1 if error occured)
+     */
+    public static final long parseDate(String value,
+                                       DateFormat[] threadLocalformats) {
+
+        Long cachedDate = null;
+        try {
+            cachedDate = (Long) parseCache.get(value);
+        } catch (Exception e) {
+        }
+        if (cachedDate != null)
+            return cachedDate.longValue();
+
+        Long date = null;
+        if (threadLocalformats != null) {
+            date = internalParseDate(value, threadLocalformats);
+            synchronized (parseCache) {
+                updateCache(parseCache, value, date);
+            }
+        } else {
+            synchronized (parseCache) {
+                date = internalParseDate(value, formats);
+                updateCache(parseCache, value, date);
+            }
+        }
+        if (date == null) {
+            return (-1L);
+        } else {
+            return date.longValue();
+        }
+
+    }
+
+    /**
+     * Updates cache.
+     *
+     * @param cache Cache to be updated
+     * @param key Key to be updated
+     * @param value New value
+     */
+    private static final void updateCache(HashMap cache, Object key,
+                                          Object value) {
+        if (value == null) {
+            return;
+        }
+        if (cache.size() > 1000) {
+            cache.clear();
+        }
+        cache.put(key, value);
+    }
+    
+}

+ 352 - 0
sandbox/src/main/java/org/acegisecurity/wrapper/redirect/SavedHttpServletRequest.java

@@ -0,0 +1,352 @@
+/* Copyright 2004, 2005 Acegi Technology Pty Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.sf.acegisecurity.wrapper.redirect;
+
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+
+import net.sf.acegisecurity.intercept.web.SecurityEnforcementFilter;
+import net.sf.acegisecurity.wrapper.SecurityContextHolderAwareRequestWrapper;
+
+/**
+ * <p>
+ * Object that saves the critical information from a request so that
+ * HTTP (header and parameter)-based authentication can reproduce it
+ * once the user has been authenticated.
+ * <p>
+ * <b>IMPLEMENTATION NOTE</b> - It is assumed that this object is accessed
+ * only from the context of a single thread, so no synchronization around
+ * internal collection classes is performed.
+ * <p>
+ * Note that SavedHttpServletRequest doesn't save uploaded file binary data, although
+ * it does save request parameters so that a POST transaction can be faithfully
+ * duplicated.  On one hand it is unfortunate to lose such type of data, but on
+ * the other hand we don't store it in session because the data can be very big
+ * and this solution can overload restricted resources.
+ * </p>
+ * <p>The original source code from Apache Tomcat<p>
+ *
+ * @see SecurityEnforcementFilter
+ * @see SecurityContextHolderAwareRequestWrapper
+ * @author Craig R. McClanahan
+ * @author Andrey Grebnev <a href="mailto:andrey.grebnev@blandware.com">&lt;andrey.grebnev@blandware.com&gt;</a>
+ * @version $Revision$ $Date$
+ */
+public class SavedHttpServletRequest {
+
+    /**
+     * This method provides ability to create SavedHttpServletRequest from HttpServletRequest
+     * @param request           request to be saved
+     * @return saved request    resulting SavedHttpServletRequest
+     */
+    public static SavedHttpServletRequest saveRequest(HttpServletRequest request) {
+        if (request.getRequestURI() == null)
+            return null;
+
+        // Create and populate a SavedHttpServletRequest object for this request
+        SavedHttpServletRequest saved = new SavedHttpServletRequest();
+        Cookie cookies[] = request.getCookies();
+        if (cookies != null) {
+            for (int i = 0; i < cookies.length; i++)
+                saved.addCookie(cookies[i]);
+        }
+        Enumeration names = request.getHeaderNames();
+        while (names.hasMoreElements()) {
+            String name = (String) names.nextElement();
+            Enumeration values = request.getHeaders(name);
+            while (values.hasMoreElements()) {
+                String value = (String) values.nextElement();
+                saved.addHeader(name, value);
+            }
+        }
+        Enumeration locales = request.getLocales();
+        while (locales.hasMoreElements()) {
+            Locale locale = (Locale) locales.nextElement();
+            saved.addLocale(locale);
+        }
+        Map parameters = request.getParameterMap();
+        Iterator paramNames = parameters.keySet().iterator();
+        while (paramNames.hasNext()) {
+            String paramName = (String) paramNames.next();
+            String paramValues[] = (String[]) parameters.get(paramName);
+            saved.addParameter(paramName, paramValues);
+        }
+        saved.setMethod(request.getMethod());
+        saved.setQueryString(request.getQueryString());
+        saved.setRequestURI(request.getRequestURI());
+//        saved.setPathInfo(request.getPathInfo());
+
+        return saved;
+    }
+
+    /**
+     * The set of Cookies associated with this Request.
+     */
+    private ArrayList cookies = new ArrayList();
+
+    /**
+     * The set of Headers associated with this Request.  Each key is a header
+     * name, while the value is a ArrayList containing one or more actual
+     * values for this header.  The values are returned as an Iterator when
+     * you ask for them.
+     */
+    private HashMap headers = new HashMap();
+
+    /**
+     * The set of Locales associated with this Request.
+     */
+    private ArrayList locales = new ArrayList();
+
+    /**
+     * The request method used on this Request.
+     */
+    private String method = null;
+
+    /**
+     * The set of request parameters associated with this Request.  Each
+     * entry is keyed by the parameter name, pointing at a String array of
+     * the corresponding values.
+     */
+    private HashMap parameters = new HashMap();
+
+    /**
+     * The request pathInfo associated with this Request.
+     */
+    private String pathInfo = null;
+
+    /**
+     * The query string associated with this Request.
+     */
+    private String queryString = null;
+
+    /**
+     * The request URI associated with this Request.
+     */
+    private String requestURI = null;
+
+    /**
+     * Adds cookie to list of cookies
+     *
+     * @param cookie cookie to add
+     */
+    public void addCookie(Cookie cookie) {
+        cookies.add(cookie);
+    }
+
+    /**
+     * Adds header
+     *
+     * @param name  header name
+     * @param value header value
+     */
+    public void addHeader(String name, String value) {
+        ArrayList values = (ArrayList) headers.get(name);
+        if (values == null) {
+            values = new ArrayList();
+            headers.put(name, values);
+        }
+        values.add(value);
+    }
+
+    /**
+     * Adds locale
+     *
+     * @param locale locale to add
+     */
+    public void addLocale(Locale locale) {
+        locales.add(locale);
+    }
+
+    /**
+     * Adds parameter
+     *
+     * @param name      parameter name
+     * @param values    parameter values
+     */
+    public void addParameter(String name, String values[]) {
+        parameters.put(name, values);
+    }
+
+    /**
+     * Returns list of cookies
+     *
+     * @return list of cookies
+     */
+    public List getCookies() {
+        return cookies;
+    }
+
+    /**
+     * Returns iterator over header names
+     *
+     * @return iterator over header names
+     */
+    public Iterator getHeaderNames() {
+        return (headers.keySet().iterator());
+    }
+
+    /**
+     * Returns iterator over header values
+     *
+     * @param name header name
+     * @return iterator over header values
+     */
+    public Iterator getHeaderValues(String name) {
+        ArrayList values = (ArrayList) headers.get(name);
+        if (values == null)
+            return ((new ArrayList()).iterator());
+        else
+            return (values.iterator());
+    }
+
+    /**
+     * Returns iterator over locales
+     *
+     * @return iterator over locales
+     */
+    public Iterator getLocales() {
+        return (locales.iterator());
+    }
+
+    /**
+     * Returns request method
+     *
+     * @return request method
+     */
+    public String getMethod() {
+        return (this.method);
+    }
+
+    /**
+     * Returns parameters
+     *
+     * @return parameters map
+     */
+    public Map getParameterMap() {
+        return parameters;
+    }
+
+    /**
+     * Returns iterator over parameter names
+     *
+     * @return iterator over parameter names
+     */
+    public Iterator getParameterNames() {
+        return (parameters.keySet().iterator());
+    }
+
+    /**
+     * Returns parameter values
+     *
+     * @param name parameter name
+     * @return parameter values
+     */
+    public String[] getParameterValues(String name) {
+        return ((String[]) parameters.get(name));
+    }
+
+    /**
+     * Returns path info
+     *
+     * @return path info
+     */
+    public String getPathInfo() {
+        return pathInfo;
+    }
+
+    /**
+     * Returns query string
+     *
+     * @return query string
+     */
+    public String getQueryString() {
+        return (this.queryString);
+    }
+
+    /**
+     * Returns request URI
+     *
+     * @return request URI
+     */
+    public String getRequestURI() {
+        return (this.requestURI);
+    }
+
+    /**
+     * Gets uri with path info and query string
+     *
+     * @return uri with path info and query string
+     */
+    public String getRequestURL() {
+        if (getRequestURI() == null)
+            return null;
+
+        StringBuffer sb = new StringBuffer(getRequestURI());
+//        if (getPathInfo() != null) {
+//            sb.append(getPathInfo());
+//        }
+        if (getQueryString() != null) {
+            sb.append('?');
+            sb.append(getQueryString());
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Sets request method
+     *
+     * @param method request method to set
+     */
+    public void setMethod(String method) {
+        this.method = method;
+    }
+
+    /**
+     * Sets path info
+     *
+     * @param pathInfo path info to set
+     */
+    public void setPathInfo(String pathInfo) {
+        this.pathInfo = pathInfo;
+    }
+
+    /**
+     * Sets query string
+     *
+     * @param queryString query string to set
+     */
+    public void setQueryString(String queryString) {
+        this.queryString = queryString;
+    }
+
+    /**
+     * Sets request URI
+     *
+     * @param requestURI request URI to set
+     */
+    public void setRequestURI(String requestURI) {
+        this.requestURI = requestURI;
+    }
+
+}

+ 8 - 0
sandbox/src/main/java/org/acegisecurity/wrapper/redirect/package.html

@@ -0,0 +1,8 @@
+<html>
+<body>
+Redirection helper code for
+<code>SecurityContextHolderAwareRequestWrapper</code>
+and
+<code>SecurityEnforcementFilter</code>.
+</body>
+</html>