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
    16  package org.acegisecurity.ui;
    17
    18  import java.io.IOException;
    19  import java.util.Properties;
    20
    21  import javax.servlet.Filter;
    22  import javax.servlet.FilterChain;
    23  import javax.servlet.FilterConfig;
    24  import javax.servlet.ServletException;
    25  import javax.servlet.ServletRequest;
    26  import javax.servlet.ServletResponse;
    27  import javax.servlet.http.HttpServletRequest;
    28  import javax.servlet.http.HttpServletResponse;
    29
    30  import org.acegisecurity.AcegiMessageSource;
    31  import org.acegisecurity.Authentication;
    32  import org.acegisecurity.AuthenticationException;
    33  import org.acegisecurity.AuthenticationManager;
    34  import org.acegisecurity.context.SecurityContextHolder;
    35  import org.acegisecurity.event.authentication.InteractiveAuthenticationSuccessEvent;
    36  import org.acegisecurity.ui.rememberme.NullRememberMeServices;
    37  import org.acegisecurity.ui.rememberme.RememberMeServices;
    38  import org.apache.commons.logging.Log;
    39  import org.apache.commons.logging.LogFactory;
    40  import org.springframework.beans.factory.InitializingBean;
    41  import org.springframework.context.ApplicationEventPublisher;
    42  import org.springframework.context.ApplicationEventPublisherAware;
    43  import org.springframework.context.MessageSource;
    44  import org.springframework.context.MessageSourceAware;
    45  import org.springframework.context.support.MessageSourceAccessor;
    46  import org.springframework.util.Assert;
    47
    48
    49  /***
    50   * Abstract processor of browser-based HTTP-based authentication requests.
    51   *
    52   * <p>
    53   * This filter is responsible for processing authentication requests. If
    54   * authentication is successful, the resulting {@link Authentication} object
    55   * will be placed into the <code>SecurityContext</code>, which is guaranteed
    56   * to have already been created by an earlier filter.
    57   * </p>
    58   *
    59   * <p>
    60   * If authentication fails, the <code>AuthenticationException</code> will be
    61   * placed into the <code>HttpSession</code> with the attribute defined by
    62   * {@link #ACEGI_SECURITY_LAST_EXCEPTION_KEY}.
    63   * </p>
    64   *
    65   * <p>
    66   * To use this filter, it is necessary to specify the following properties:
    67   * </p>
    68   *
    69   * <ul>
    70   * <li>
    71   * <code>defaultTargetUrl</code> indicates the URL that should be used for
    72   * redirection if the <code>HttpSession</code> attribute named {@link
    73   * #ACEGI_SECURITY_TARGET_URL_KEY} does not indicate the target URL once
    74   * authentication is completed successfully. eg: <code>/</code>. This will be
    75   * treated as relative to the web-app's context path, and should include the
    76   * leading <code>/</code>.
    77   * </li>
    78   * <li>
    79   * <code>authenticationFailureUrl</code> indicates the URL that should be used
    80   * for redirection if the authentication request fails. eg:
    81   * <code>/login.jsp?login_error=1</code>.
    82   * </li>
    83   * <li>
    84   * <code>filterProcessesUrl</code> indicates the URL that this filter will
    85   * respond to. This parameter varies by subclass.
    86   * </li>
    87   * <li>
    88   * <code>alwaysUseDefaultTargetUrl</code> causes successful authentication to
    89   * always redirect to the <code>defaultTargetUrl</code>, even if the
    90   * <code>HttpSession</code> attribute named {@link
    91   * #ACEGI_SECURITY_TARGET_URL_KEY} defines the intended target URL.
    92   * </li>
    93   * </ul>
    94   *
    95   * <p>
    96   * To configure this filter to redirect to specific pages as the result of
    97   * specific {@link AuthenticationException}s you can do the following.
    98   * Configure the <code>exceptionMappings</code> property in your application
    99   * xml. This property is a java.util.Properties object that maps a
    100  * fully-qualified exception class name to a redirection url target.<br>
    101  * For example:<br>
    102  * <code> &lt;property name="exceptionMappings"&gt;<br>
    103  * &nbsp;&nbsp;&lt;props&gt;<br>
    104  * &nbsp;&nbsp;&nbsp;&nbsp;&lt;prop&gt; key="org.acegisecurity.BadCredentialsException"&gt;/bad_credentials.jsp&lt;/prop&gt;<br>
    105  * &nbsp;&nbsp;&lt;/props&gt;<br>
    106  * &lt;/property&gt;<br>
    107  * </code><br>
    108  * The example above would redirect all {@link
    109  * org.acegisecurity.BadCredentialsException}s thrown, to a page in the
    110  * web-application called /bad_credentials.jsp.
    111  * </p>
    112  *
    113  * <p>
    114  * Any {@link AuthenticationException} thrown that cannot be matched in the
    115  * <code>exceptionMappings</code> will be redirected to the
    116  * <code>authenticationFailureUrl</code>
    117  * </p>
    118  *
    119  * <p>
    120  * If authentication is successful, an {@link
    121  * org.acegisecurity.event.authentication.InteractiveAuthenticationSuccessEvent}
    122  * will be published to the application context. No events will be published
    123  * if authentication was unsuccessful, because this would generally be
    124  * recorded via an <code>AuthenticationManager</code>-specific application
    125  * event.
    126  * </p>
    127  */
    128 public abstract class AbstractProcessingFilter implements Filter,
    129     InitializingBean, ApplicationEventPublisherAware, MessageSourceAware {
    130     //~ Static fields/initializers =============================================
    131
    132     public static final String ACEGI_SECURITY_TARGET_URL_KEY = "ACEGI_SECURITY_TARGET_URL";
    133     public static final String ACEGI_SECURITY_LAST_EXCEPTION_KEY = "ACEGI_SECURITY_LAST_EXCEPTION";
    134     protected static final Log logger = LogFactory.getLog(AbstractProcessingFilter.class);
    135
    136     //~ Instance fields ========================================================
    137
    138     private ApplicationEventPublisher eventPublisher;
    139     private AuthenticationManager authenticationManager;
    140     protected MessageSourceAccessor messages = AcegiMessageSource.getAccessor();
    141     private Properties exceptionMappings = new Properties();
    142     private RememberMeServices rememberMeServices = new NullRememberMeServices();
    143
    144     /*** Where to redirect the browser to if authentication fails */
    145     private String authenticationFailureUrl;
    146
    147     /***
    148      * Where to redirect the browser to if authentication is successful but
    149      * ACEGI_SECURITY_TARGET_URL_KEY is <code>null</code>
    150      */
    151     private String defaultTargetUrl;
    152
    153     /***
    154      * The URL destination that this filter intercepts and processes (usually
    155      * something like <code>/j_acegi_security_check</code>)
    156      */
    157     private String filterProcessesUrl = getDefaultFilterProcessesUrl();
    158
    159     /***
    160      * If <code>true</code>, will always redirect to {@link #defaultTargetUrl}
    161      * upon successful authentication, irrespective of the page that caused
    162      * the authentication request (defaults to <code>false</code>).
    163      */
    164     private boolean alwaysUseDefaultTargetUrl = false;
    165
    166     /***
    167      * Indicates if the filter chain should be continued prior to delegation to
    168      * {@link #successfulAuthentication(HttpServletRequest,
    169      * HttpServletResponse, Authentication)}, which may be useful in certain
    170      * environment (eg Tapestry). Defaults to <code>false</code>.
    171      */
    172     private boolean continueChainBeforeSuccessfulAuthentication = false;
    173
    174     //~ Methods ================================================================
    175
    176     public void afterPropertiesSet() throws Exception {
    177         Assert.hasLength(filterProcessesUrl,
    178             "filterProcessesUrl must be specified");
    179         Assert.hasLength(defaultTargetUrl, "defaultTargetUrl must be specified");
    180         Assert.hasLength(authenticationFailureUrl,
    181             "authenticationFailureUrl must be specified");
    182         Assert.notNull(authenticationManager,
    183             "authenticationManager must be specified");
    184         Assert.notNull(this.rememberMeServices);
    185     }
    186
    187     /***
    188      * Performs actual authentication.
    189      *
    190      * @param request from which to extract parameters and perform the
    191      *        authentication
    192      *
    193      * @return the authenticated user
    194      *
    195      * @throws AuthenticationException if authentication fails
    196      */
    197     public abstract Authentication attemptAuthentication(
    198         HttpServletRequest request) throws AuthenticationException;
    199
    200     /***
    201      * Does nothing. We use IoC container lifecycle services instead.
    202      */
    203     public void destroy() {}
    204
    205     public void doFilter(ServletRequest request, ServletResponse response,
    206         FilterChain chain) throws IOException, ServletException {
    207         if (!(request instanceof HttpServletRequest)) {
    208             throw new ServletException("Can only process HttpServletRequest");
    209         }
    210
    211         if (!(response instanceof HttpServletResponse)) {
    212             throw new ServletException("Can only process HttpServletResponse");
    213         }
    214
    215         HttpServletRequest httpRequest = (HttpServletRequest) request;
    216         HttpServletResponse httpResponse = (HttpServletResponse) response;
    217
    218         if (requiresAuthentication(httpRequest, httpResponse)) {
    219             if (logger.isDebugEnabled()) {
    220                 logger.debug("Request is to process authentication");
    221             }
    222
    223             onPreAuthentication(httpRequest, httpResponse);
    224
    225             Authentication authResult;
    226
    227             try {
    228                 authResult = attemptAuthentication(httpRequest);
    229             } catch (AuthenticationException failed) {
    230                 // Authentication failed
    231                 unsuccessfulAuthentication(httpRequest, httpResponse, failed);
    232
    233                 return;
    234             }
    235
    236             // Authentication success
    237             if (continueChainBeforeSuccessfulAuthentication) {
    238                 chain.doFilter(request, response);
    239             }
    240
    241             successfulAuthentication(httpRequest, httpResponse, authResult);
    242
    243             return;
    244         }
    245
    246         chain.doFilter(request, response);
    247     }
    248
    249     public String getAuthenticationFailureUrl() {
    250         return authenticationFailureUrl;
    251     }
    252
    253     public AuthenticationManager getAuthenticationManager() {
    254         return authenticationManager;
    255     }
    256
    257     /***
    258      * Specifies the default <code>filterProcessesUrl</code> for the
    259      * implementation.
    260      *
    261      * @return the default <code>filterProcessesUrl</code>
    262      */
    263     public abstract String getDefaultFilterProcessesUrl();
    264
    265     public String getDefaultTargetUrl() {
    266         return defaultTargetUrl;
    267     }
    268
    269     public Properties getExceptionMappings() {
    270         return new Properties(exceptionMappings);
    271     }
    272
    273     public String getFilterProcessesUrl() {
    274         return filterProcessesUrl;
    275     }
    276
    277     public RememberMeServices getRememberMeServices() {
    278         return rememberMeServices;
    279     }
    280
    281     /***
    282      * Does nothing. We use IoC container lifecycle services instead.
    283      *
    284      * @param arg0 ignored
    285      *
    286      * @throws ServletException ignored
    287      */
    288     public void init(FilterConfig arg0) throws ServletException {}
    289
    290     public boolean isAlwaysUseDefaultTargetUrl() {
    291         return alwaysUseDefaultTargetUrl;
    292     }
    293
    294     public boolean isContinueChainBeforeSuccessfulAuthentication() {
    295         return continueChainBeforeSuccessfulAuthentication;
    296     }
    297
    298     protected void onPreAuthentication(HttpServletRequest request,
    299         HttpServletResponse response) throws IOException {}
    300
    301     protected void onSuccessfulAuthentication(HttpServletRequest request,
    302         HttpServletResponse response, Authentication authResult)
    303         throws IOException {}
    304
    305     protected void onUnsuccessfulAuthentication(HttpServletRequest request,
    306         HttpServletResponse response) throws IOException {}
    307
    308     /***
    309      * <p>
    310      * Indicates whether this filter should attempt to process a login request
    311      * for the current invocation.
    312      * </p>
    313      *
    314      * <p>
    315      * It strips any parameters from the "path" section of the request URL
    316      * (such as the jsessionid parameter in
    317      * <em>http://host/myapp/index.html;jsessionid=blah</em>) before matching
    318      * against the <code>filterProcessesUrl</code> property.
    319      * </p>
    320      *
    321      * <p>
    322      * Subclasses may override for special requirements, such as Tapestry
    323      * integration.
    324      * </p>
    325      *
    326      * @param request as received from the filter chain
    327      * @param response as received from the filter chain
    328      *
    329      * @return <code>true</code> if the filter should attempt authentication,
    330      *         <code>false</code> otherwise
    331      */
    332     protected boolean requiresAuthentication(HttpServletRequest request,
    333         HttpServletResponse response) {
    334         String uri = request.getRequestURI();
    335         int pathParamIndex = uri.indexOf(';');
    336
    337         if (pathParamIndex > 0) {
    338             // strip everything after the first semi-colon
    339             uri = uri.substring(0, pathParamIndex);
    340         }
    341
    342         return uri.endsWith(request.getContextPath() + filterProcessesUrl);
    343     }
    344
    345     public void setAlwaysUseDefaultTargetUrl(boolean alwaysUseDefaultTargetUrl) {
    346         this.alwaysUseDefaultTargetUrl = alwaysUseDefaultTargetUrl;
    347     }
    348
    349     public void setApplicationEventPublisher(
    350         ApplicationEventPublisher eventPublisher) {
    351         this.eventPublisher = eventPublisher;
    352     }
    353
    354     public void setAuthenticationFailureUrl(String authenticationFailureUrl) {
    355         this.authenticationFailureUrl = authenticationFailureUrl;
    356     }
    357
    358     public void setAuthenticationManager(
    359         AuthenticationManager authenticationManager) {
    360         this.authenticationManager = authenticationManager;
    361     }
    362
    363     public void setContinueChainBeforeSuccessfulAuthentication(
    364         boolean continueChainBeforeSuccessfulAuthentication) {
    365         this.continueChainBeforeSuccessfulAuthentication = continueChainBeforeSuccessfulAuthentication;
    366     }
    367
    368     public void setDefaultTargetUrl(String defaultTargetUrl) {
    369         this.defaultTargetUrl = defaultTargetUrl;
    370     }
    371
    372     public void setExceptionMappings(Properties exceptionMappings) {
    373         this.exceptionMappings = exceptionMappings;
    374     }
    375
    376     public void setFilterProcessesUrl(String filterProcessesUrl) {
    377         this.filterProcessesUrl = filterProcessesUrl;
    378     }
    379
    380     public void setMessageSource(MessageSource messageSource) {
    381         this.messages = new MessageSourceAccessor(messageSource);
    382     }
    383
    384     public void setRememberMeServices(RememberMeServices rememberMeServices) {
    385         this.rememberMeServices = rememberMeServices;
    386     }
    387
    388     protected void successfulAuthentication(HttpServletRequest request,
    389         HttpServletResponse response, Authentication authResult)
    390         throws IOException {
    391         if (logger.isDebugEnabled()) {
    392             logger.debug("Authentication success: " + authResult.toString());
    393         }
    394
    395         SecurityContextHolder.getContext().setAuthentication(authResult);
    396
    397         if (logger.isDebugEnabled()) {
    398             logger.debug(
    399                 "Updated SecurityContextHolder to contain the following Authentication: '"
    400                 + authResult + "'");
    401         }
    402
    403         String targetUrl = (String) request.getSession()
    404                                            .getAttribute(ACEGI_SECURITY_TARGET_URL_KEY);
    405         request.getSession().removeAttribute(ACEGI_SECURITY_TARGET_URL_KEY);
    406
    407         if (alwaysUseDefaultTargetUrl == true) {
    408             targetUrl = null;
    409         }
    410
    411         if (targetUrl == null) {
    412             targetUrl = request.getContextPath() + defaultTargetUrl;
    413         }
    414
    415         if (logger.isDebugEnabled()) {
    416             logger.debug(
    417                 "Redirecting to target URL from HTTP Session (or default): "
    418                 + targetUrl);
    419         }
    420
    421         onSuccessfulAuthentication(request, response, authResult);
    422
    423         rememberMeServices.loginSuccess(request, response, authResult);
    424
    425         // Fire event
    426         if (this.eventPublisher != null) {
    427             eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
    428                     authResult, this.getClass()));
    429         }
    430
    431         response.sendRedirect(response.encodeRedirectURL(targetUrl));
    432     }
    433
    434     protected void unsuccessfulAuthentication(HttpServletRequest request,
    435         HttpServletResponse response, AuthenticationException failed)
    436         throws IOException {
    437         SecurityContextHolder.getContext().setAuthentication(null);
    438
    439         if (logger.isDebugEnabled()) {
    440             logger.debug(
    441                 "Updated SecurityContextHolder to contain null Authentication");
    442         }
    443
    444         String failureUrl = exceptionMappings.getProperty(failed.getClass()
    445                                                                 .getName(),
    446                 authenticationFailureUrl);
    447
    448         if (logger.isDebugEnabled()) {
    449             logger.debug("Authentication request failed: " + failed.toString());
    450         }
    451
    452         try {
    453             request.getSession()
    454                    .setAttribute(ACEGI_SECURITY_LAST_EXCEPTION_KEY, failed);
    455         } catch (Exception ignored) {}
    456
    457         onUnsuccessfulAuthentication(request, response);
    458
    459         rememberMeServices.loginFail(request, response);
    460
    461         response.sendRedirect(response.encodeRedirectURL(request.getContextPath()
    462                 + failureUrl));
    463     }
    464 }