View Javadoc

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