Java tutorial
/* 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> * <property name="exceptionMappings"> * <props> * <prop> key="org.acegisecurity.BadCredentialsException">/bad_credentials.jsp</prop> * </props> * </property> * </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; } }