瀏覽代碼

Filter to ensure web requests are received over a suitable secure channel.

Ben Alex 21 年之前
父節點
當前提交
2c97583f27

+ 4 - 0
build.xml

@@ -183,6 +183,7 @@
 				<include name="net/sf/acegisecurity/vote/**"/>
 				<include name="net/sf/acegisecurity/ui/**"/>
 				<include name="net/sf/acegisecurity/util/**"/>
+				<include name="net/sf/acegisecurity/securechannel/**"/>
 				<include name="net/sf/acegisecurity/intercept/**"/>
 				<include name="net/sf/acegisecurity/adapters/*"/>
 				<include name="net/sf/acegisecurity/adapters/catalina/*"/>
@@ -204,6 +205,7 @@
 				<include name="net/sf/acegisecurity/vote/**"/>
 				<include name="net/sf/acegisecurity/ui/**"/>
 				<include name="net/sf/acegisecurity/util/**"/>
+				<include name="net/sf/acegisecurity/securechannel/**"/>
 				<include name="net/sf/acegisecurity/intercept/**"/>
 				<include name="net/sf/acegisecurity/adapters/*"/>
 				<include name="net/sf/acegisecurity/adapters/jetty/*"/>
@@ -224,6 +226,7 @@
 				<include name="net/sf/acegisecurity/vote/**"/>
 				<include name="net/sf/acegisecurity/ui/**"/>
 				<include name="net/sf/acegisecurity/util/**"/>
+				<include name="net/sf/acegisecurity/securechannel/**"/>
 				<include name="net/sf/acegisecurity/intercept/**"/>
 				<include name="net/sf/acegisecurity/adapters/*"/>
 				<include name="net/sf/acegisecurity/adapters/jboss/*"/>
@@ -244,6 +247,7 @@
 				<include name="net/sf/acegisecurity/vote/**"/>
 				<include name="net/sf/acegisecurity/ui/**"/>
 				<include name="net/sf/acegisecurity/util/**"/>
+				<include name="net/sf/acegisecurity/securechannel/**"/>
 				<include name="net/sf/acegisecurity/intercept/**"/>
 				<include name="net/sf/acegisecurity/adapters/*"/>
 				<include name="net/sf/acegisecurity/adapters/resin/*"/>

+ 38 - 0
core/src/main/java/org/acegisecurity/securechannel/ChannelDecisionManager.java

@@ -0,0 +1,38 @@
+/* Copyright 2004 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.securechannel;
+
+import net.sf.acegisecurity.ConfigAttributeDefinition;
+import net.sf.acegisecurity.intercept.web.FilterInvocation;
+
+
+/**
+ * Decides whether a web channel provides sufficient security.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public interface ChannelDecisionManager {
+    //~ Methods ================================================================
+
+    /**
+     * Decided whether the presented {@link FilterInvocation} provides
+     * sufficient security based on the requested {@link
+     * ConfigAttributeDefinition}.
+     */
+    public void decide(FilterInvocation invocation,
+        ConfigAttributeDefinition config) throws SecureChannelRequiredException;
+}

+ 83 - 0
core/src/main/java/org/acegisecurity/securechannel/ChannelDecisionManagerImpl.java

@@ -0,0 +1,83 @@
+/* Copyright 2004 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.securechannel;
+
+import net.sf.acegisecurity.ConfigAttribute;
+import net.sf.acegisecurity.ConfigAttributeDefinition;
+import net.sf.acegisecurity.intercept.web.FilterInvocation;
+
+import org.springframework.beans.factory.InitializingBean;
+
+import java.util.Iterator;
+
+
+/**
+ * <p>
+ * Requires a secure channel for a web request if a  {@link
+ * ConfigAttribute#getAttribute()} keyword is detected.
+ * </p>
+ * 
+ * <P>
+ * The default keyword string is <Code>REQUIRES_SECURE_CHANNEL</code>, but this
+ * may be overriden to any value. The <code>ConfigAttribute</code> must
+ * exactly match the case of the keyword string.
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class ChannelDecisionManagerImpl implements InitializingBean,
+    ChannelDecisionManager {
+    //~ Instance fields ========================================================
+
+    private String keyword = "REQUIRES_SECURE_CHANNEL";
+
+    //~ Methods ================================================================
+
+    public void setKeyword(String keyword) {
+        this.keyword = keyword;
+    }
+
+    public String getKeyword() {
+        return keyword;
+    }
+
+    public void afterPropertiesSet() throws Exception {
+        if ((keyword == null) || "".equals(keyword)) {
+            throw new IllegalArgumentException("keyword required");
+        }
+    }
+
+    public void decide(FilterInvocation invocation,
+        ConfigAttributeDefinition config) throws SecureChannelRequiredException {
+        if ((invocation == null) || (config == null)) {
+            throw new IllegalArgumentException("Nulls cannot be provided");
+        }
+
+        Iterator iter = config.getConfigAttributes();
+
+        while (iter.hasNext()) {
+            ConfigAttribute attribute = (ConfigAttribute) iter.next();
+
+            if (attribute.equals(keyword)) {
+                if (!invocation.getHttpRequest().isSecure()) {
+                    throw new SecureChannelRequiredException(
+                        "Request is not being made over a secure channel");
+                }
+            }
+        }
+    }
+}

+ 49 - 0
core/src/main/java/org/acegisecurity/securechannel/ChannelEntryPoint.java

@@ -0,0 +1,49 @@
+/* Copyright 2004 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.securechannel;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+
+
+/**
+ * Used by {@link ChannelProcessingFilter} to launch a secure web channel.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public interface ChannelEntryPoint {
+    //~ Methods ================================================================
+
+    /**
+     * Commences a secure channel.
+     * 
+     * <P>
+     * Implementations should modify the headers on the
+     * <code>ServletResponse</code> as necessary to commence the user agent
+     * using the secure channel.
+     * </p>
+     *
+     * @param request that resulted in a
+     *        <code>SecureChannelRequiredException</code>
+     * @param response so that the user agent can begin using a secure channel
+     */
+    public void commence(ServletRequest request, ServletResponse response)
+        throws IOException, ServletException;
+}

+ 152 - 0
core/src/main/java/org/acegisecurity/securechannel/ChannelProcessingFilter.java

@@ -0,0 +1,152 @@
+/* Copyright 2004 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.securechannel;
+
+import net.sf.acegisecurity.ConfigAttributeDefinition;
+import net.sf.acegisecurity.intercept.web.FilterInvocation;
+import net.sf.acegisecurity.intercept.web.FilterInvocationDefinitionSource;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.beans.factory.InitializingBean;
+
+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;
+
+
+/**
+ * Ensures a request is delivered over a secure channel.
+ * 
+ * <p>
+ * Internally uses a {@link FilterInvocation} to represent the request, so that
+ * the <code>FilterInvocation</code>-related property editors and lookup
+ * classes can be used.
+ * </p>
+ * 
+ * <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
+ * @version $Id$
+ */
+public class ChannelProcessingFilter implements InitializingBean, Filter {
+    //~ Static fields/initializers =============================================
+
+    private static final Log logger = LogFactory.getLog(ChannelProcessingFilter.class);
+
+    //~ Instance fields ========================================================
+
+    private ChannelDecisionManager channelDecisionManager;
+    private ChannelEntryPoint channelEntryPoint;
+    private FilterInvocationDefinitionSource filterInvocationDefinitionSource;
+
+    //~ Methods ================================================================
+
+    public void setChannelDecisionManager(
+        ChannelDecisionManager channelDecisionManager) {
+        this.channelDecisionManager = channelDecisionManager;
+    }
+
+    public ChannelDecisionManager getChannelDecisionManager() {
+        return channelDecisionManager;
+    }
+
+    public void setChannelEntryPoint(ChannelEntryPoint channelEntryPoint) {
+        this.channelEntryPoint = channelEntryPoint;
+    }
+
+    public ChannelEntryPoint getChannelEntryPoint() {
+        return channelEntryPoint;
+    }
+
+    public void setFilterInvocationDefinitionSource(
+        FilterInvocationDefinitionSource filterInvocationDefinitionSource) {
+        this.filterInvocationDefinitionSource = filterInvocationDefinitionSource;
+    }
+
+    public FilterInvocationDefinitionSource getFilterInvocationDefinitionSource() {
+        return filterInvocationDefinitionSource;
+    }
+
+    public void afterPropertiesSet() throws Exception {
+        if (filterInvocationDefinitionSource == null) {
+            throw new IllegalArgumentException(
+                "filterInvocationDefinitionSource must be specified");
+        }
+
+        if (channelDecisionManager == null) {
+            throw new IllegalArgumentException(
+                "channelDecisionManager must be specified");
+        }
+
+        if (channelEntryPoint == null) {
+            throw new IllegalArgumentException(
+                "channelEntryPoint 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);
+        ConfigAttributeDefinition attr = this.filterInvocationDefinitionSource
+            .getAttributes(fi);
+
+        if (attr != null) {
+            if (logger.isDebugEnabled()) {
+                logger.debug("Request : " + request.toString()
+                    + "; ConfigAttributes: " + attr.toString());
+            }
+
+            try {
+                channelDecisionManager.decide(fi, attr);
+            } catch (SecureChannelRequiredException channelException) {
+                if (logger.isDebugEnabled()) {
+                    logger.debug("Channel insufficient ("
+                        + channelException.getMessage()
+                        + "); delegating to channelEntryPoint");
+                }
+
+                channelEntryPoint.commence(request, response);
+            }
+        }
+
+        chain.doFilter(request, response);
+    }
+
+    public void init(FilterConfig filterConfig) throws ServletException {}
+}

+ 158 - 0
core/src/main/java/org/acegisecurity/securechannel/RetryWithHttpsEntryPoint.java

@@ -0,0 +1,158 @@
+/* Copyright 2004 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.securechannel;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.beans.factory.InitializingBean;
+
+import java.io.IOException;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+
+/**
+ * Commences a secure channel by retrying the original request using HTTPS.
+ * 
+ * <P>
+ * This entry point should suffice in most circumstances. However, it is not
+ * intended to properly handle HTTP POSTs or other usage where a standard
+ * redirect would cause an issue.
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class RetryWithHttpsEntryPoint implements InitializingBean,
+    ChannelEntryPoint {
+    //~ Static fields/initializers =============================================
+
+    private static final Log logger = LogFactory.getLog(RetryWithHttpsEntryPoint.class);
+
+    //~ Instance fields ========================================================
+
+    private Map httpsPortMappings;
+
+    //~ Constructors ===========================================================
+
+    public RetryWithHttpsEntryPoint() {
+        httpsPortMappings = new HashMap();
+        httpsPortMappings.put(new Integer(80), new Integer(443));
+        httpsPortMappings.put(new Integer(8080), new Integer(8443));
+    }
+
+    //~ Methods ================================================================
+
+    /**
+     * <p>
+     * Set to override the default http port to https port mappings of 80:443,
+     * and  8080:8443.
+     * </p>
+     * In a Spring XML ApplicationContext, a definition would look something
+     * like this:
+     * <pre>
+     *   &lt;property name="httpsPortMapping">
+     *     &lt;map>
+     *       &lt;entry key="80">&lt;value>443&lt;/value>&lt;/entry>
+     *       &lt;entry key="8080">&lt;value>8443&lt;/value>&lt;/entry>
+     *     &lt;/map>
+     *   &lt;/property>
+     * </pre>
+     *
+     * @param newMappings A Map consisting of String keys and String values,
+     *        where for each entry the key is the string representation of an
+     *        integer http port number, and the value is the string
+     *        representation of the corresponding integer https port number.
+     *
+     * @throws IllegalArgumentException if input map does not consist of String
+     *         keys and values, each representing an integer port number in
+     *         the range 1-65535 for that mapping.
+     */
+    public void setHttpsPortMappings(HashMap newMappings) {
+        httpsPortMappings.clear();
+
+        Iterator it = newMappings.entrySet().iterator();
+
+        while (it.hasNext()) {
+            Map.Entry entry = (Map.Entry) it.next();
+            Integer httpPort = new Integer((String) entry.getKey());
+            Integer httpsPort = new Integer((String) entry.getValue());
+
+            if ((httpPort.intValue() < 1) || (httpPort.intValue() > 65535)
+                || (httpsPort.intValue() < 1) || (httpsPort.intValue() > 65535)) {
+                throw new IllegalArgumentException(
+                    "one or both ports out of legal range: " + httpPort + ", "
+                    + httpsPort);
+            }
+
+            httpsPortMappings.put(httpPort, httpsPort);
+
+            if (httpsPortMappings.size() < 1) {
+                throw new IllegalArgumentException("must map at least one port");
+            }
+        }
+    }
+
+    public void afterPropertiesSet() throws Exception {
+        if (httpsPortMappings == null) {
+            throw new IllegalArgumentException("httpsPortMappings required");
+        }
+    }
+
+    public void commence(ServletRequest request, ServletResponse response)
+        throws IOException, ServletException {
+        HttpServletRequest req = (HttpServletRequest) request;
+
+        String pathInfo = req.getPathInfo();
+        String queryString = req.getQueryString();
+        String contextPath = req.getContextPath();
+        String destination = req.getServletPath()
+            + ((pathInfo == null) ? "" : pathInfo)
+            + ((queryString == null) ? "" : ("?" + queryString));
+
+        String redirectUrl = contextPath;
+
+        Integer httpPort = new Integer(req.getServerPort());
+        Integer httpsPort = (Integer) httpsPortMappings.get(httpPort);
+
+        if (httpsPort != null) {
+            String serverName = req.getServerName();
+            redirectUrl = "https://" + serverName + ":" + httpsPort
+                + contextPath + destination;
+        }
+
+        ((HttpServletResponse) response).sendRedirect(redirectUrl);
+    }
+
+    /**
+     * Returns the translated (Integer -> Integer) version of the original port
+     * mapping specified via setHttpsPortMapping()
+     *
+     * @return DOCUMENT ME!
+     */
+    protected Map getTranslatedHttpsPortMappings() {
+        return httpsPortMappings;
+    }
+}

+ 50 - 0
core/src/main/java/org/acegisecurity/securechannel/SecureChannelRequiredException.java

@@ -0,0 +1,50 @@
+/* Copyright 2004 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.securechannel;
+
+import net.sf.acegisecurity.AccessDeniedException;
+
+
+/**
+ * Thrown if a secure web channel is required, but is not present.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class SecureChannelRequiredException extends AccessDeniedException {
+    //~ Constructors ===========================================================
+
+    /**
+     * Constructs a <code>SecureChannelRequiredException</code> with the
+     * specified message.
+     *
+     * @param msg the detail message.
+     */
+    public SecureChannelRequiredException(String msg) {
+        super(msg);
+    }
+
+    /**
+     * Constructs a <code>SecureChannelRequiredException</code> with the
+     * specified message and root cause.
+     *
+     * @param msg the detail message.
+     * @param t root cause
+     */
+    public SecureChannelRequiredException(String msg, Throwable t) {
+        super(msg, t);
+    }
+}

+ 6 - 0
core/src/main/java/org/acegisecurity/securechannel/package.html

@@ -0,0 +1,6 @@
+<html>
+<body>
+Classes that ensure web requests are received over required
+transport channels.
+</body>
+</html>

+ 19 - 0
samples/contacts/etc/filter/applicationContext.xml

@@ -133,6 +133,25 @@
 
 	<bean id="backendContactManagerTarget" class="sample.contact.ContactManagerBackend"/>
 
+	<!-- ===================== HTTP CHANNEL REQUIREMENTS ==================== -->
+
+	<bean id="channelProcessingFilter" class="net.sf.acegisecurity.securechannel.ChannelProcessingFilter">
+		<property name="channelDecisionManager"><ref bean="channelDecisionManager"/></property>
+		<property name="channelEntryPoint"><ref bean="channelEntryPoint"/></property>
+ 		<property name="filterInvocationDefinitionSource">
+			<value>
+			    CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
+				\A/secure/.*\Z=REQUIRES_SECURE_CHANNEL
+				\A/info/.*\Z=REQUIRES_SECURE_CHANNEL
+				\A/acegilogin.jsp.*\Z=REQUIRES_SECURE_CHANNEL
+			</value>
+		</property>
+	</bean>
+
+	<bean id="channelDecisionManager" class="net.sf.acegisecurity.securechannel.ChannelDecisionManagerImpl"/>
+
+	<bean id="channelEntryPoint" class="net.sf.acegisecurity.securechannel.RetryWithHttpsEntryPoint"/>
+
 	<!-- ===================== HTTP REQUEST SECURITY ==================== -->
 
 	<bean id="authenticationProcessingFilter" class="net.sf.acegisecurity.ui.webapp.AuthenticationProcessingFilter">

+ 14 - 0
samples/contacts/etc/filter/web.xml

@@ -24,6 +24,15 @@
 		<param-value>/WEB-INF/applicationContext.xml</param-value>
 	</context-param>
 
+    <filter>
+        <filter-name>Acegi Channel Processing Filter</filter-name>
+        <filter-class>net.sf.acegisecurity.util.FilterToBeanProxy</filter-class>
+        <init-param>
+            <param-name>targetClass</param-name>
+            <param-value>net.sf.acegisecurity.securechannel.ChannelProcessingFilter</param-value>
+        </init-param>
+    </filter>
+
     <filter>
         <filter-name>Acegi Authentication Processing Filter</filter-name>
         <filter-class>net.sf.acegisecurity.util.FilterToBeanProxy</filter-class>
@@ -56,6 +65,11 @@
         </init-param>
     </filter>
 
+    <filter-mapping>
+      <filter-name>Acegi Channel Processing Filter</filter-name>
+      <url-pattern>/*</url-pattern>
+    </filter-mapping>
+
     <filter-mapping>
       <filter-name>Acegi Authentication Processing Filter</filter-name>
       <url-pattern>/*</url-pattern>