View Javadoc

    1   /* Copyright 2004, 2005 Acegi Technology Pty Limited
    2    *
    3    * Licensed under the Apache License, Version 2.0 (the "License");
    4    * you may not use this file except in compliance with the License.
    5    * You may obtain a copy of the License at
    6    *
    7    *     http://www.apache.org/licenses/LICENSE-2.0
    8    *
    9    * Unless required by applicable law or agreed to in writing, software
    10   * distributed under the License is distributed on an "AS IS" BASIS,
    11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12   * See the License for the specific language governing permissions and
    13   * limitations under the License.
    14   */
    15  package org.acegisecurity.intercept.web;
    16
    17  import org.acegisecurity.AccessDeniedException;
    18  import org.acegisecurity.AuthenticationException;
    19  import org.acegisecurity.AuthenticationTrustResolver;
    20  import org.acegisecurity.AuthenticationTrustResolverImpl;
    21  import org.acegisecurity.InsufficientAuthenticationException;
    22  import org.acegisecurity.context.SecurityContextHolder;
    23  import org.acegisecurity.ui.AbstractProcessingFilter;
    24  import org.acegisecurity.util.PortResolver;
    25  import org.acegisecurity.util.PortResolverImpl;
    26
    27  import org.apache.commons.logging.Log;
    28  import org.apache.commons.logging.LogFactory;
    29
    30  import org.springframework.beans.factory.InitializingBean;
    31
    32  import org.springframework.util.Assert;
    33
    34  import java.io.IOException;
    35
    36  import javax.servlet.Filter;
    37  import javax.servlet.FilterChain;
    38  import javax.servlet.FilterConfig;
    39  import javax.servlet.ServletException;
    40  import javax.servlet.ServletRequest;
    41  import javax.servlet.ServletResponse;
    42  import javax.servlet.http.HttpServletRequest;
    43  import javax.servlet.http.HttpServletResponse;
    44
    45
    46  /***
    47   * Wraps requests to the {@link FilterSecurityInterceptor}.
    48   *
    49   * <p>
    50   * This filter is necessary because it provides the bridge between incoming
    51   * requests and the <code>FilterSecurityInterceptor</code> instance.
    52   * </p>
    53   *
    54   * <p>
    55   * If an {@link AuthenticationException} is detected, the filter will launch
    56   * the <code>authenticationEntryPoint</code>. This allows common handling of
    57   * authentication failures originating from any subclass of {@link
    58   * org.acegisecurity.intercept.AbstractSecurityInterceptor}.
    59   * </p>
    60   *
    61   * <p>
    62   * If an {@link AccessDeniedException} is detected, the filter will determine
    63   * whether or not the user is an anonymous user. If they are an anonymous
    64   * user, the <code>authenticationEntryPoint</code> will be launched. If they
    65   * are not an anonymous user, the filter will respond with a
    66   * <code>HttpServletResponse.SC_FORBIDDEN</code> (403 error).  In addition,
    67   * the <code>AccessDeniedException</code> itself will be placed in the
    68   * <code>HttpSession</code> attribute keyed against {@link
    69   * #ACEGI_SECURITY_ACCESS_DENIED_EXCEPTION_KEY} (to allow access to the stack
    70   * trace etc). Again, this allows common access denied handling irrespective
    71   * of the originating security interceptor.
    72   * </p>
    73   *
    74   * <p>
    75   * To use this filter, it is necessary to specify the following properties:
    76   * </p>
    77   *
    78   * <ul>
    79   * <li>
    80   * <code>filterSecurityInterceptor</code> indicates the
    81   * <code>FilterSecurityInterceptor</code> to delegate HTTP security decisions
    82   * to.
    83   * </li>
    84   * <li>
    85   * <code>authenticationEntryPoint</code> indicates the handler that should
    86   * commence the authentication process if an
    87   * <code>AuthenticationException</code> is detected. Note that this may also
    88   * switch the current protocol from http to https for an SSL login.
    89   * </li>
    90   * <li>
    91   * <code>portResolver</code> is used to determine the "real" port that a
    92   * request was received on.
    93   * </li>
    94   * </ul>
    95   *
    96   * <P>
    97   * <B>Do not use this class directly.</B> Instead configure
    98   * <code>web.xml</code> to use the {@link
    99   * org.acegisecurity.util.FilterToBeanProxy}.
    100  * </p>
    101  *
    102  * @author Ben Alex
    103  * @author colin sampaleanu
    104  * @version $Id: SecurityEnforcementFilter.java,v 1.22 2005/11/25 04:38:18 benalex Exp $
    105  */
    106 public class SecurityEnforcementFilter implements Filter, InitializingBean {
    107     private static final Log logger = LogFactory.getLog(SecurityEnforcementFilter.class);
    108     public static final String ACEGI_SECURITY_ACCESS_DENIED_EXCEPTION_KEY = "ACEGI_SECURITY_403_EXCEPTION";
    109     private AuthenticationEntryPoint authenticationEntryPoint;
    110     private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl();
    111     private FilterSecurityInterceptor filterSecurityInterceptor;
    112     private PortResolver portResolver = new PortResolverImpl();
    113     private boolean createSessionAllowed = true;
    114
    115     public void setAuthenticationEntryPoint(
    116         AuthenticationEntryPoint authenticationEntryPoint) {
    117         this.authenticationEntryPoint = authenticationEntryPoint;
    118     }
    119
    120     public AuthenticationEntryPoint getAuthenticationEntryPoint() {
    121         return authenticationEntryPoint;
    122     }
    123
    124     public void setAuthenticationTrustResolver(
    125         AuthenticationTrustResolver authenticationTrustResolver) {
    126         this.authenticationTrustResolver = authenticationTrustResolver;
    127     }
    128
    129     /***
    130      * If <code>true</code>, indicates that <code>SecurityEnforcementFilter</code> is permitted
    131      * to store the target URL and exception information in the <code>HttpSession</code> (the
    132      * default). In situations where you do not wish to unnecessarily create <code>HttpSession</code>s
    133      * - because the user agent will know the failed URL, such as with BASIC or Digest authentication
    134      * - you may wish to set this property to <code>false</code>. Remember to also set the
    135      * {@link org.acegisecurity.context.HttpSessionContextIntegrationFilter#allowSessionCreation}
    136      * to <code>false</code> if you set this property to <code>false</code>.
    137      *
    138      * @return <code>true</code> if the <code>HttpSession</code> will be used to store information
    139      * about the failed request, <code>false</code> if the <code>HttpSession</code> will not be
    140      * used
    141      */
    142     public boolean isCreateSessionAllowed() {
    143         return createSessionAllowed;
    144     }
    145
    146     public void setCreateSessionAllowed(boolean createSessionAllowed) {
    147         this.createSessionAllowed = createSessionAllowed;
    148     }
    149
    150     public AuthenticationTrustResolver getAuthenticationTrustResolver() {
    151         return authenticationTrustResolver;
    152     }
    153
    154     public void setFilterSecurityInterceptor(
    155         FilterSecurityInterceptor filterSecurityInterceptor) {
    156         this.filterSecurityInterceptor = filterSecurityInterceptor;
    157     }
    158
    159     public FilterSecurityInterceptor getFilterSecurityInterceptor() {
    160         return filterSecurityInterceptor;
    161     }
    162
    163     public void setPortResolver(PortResolver portResolver) {
    164         this.portResolver = portResolver;
    165     }
    166
    167     public PortResolver getPortResolver() {
    168         return portResolver;
    169     }
    170
    171     public void afterPropertiesSet() throws Exception {
    172         Assert.notNull(authenticationEntryPoint,
    173             "authenticationEntryPoint must be specified");
    174         Assert.notNull(filterSecurityInterceptor,
    175             "filterSecurityInterceptor must be specified");
    176         Assert.notNull(portResolver, "portResolver must be specified");
    177         Assert.notNull(authenticationTrustResolver,
    178             "authenticationTrustResolver must be specified");
    179     }
    180
    181     public void destroy() {
    182     }
    183
    184     public void doFilter(ServletRequest request, ServletResponse response,
    185         FilterChain chain) throws IOException, ServletException {
    186         if (!(request instanceof HttpServletRequest)) {
    187             throw new ServletException("HttpServletRequest required");
    188         }
    189
    190         if (!(response instanceof HttpServletResponse)) {
    191             throw new ServletException("HttpServletResponse required");
    192         }
    193
    194         FilterInvocation fi = new FilterInvocation(request, response, chain);
    195
    196         try {
    197             filterSecurityInterceptor.invoke(fi);
    198
    199             if (logger.isDebugEnabled()) {
    200                 logger.debug("Chain processed normally");
    201             }
    202         } catch (AuthenticationException authentication) {
    203             if (logger.isDebugEnabled()) {
    204                 logger.debug("Authentication exception occurred; redirecting to authentication entry point",
    205                     authentication);
    206             }
    207
    208             sendStartAuthentication(fi, authentication);
    209         } catch (AccessDeniedException accessDenied) {
    210             if (authenticationTrustResolver.isAnonymous(
    211                         SecurityContextHolder.getContext().getAuthentication())) {
    212                 if (logger.isDebugEnabled()) {
    213                     logger.debug("Access is denied (user is anonymous); redirecting to authentication entry point",
    214                         accessDenied);
    215                 }
    216
    217                 sendStartAuthentication(fi,
    218                     new InsufficientAuthenticationException(
    219                         "Full authentication is required to access this resource"));
    220             } else {
    221                 if (logger.isDebugEnabled()) {
    222                     logger.debug("Access is denied (user is not anonymous); sending back forbidden response",
    223                         accessDenied);
    224                 }
    225
    226                 sendAccessDeniedError(fi, accessDenied);
    227             }
    228         } catch (ServletException e) {
    229             throw e;
    230         } catch (IOException e) {
    231             throw e;
    232         } catch (Throwable otherException) {
    233             throw new ServletException(otherException);
    234         }
    235     }
    236
    237     public void init(FilterConfig filterConfig) throws ServletException {
    238     }
    239
    240     protected void sendAccessDeniedError(FilterInvocation fi,
    241         AccessDeniedException accessDenied)
    242         throws ServletException, IOException {
    243         if (createSessionAllowed) {
    244             ((HttpServletRequest) fi.getRequest()).getSession().setAttribute(ACEGI_SECURITY_ACCESS_DENIED_EXCEPTION_KEY,
    245                 accessDenied);
    246         }
    247
    248         ((HttpServletResponse) fi.getResponse()).sendError(HttpServletResponse.SC_FORBIDDEN,
    249             accessDenied.getMessage()); // 403
    250     }
    251
    252     protected void sendStartAuthentication(FilterInvocation fi,
    253         AuthenticationException reason) throws ServletException, IOException {
    254         HttpServletRequest request = (HttpServletRequest) fi.getRequest();
    255
    256         int port = portResolver.getServerPort(request);
    257         boolean includePort = true;
    258
    259         if ("http".equals(request.getScheme().toLowerCase()) && (port == 80)) {
    260             includePort = false;
    261         }
    262
    263         if ("https".equals(request.getScheme().toLowerCase()) && (port == 443)) {
    264             includePort = false;
    265         }
    266
    267         String targetUrl = request.getScheme() + "://" +
    268             request.getServerName() + ((includePort) ? (":" + port) : "") +
    269             request.getContextPath() + fi.getRequestUrl();
    270
    271         if (logger.isDebugEnabled()) {
    272             logger.debug(
    273                 "Authentication entry point being called; target URL added to Session: " +
    274                 targetUrl);
    275         }
    276
    277         if (createSessionAllowed) {
    278             ((HttpServletRequest) request).getSession().setAttribute(AbstractProcessingFilter.ACEGI_SECURITY_TARGET_URL_KEY,
    279                 targetUrl);
    280         }
    281
    282         // SEC-112: Clear the SecurityContextHolder's Authentication, as the
    283         // existing Authentication is no longer considered valid
    284         SecurityContextHolder.getContext().setAuthentication(null);
    285
    286         authenticationEntryPoint.commence(request,
    287             (HttpServletResponse) fi.getResponse(), reason);
    288     }
    289 }