org.acegisecurity.ui.AbstractProcessingFilter.java Source code

Java tutorial

Introduction

Here is the source code for org.acegisecurity.ui.AbstractProcessingFilter.java

Source

/* Copyright 2004, 2005, 2006 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 org.acegisecurity.ui;

import org.acegisecurity.AcegiMessageSource;
import org.acegisecurity.Authentication;
import org.acegisecurity.AuthenticationException;
import org.acegisecurity.AuthenticationManager;

import org.acegisecurity.context.SecurityContextHolder;

import org.acegisecurity.event.authentication.InteractiveAuthenticationSuccessEvent;

import org.acegisecurity.ui.rememberme.NullRememberMeServices;
import org.acegisecurity.ui.rememberme.RememberMeServices;
import org.acegisecurity.ui.savedrequest.SavedRequest;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.beans.factory.InitializingBean;

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;

import org.springframework.util.Assert;

import java.io.IOException;

import java.util.Properties;

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;

/**
 * Abstract processor of browser-based HTTP-based authentication requests.
 * <p>
 * This filter is responsible for processing authentication requests. If
 * authentication is successful, the resulting {@link Authentication} object
 * will be placed into the <code>SecurityContext</code>, which is guaranteed
 * to have already been created by an earlier filter.
 * </p>
 * <p>
 * If authentication fails, the <code>AuthenticationException</code> will be
 * placed into the <code>HttpSession</code> with the attribute defined by
 * {@link #ACEGI_SECURITY_LAST_EXCEPTION_KEY}.
 * </p>
 * <p>
 * To use this filter, it is necessary to specify the following properties:
 * </p>
 * <ul>
 * <li><code>defaultTargetUrl</code> indicates the URL that should be used
 * for redirection if the <code>HttpSession</code> attribute named
 * {@link #ACEGI_SAVED_REQUEST_KEY} does not indicate the target URL once
 * authentication is completed successfully. eg: <code>/</code>. The
 * <code>defaultTargetUrl</code> will be treated as relative to the web-app's
 * context path, and should include the leading <code>/</code>.
 * Alternatively, inclusion of a scheme name (eg http:// or https://) as the
 * prefix will denote a fully-qualified URL and this is also supported.</li>
 * <li><code>authenticationFailureUrl</code> indicates the URL that should be
 * used for redirection if the authentication request fails. eg:
 * <code>/login.jsp?login_error=1</code>.</li>
 * <li><code>filterProcessesUrl</code> indicates the URL that this filter
 * will respond to. This parameter varies by subclass.</li>
 * <li><code>alwaysUseDefaultTargetUrl</code> causes successful
 * authentication to always redirect to the <code>defaultTargetUrl</code>,
 * even if the <code>HttpSession</code> attribute named {@link
 * #ACEGI_SAVED_REQUEST_KEY} defines the intended target URL.</li>
 * </ul>
 * <p>
 * To configure this filter to redirect to specific pages as the result of
 * specific {@link AuthenticationException}s you can do the following.
 * Configure the <code>exceptionMappings</code> property in your application
 * xml. This property is a java.util.Properties object that maps a
 * fully-qualified exception class name to a redirection url target. For
 * example:
 * 
 * <pre>
 *  &lt;property name=&quot;exceptionMappings&quot;&gt;
 *    &lt;props&gt;
 *      &lt;prop&gt; key=&quot;org.acegisecurity.BadCredentialsException&quot;&gt;/bad_credentials.jsp&lt;/prop&gt;
 *    &lt;/props&gt;
 *  &lt;/property&gt;
 * </pre>
 * 
 * The example above would redirect all
 * {@link org.acegisecurity.BadCredentialsException}s thrown, to a page in the
 * web-application called /bad_credentials.jsp.
 * </p>
 * <p>
 * Any {@link AuthenticationException} thrown that cannot be matched in the
 * <code>exceptionMappings</code> will be redirected to the
 * <code>authenticationFailureUrl</code>
 * </p>
 * <p>
 * If authentication is successful, an {@link
 * org.acegisecurity.event.authentication.InteractiveAuthenticationSuccessEvent}
 * will be published to the application context. No events will be published if
 * authentication was unsuccessful, because this would generally be recorded via
 * an <code>AuthenticationManager</code>-specific application event.
 * </p>
 * 
 * @author Ben Alex
 * @version $Id: AbstractProcessingFilter.java 1909 2007-06-19 04:08:19Z
 * vishalpuri $
 */
public abstract class AbstractProcessingFilter
        implements Filter, InitializingBean, ApplicationEventPublisherAware, MessageSourceAware {
    // ~ Static fields/initializers
    // =====================================================================================

    public static final String ACEGI_SAVED_REQUEST_KEY = "ACEGI_SAVED_REQUEST_KEY";

    public static final String ACEGI_SECURITY_LAST_EXCEPTION_KEY = "ACEGI_SECURITY_LAST_EXCEPTION";

    // ~ Instance fields
    // ================================================================================================

    protected ApplicationEventPublisher eventPublisher;

    protected AuthenticationDetailsSource authenticationDetailsSource = new AuthenticationDetailsSourceImpl();

    private AuthenticationManager authenticationManager;

    protected final Log logger = LogFactory.getLog(this.getClass());

    protected MessageSourceAccessor messages = AcegiMessageSource.getAccessor();

    private Properties exceptionMappings = new Properties();

    private RememberMeServices rememberMeServices = new NullRememberMeServices();

    /** Where to redirect the browser to if authentication fails */
    private String authenticationFailureUrl;

    /**
     * Where to redirect the browser to if authentication is successful but
     * ACEGI_SAVED_REQUEST_KEY is <code>null</code>
     */
    private String defaultTargetUrl;

    /**
     * The URL destination that this filter intercepts and processes (usually
     * something like <code>/j_acegi_security_check</code>)
     */
    private String filterProcessesUrl = getDefaultFilterProcessesUrl();

    /**
     * If <code>true</code>, will always redirect to the value of
     * {@link #getDefaultTargetUrl} upon successful authentication, irrespective
     * of the page that caused the authentication request (defaults to
     * <code>false</code>).
     */
    private boolean alwaysUseDefaultTargetUrl = false;

    /**
     * Indicates if the filter chain should be continued prior to delegation to
     * {@link #successfulAuthentication(HttpServletRequest, HttpServletResponse,
     * Authentication)}, which may be useful in certain environment (eg
     * Tapestry). Defaults to <code>false</code>.
     */
    private boolean continueChainBeforeSuccessfulAuthentication = false;

    /**
     * Specifies the buffer size to use in the event of a directory. A buffer
     * size is used to ensure the response is not written back to the client
     * immediately. This provides a way for the <code>HttpSession</code> to be
     * updated before the browser redirect will be sent. Defaults to an 8 Kb
     * buffer.
     */
    private int bufferSize = 8 * 1024;

    /**
     * If true, causes any redirection URLs to be calculated minus the protocol
     * and context path (defaults to false).
     */
    private boolean useRelativeContext = false;

    // ~ Methods
    // ========================================================================================================

    public void afterPropertiesSet() throws Exception {
        Assert.hasLength(filterProcessesUrl, "filterProcessesUrl must be specified");
        Assert.hasLength(defaultTargetUrl, "defaultTargetUrl must be specified");
        Assert.hasLength(authenticationFailureUrl, "authenticationFailureUrl must be specified");
        Assert.notNull(authenticationManager, "authenticationManager must be specified");
        Assert.notNull(this.rememberMeServices);
    }

    /**
     * Performs actual authentication.
     * 
     * @param request from which to extract parameters and perform the
     * authentication
     * 
     * @return the authenticated user
     * 
     * @throws AuthenticationException if authentication fails
     */
    public abstract Authentication attemptAuthentication(HttpServletRequest request) throws AuthenticationException;

    /**
     * Does nothing. We use IoC container lifecycle services instead.
     */
    public void destroy() {
    }

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        if (!(request instanceof HttpServletRequest)) {
            throw new ServletException("Can only process HttpServletRequest");
        }

        if (!(response instanceof HttpServletResponse)) {
            throw new ServletException("Can only process HttpServletResponse");
        }

        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        if (requiresAuthentication(httpRequest, httpResponse)) {
            if (logger.isDebugEnabled()) {
                logger.debug("Request is to process authentication");
            }

            Authentication authResult;

            try {
                onPreAuthentication(httpRequest, httpResponse);
                authResult = attemptAuthentication(httpRequest);
            } catch (AuthenticationException failed) {
                // Authentication failed
                unsuccessfulAuthentication(httpRequest, httpResponse, failed);

                return;
            }

            // Authentication success
            if (continueChainBeforeSuccessfulAuthentication) {
                chain.doFilter(request, response);
            }

            successfulAuthentication(httpRequest, httpResponse, authResult);

            return;
        }

        chain.doFilter(request, response);
    }

    public String getAuthenticationFailureUrl() {
        return authenticationFailureUrl;
    }

    public AuthenticationManager getAuthenticationManager() {
        return authenticationManager;
    }

    /**
     * Specifies the default <code>filterProcessesUrl</code> for the
     * implementation.
     * 
     * @return the default <code>filterProcessesUrl</code>
     */
    public abstract String getDefaultFilterProcessesUrl();

    /**
     * Supplies the default target Url that will be used if no saved request is
     * found or the <tt>alwaysUseDefaultTargetUrl</tt> propert is set to true.
     * Override this method of you want to provide a customized default Url (for
     * example if you want different Urls depending on the authorities of the
     * user who has just logged in).
     * 
     * @return the defaultTargetUrl property
     */
    public String getDefaultTargetUrl() {
        return defaultTargetUrl;
    }

    public Properties getExceptionMappings() {
        return new Properties(exceptionMappings);
    }

    public String getFilterProcessesUrl() {
        return filterProcessesUrl;
    }

    public RememberMeServices getRememberMeServices() {
        return rememberMeServices;
    }

    /**
     * Does nothing. We use IoC container lifecycle services instead.
     * 
     * @param arg0 ignored
     * 
     * @throws ServletException ignored
     */
    public void init(FilterConfig arg0) throws ServletException {
    }

    public boolean isAlwaysUseDefaultTargetUrl() {
        return alwaysUseDefaultTargetUrl;
    }

    public boolean isContinueChainBeforeSuccessfulAuthentication() {
        return continueChainBeforeSuccessfulAuthentication;
    }

    public static String obtainFullRequestUrl(HttpServletRequest request) {
        SavedRequest savedRequest = (SavedRequest) request.getSession()
                .getAttribute(AbstractProcessingFilter.ACEGI_SAVED_REQUEST_KEY);

        return (savedRequest == null) ? null : savedRequest.getFullRequestUrl();
    }

    protected void onPreAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException, IOException {
    }

    protected void onSuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
            Authentication authResult) throws IOException {
    }

    protected void onUnsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException failed) throws IOException {
    }

    /**
     * <p>
     * Indicates whether this filter should attempt to process a login request
     * for the current invocation.
     * </p>
     * <p>
     * It strips any parameters from the "path" section of the request URL (such
     * as the jsessionid parameter in
     * <em>http://host/myapp/index.html;jsessionid=blah</em>) before matching
     * against the <code>filterProcessesUrl</code> property.
     * </p>
     * <p>
     * Subclasses may override for special requirements, such as Tapestry
     * integration.
     * </p>
     * 
     * @param request as received from the filter chain
     * @param response as received from the filter chain
     * 
     * @return <code>true</code> if the filter should attempt authentication,
     * <code>false</code> otherwise
     */
    protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
        String uri = request.getRequestURI();
        int pathParamIndex = uri.indexOf(';');

        if (pathParamIndex > 0) {
            // strip everything after the first semi-colon
            uri = uri.substring(0, pathParamIndex);
        }

        if ("".equals(request.getContextPath())) {
            return uri.endsWith(filterProcessesUrl);
        }

        return uri.endsWith(request.getContextPath() + filterProcessesUrl);
    }

    protected void sendRedirect(HttpServletRequest request, HttpServletResponse response, String url)
            throws IOException {
        String finalUrl;
        if (!url.startsWith("http://") && !url.startsWith("https://")) {
            if (useRelativeContext) {
                finalUrl = url;
            } else {
                finalUrl = request.getContextPath() + url;
            }
        } else if (useRelativeContext) {
            // Calculate the relative URL from the fully qualifed URL, minus the
            // protocol and base context.
            int len = request.getContextPath().length();
            int index = url.indexOf(request.getContextPath()) + len;
            finalUrl = url.substring(index);
            if (finalUrl.length() > 1 && finalUrl.charAt(0) == '/') {
                finalUrl = finalUrl.substring(1);
            }
        } else {
            finalUrl = url;
        }

        Assert.isTrue(!response.isCommitted(),
                "Response already committed; the authentication mechanism must be able to modify buffer size");
        response.setBufferSize(bufferSize);
        response.sendRedirect(response.encodeRedirectURL(finalUrl));
    }

    public void setAlwaysUseDefaultTargetUrl(boolean alwaysUseDefaultTargetUrl) {
        this.alwaysUseDefaultTargetUrl = alwaysUseDefaultTargetUrl;
    }

    public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }

    public void setAuthenticationDetailsSource(AuthenticationDetailsSource authenticationDetailsSource) {
        Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource required");
        this.authenticationDetailsSource = authenticationDetailsSource;
    }

    public void setAuthenticationFailureUrl(String authenticationFailureUrl) {
        this.authenticationFailureUrl = authenticationFailureUrl;
    }

    public void setAuthenticationManager(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    public void setContinueChainBeforeSuccessfulAuthentication(
            boolean continueChainBeforeSuccessfulAuthentication) {
        this.continueChainBeforeSuccessfulAuthentication = continueChainBeforeSuccessfulAuthentication;
    }

    public void setDefaultTargetUrl(String defaultTargetUrl) {
        Assert.isTrue(defaultTargetUrl.startsWith("/") | defaultTargetUrl.startsWith("http"),
                "defaultTarget must start with '/' or with 'http(s)'");
        this.defaultTargetUrl = defaultTargetUrl;
    }

    public void setExceptionMappings(Properties exceptionMappings) {
        this.exceptionMappings = exceptionMappings;
    }

    public void setFilterProcessesUrl(String filterProcessesUrl) {
        this.filterProcessesUrl = filterProcessesUrl;
    }

    public void setMessageSource(MessageSource messageSource) {
        this.messages = new MessageSourceAccessor(messageSource);
    }

    public void setRememberMeServices(RememberMeServices rememberMeServices) {
        this.rememberMeServices = rememberMeServices;
    }

    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
            Authentication authResult) throws IOException {
        if (logger.isDebugEnabled()) {
            logger.debug("Authentication success: " + authResult.toString());
        }

        SecurityContextHolder.getContext().setAuthentication(authResult);

        if (logger.isDebugEnabled()) {
            logger.debug(
                    "Updated SecurityContextHolder to contain the following Authentication: '" + authResult + "'");
        }

        String targetUrl = determineTargetUrl(request);

        if (logger.isDebugEnabled()) {
            logger.debug("Redirecting to target URL from HTTP Session (or default): " + targetUrl);
        }

        onSuccessfulAuthentication(request, response, authResult);

        rememberMeServices.loginSuccess(request, response, authResult);

        // Fire event
        if (this.eventPublisher != null) {
            eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
        }

        sendRedirect(request, response, targetUrl);
    }

    protected String determineTargetUrl(HttpServletRequest request) {
        // Don't attempt to obtain the url from the saved request if
        // alwaysUsedefaultTargetUrl is set
        String targetUrl = alwaysUseDefaultTargetUrl ? null : obtainFullRequestUrl(request);

        if (targetUrl == null) {
            targetUrl = getDefaultTargetUrl();
        }

        return targetUrl;
    }

    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException failed) throws IOException {
        SecurityContextHolder.getContext().setAuthentication(null);

        if (logger.isDebugEnabled()) {
            logger.debug("Updated SecurityContextHolder to contain null Authentication");
        }

        String failureUrl = determineFailureUrl(request, failed);

        if (logger.isDebugEnabled()) {
            logger.debug("Authentication request failed: " + failed.toString());
        }

        try {
            request.getSession().setAttribute(ACEGI_SECURITY_LAST_EXCEPTION_KEY, failed);
        } catch (Exception ignored) {
        }

        onUnsuccessfulAuthentication(request, response, failed);

        rememberMeServices.loginFail(request, response);

        sendRedirect(request, response, failureUrl);
    }

    protected String determineFailureUrl(HttpServletRequest request, AuthenticationException failed) {
        return exceptionMappings.getProperty(failed.getClass().getName(), authenticationFailureUrl);
    }

    public AuthenticationDetailsSource getAuthenticationDetailsSource() {
        // Required due to SEC-310
        return authenticationDetailsSource;
    }

    public void setBufferSize(int bufferSize) {
        this.bufferSize = bufferSize;
    }

    public void setUseRelativeContext(boolean useRelativeContext) {
        this.useRelativeContext = useRelativeContext;
    }

}