001: /* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
002: *
003: * Licensed under the Apache License, Version 2.0 (the "License");
004: * you may not use this file except in compliance with the License.
005: * You may obtain a copy of the License at
006: *
007: * http://www.apache.org/licenses/LICENSE-2.0
008: *
009: * Unless required by applicable law or agreed to in writing, software
010: * distributed under the License is distributed on an "AS IS" BASIS,
011: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012: * See the License for the specific language governing permissions and
013: * limitations under the License.
014: */
015:
016: package org.acegisecurity.ui;
017:
018: import org.acegisecurity.AcegiMessageSource;
019: import org.acegisecurity.Authentication;
020: import org.acegisecurity.AuthenticationException;
021: import org.acegisecurity.AuthenticationManager;
022:
023: import org.acegisecurity.context.SecurityContextHolder;
024:
025: import org.acegisecurity.event.authentication.InteractiveAuthenticationSuccessEvent;
026:
027: import org.acegisecurity.ui.rememberme.NullRememberMeServices;
028: import org.acegisecurity.ui.rememberme.RememberMeServices;
029: import org.acegisecurity.ui.savedrequest.SavedRequest;
030:
031: import org.apache.commons.logging.Log;
032: import org.apache.commons.logging.LogFactory;
033:
034: import org.springframework.beans.factory.InitializingBean;
035:
036: import org.springframework.context.ApplicationEventPublisher;
037: import org.springframework.context.ApplicationEventPublisherAware;
038: import org.springframework.context.MessageSource;
039: import org.springframework.context.MessageSourceAware;
040: import org.springframework.context.support.MessageSourceAccessor;
041:
042: import org.springframework.util.Assert;
043:
044: import java.io.IOException;
045:
046: import java.util.Properties;
047:
048: import javax.servlet.Filter;
049: import javax.servlet.FilterChain;
050: import javax.servlet.FilterConfig;
051: import javax.servlet.ServletException;
052: import javax.servlet.ServletRequest;
053: import javax.servlet.ServletResponse;
054: import javax.servlet.http.HttpServletRequest;
055: import javax.servlet.http.HttpServletResponse;
056:
057: /**
058: * Abstract processor of browser-based HTTP-based authentication requests.
059: * <p>
060: * This filter is responsible for processing authentication requests. If
061: * authentication is successful, the resulting {@link Authentication} object
062: * will be placed into the <code>SecurityContext</code>, which is guaranteed
063: * to have already been created by an earlier filter.
064: * </p>
065: * <p>
066: * If authentication fails, the <code>AuthenticationException</code> will be
067: * placed into the <code>HttpSession</code> with the attribute defined by
068: * {@link #ACEGI_SECURITY_LAST_EXCEPTION_KEY}.
069: * </p>
070: * <p>
071: * To use this filter, it is necessary to specify the following properties:
072: * </p>
073: * <ul>
074: * <li><code>defaultTargetUrl</code> indicates the URL that should be used
075: * for redirection if the <code>HttpSession</code> attribute named
076: * {@link #ACEGI_SAVED_REQUEST_KEY} does not indicate the target URL once
077: * authentication is completed successfully. eg: <code>/</code>. The
078: * <code>defaultTargetUrl</code> will be treated as relative to the web-app's
079: * context path, and should include the leading <code>/</code>.
080: * Alternatively, inclusion of a scheme name (eg http:// or https://) as the
081: * prefix will denote a fully-qualified URL and this is also supported.</li>
082: * <li><code>authenticationFailureUrl</code> indicates the URL that should be
083: * used for redirection if the authentication request fails. eg:
084: * <code>/login.jsp?login_error=1</code>.</li>
085: * <li><code>filterProcessesUrl</code> indicates the URL that this filter
086: * will respond to. This parameter varies by subclass.</li>
087: * <li><code>alwaysUseDefaultTargetUrl</code> causes successful
088: * authentication to always redirect to the <code>defaultTargetUrl</code>,
089: * even if the <code>HttpSession</code> attribute named {@link
090: * #ACEGI_SAVED_REQUEST_KEY} defines the intended target URL.</li>
091: * </ul>
092: * <p>
093: * To configure this filter to redirect to specific pages as the result of
094: * specific {@link AuthenticationException}s you can do the following.
095: * Configure the <code>exceptionMappings</code> property in your application
096: * xml. This property is a java.util.Properties object that maps a
097: * fully-qualified exception class name to a redirection url target. For
098: * example:
099: *
100: * <pre>
101: * <property name="exceptionMappings">
102: * <props>
103: * <prop> key="org.acegisecurity.BadCredentialsException">/bad_credentials.jsp</prop>
104: * </props>
105: * </property>
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,
130: InitializingBean, ApplicationEventPublisherAware,
131: MessageSourceAware {
132: // ~ Static fields/initializers
133: // =====================================================================================
134:
135: public static final String ACEGI_SAVED_REQUEST_KEY = "ACEGI_SAVED_REQUEST_KEY";
136:
137: public static final String ACEGI_SECURITY_LAST_EXCEPTION_KEY = "ACEGI_SECURITY_LAST_EXCEPTION";
138:
139: // ~ Instance fields
140: // ================================================================================================
141:
142: protected ApplicationEventPublisher eventPublisher;
143:
144: protected AuthenticationDetailsSource authenticationDetailsSource = new AuthenticationDetailsSourceImpl();
145:
146: private AuthenticationManager authenticationManager;
147:
148: protected final Log logger = LogFactory.getLog(this .getClass());
149:
150: protected MessageSourceAccessor messages = AcegiMessageSource
151: .getAccessor();
152:
153: private Properties exceptionMappings = new Properties();
154:
155: private RememberMeServices rememberMeServices = new NullRememberMeServices();
156:
157: /** Where to redirect the browser to if authentication fails */
158: private String authenticationFailureUrl;
159:
160: /**
161: * Where to redirect the browser to if authentication is successful but
162: * ACEGI_SAVED_REQUEST_KEY is <code>null</code>
163: */
164: private String defaultTargetUrl;
165:
166: /**
167: * The URL destination that this filter intercepts and processes (usually
168: * something like <code>/j_acegi_security_check</code>)
169: */
170: private String filterProcessesUrl = getDefaultFilterProcessesUrl();
171:
172: /**
173: * If <code>true</code>, will always redirect to the value of
174: * {@link #getDefaultTargetUrl} upon successful authentication, irrespective
175: * of the page that caused the authentication request (defaults to
176: * <code>false</code>).
177: */
178: private boolean alwaysUseDefaultTargetUrl = false;
179:
180: /**
181: * Indicates if the filter chain should be continued prior to delegation to
182: * {@link #successfulAuthentication(HttpServletRequest, HttpServletResponse,
183: * Authentication)}, which may be useful in certain environment (eg
184: * Tapestry). Defaults to <code>false</code>.
185: */
186: private boolean continueChainBeforeSuccessfulAuthentication = false;
187:
188: /**
189: * Specifies the buffer size to use in the event of a directory. A buffer
190: * size is used to ensure the response is not written back to the client
191: * immediately. This provides a way for the <code>HttpSession</code> to be
192: * updated before the browser redirect will be sent. Defaults to an 8 Kb
193: * buffer.
194: */
195: private int bufferSize = 8 * 1024;
196:
197: /**
198: * If true, causes any redirection URLs to be calculated minus the protocol
199: * and context path (defaults to false).
200: */
201: private boolean useRelativeContext = false;
202:
203: // ~ Methods
204: // ========================================================================================================
205:
206: public void afterPropertiesSet() throws Exception {
207: Assert.hasLength(filterProcessesUrl,
208: "filterProcessesUrl must be specified");
209: Assert.hasLength(defaultTargetUrl,
210: "defaultTargetUrl must be specified");
211: Assert.hasLength(authenticationFailureUrl,
212: "authenticationFailureUrl must be specified");
213: Assert.notNull(authenticationManager,
214: "authenticationManager must be specified");
215: Assert.notNull(this .rememberMeServices);
216: }
217:
218: /**
219: * Performs actual authentication.
220: *
221: * @param request from which to extract parameters and perform the
222: * authentication
223: *
224: * @return the authenticated user
225: *
226: * @throws AuthenticationException if authentication fails
227: */
228: public abstract Authentication attemptAuthentication(
229: HttpServletRequest request) throws AuthenticationException;
230:
231: /**
232: * Does nothing. We use IoC container lifecycle services instead.
233: */
234: public void destroy() {
235: }
236:
237: public void doFilter(ServletRequest request,
238: ServletResponse response, FilterChain chain)
239: throws IOException, ServletException {
240: if (!(request instanceof HttpServletRequest)) {
241: throw new ServletException(
242: "Can only process HttpServletRequest");
243: }
244:
245: if (!(response instanceof HttpServletResponse)) {
246: throw new ServletException(
247: "Can only process HttpServletResponse");
248: }
249:
250: HttpServletRequest httpRequest = (HttpServletRequest) request;
251: HttpServletResponse httpResponse = (HttpServletResponse) response;
252:
253: if (requiresAuthentication(httpRequest, httpResponse)) {
254: if (logger.isDebugEnabled()) {
255: logger.debug("Request is to process authentication");
256: }
257:
258: Authentication authResult;
259:
260: try {
261: onPreAuthentication(httpRequest, httpResponse);
262: authResult = attemptAuthentication(httpRequest);
263: } catch (AuthenticationException failed) {
264: // Authentication failed
265: unsuccessfulAuthentication(httpRequest, httpResponse,
266: failed);
267:
268: return;
269: }
270:
271: // Authentication success
272: if (continueChainBeforeSuccessfulAuthentication) {
273: chain.doFilter(request, response);
274: }
275:
276: successfulAuthentication(httpRequest, httpResponse,
277: authResult);
278:
279: return;
280: }
281:
282: chain.doFilter(request, response);
283: }
284:
285: public String getAuthenticationFailureUrl() {
286: return authenticationFailureUrl;
287: }
288:
289: public AuthenticationManager getAuthenticationManager() {
290: return authenticationManager;
291: }
292:
293: /**
294: * Specifies the default <code>filterProcessesUrl</code> for the
295: * implementation.
296: *
297: * @return the default <code>filterProcessesUrl</code>
298: */
299: public abstract String getDefaultFilterProcessesUrl();
300:
301: /**
302: * Supplies the default target Url that will be used if no saved request is
303: * found or the <tt>alwaysUseDefaultTargetUrl</tt> propert is set to true.
304: * Override this method of you want to provide a customized default Url (for
305: * example if you want different Urls depending on the authorities of the
306: * user who has just logged in).
307: *
308: * @return the defaultTargetUrl property
309: */
310: public String getDefaultTargetUrl() {
311: return defaultTargetUrl;
312: }
313:
314: public Properties getExceptionMappings() {
315: return new Properties(exceptionMappings);
316: }
317:
318: public String getFilterProcessesUrl() {
319: return filterProcessesUrl;
320: }
321:
322: public RememberMeServices getRememberMeServices() {
323: return rememberMeServices;
324: }
325:
326: /**
327: * Does nothing. We use IoC container lifecycle services instead.
328: *
329: * @param arg0 ignored
330: *
331: * @throws ServletException ignored
332: */
333: public void init(FilterConfig arg0) throws ServletException {
334: }
335:
336: public boolean isAlwaysUseDefaultTargetUrl() {
337: return alwaysUseDefaultTargetUrl;
338: }
339:
340: public boolean isContinueChainBeforeSuccessfulAuthentication() {
341: return continueChainBeforeSuccessfulAuthentication;
342: }
343:
344: public static String obtainFullRequestUrl(HttpServletRequest request) {
345: SavedRequest savedRequest = (SavedRequest) request
346: .getSession()
347: .getAttribute(
348: AbstractProcessingFilter.ACEGI_SAVED_REQUEST_KEY);
349:
350: return (savedRequest == null) ? null : savedRequest
351: .getFullRequestUrl();
352: }
353:
354: protected void onPreAuthentication(HttpServletRequest request,
355: HttpServletResponse response)
356: throws AuthenticationException, IOException {
357: }
358:
359: protected void onSuccessfulAuthentication(
360: HttpServletRequest request, HttpServletResponse response,
361: Authentication authResult) throws IOException {
362: }
363:
364: protected void onUnsuccessfulAuthentication(
365: HttpServletRequest request, HttpServletResponse response,
366: AuthenticationException failed) throws IOException {
367: }
368:
369: /**
370: * <p>
371: * Indicates whether this filter should attempt to process a login request
372: * for the current invocation.
373: * </p>
374: * <p>
375: * It strips any parameters from the "path" section of the request URL (such
376: * as the jsessionid parameter in
377: * <em>http://host/myapp/index.html;jsessionid=blah</em>) before matching
378: * against the <code>filterProcessesUrl</code> property.
379: * </p>
380: * <p>
381: * Subclasses may override for special requirements, such as Tapestry
382: * integration.
383: * </p>
384: *
385: * @param request as received from the filter chain
386: * @param response as received from the filter chain
387: *
388: * @return <code>true</code> if the filter should attempt authentication,
389: * <code>false</code> otherwise
390: */
391: protected boolean requiresAuthentication(
392: HttpServletRequest request, HttpServletResponse response) {
393: String uri = request.getRequestURI();
394: int pathParamIndex = uri.indexOf(';');
395:
396: if (pathParamIndex > 0) {
397: // strip everything after the first semi-colon
398: uri = uri.substring(0, pathParamIndex);
399: }
400:
401: if ("".equals(request.getContextPath())) {
402: return uri.endsWith(filterProcessesUrl);
403: }
404:
405: return uri.endsWith(request.getContextPath()
406: + filterProcessesUrl);
407: }
408:
409: protected void sendRedirect(HttpServletRequest request,
410: HttpServletResponse response, String url)
411: throws IOException {
412: String finalUrl;
413: if (!url.startsWith("http://") && !url.startsWith("https://")) {
414: if (useRelativeContext) {
415: finalUrl = url;
416: } else {
417: finalUrl = request.getContextPath() + url;
418: }
419: } else if (useRelativeContext) {
420: // Calculate the relative URL from the fully qualifed URL, minus the
421: // protocol and base context.
422: int len = request.getContextPath().length();
423: int index = url.indexOf(request.getContextPath()) + len;
424: finalUrl = url.substring(index);
425: if (finalUrl.length() > 1 && finalUrl.charAt(0) == '/') {
426: finalUrl = finalUrl.substring(1);
427: }
428: } else {
429: finalUrl = url;
430: }
431:
432: Assert
433: .isTrue(
434: !response.isCommitted(),
435: "Response already committed; the authentication mechanism must be able to modify buffer size");
436: response.setBufferSize(bufferSize);
437: response.sendRedirect(response.encodeRedirectURL(finalUrl));
438: }
439:
440: public void setAlwaysUseDefaultTargetUrl(
441: boolean alwaysUseDefaultTargetUrl) {
442: this .alwaysUseDefaultTargetUrl = alwaysUseDefaultTargetUrl;
443: }
444:
445: public void setApplicationEventPublisher(
446: ApplicationEventPublisher eventPublisher) {
447: this .eventPublisher = eventPublisher;
448: }
449:
450: public void setAuthenticationDetailsSource(
451: AuthenticationDetailsSource authenticationDetailsSource) {
452: Assert.notNull(authenticationDetailsSource,
453: "AuthenticationDetailsSource required");
454: this .authenticationDetailsSource = authenticationDetailsSource;
455: }
456:
457: public void setAuthenticationFailureUrl(
458: String authenticationFailureUrl) {
459: this .authenticationFailureUrl = authenticationFailureUrl;
460: }
461:
462: public void setAuthenticationManager(
463: AuthenticationManager authenticationManager) {
464: this .authenticationManager = authenticationManager;
465: }
466:
467: public void setContinueChainBeforeSuccessfulAuthentication(
468: boolean continueChainBeforeSuccessfulAuthentication) {
469: this .continueChainBeforeSuccessfulAuthentication = continueChainBeforeSuccessfulAuthentication;
470: }
471:
472: public void setDefaultTargetUrl(String defaultTargetUrl) {
473: Assert.isTrue(defaultTargetUrl.startsWith("/")
474: | defaultTargetUrl.startsWith("http"),
475: "defaultTarget must start with '/' or with 'http(s)'");
476: this .defaultTargetUrl = defaultTargetUrl;
477: }
478:
479: public void setExceptionMappings(Properties exceptionMappings) {
480: this .exceptionMappings = exceptionMappings;
481: }
482:
483: public void setFilterProcessesUrl(String filterProcessesUrl) {
484: this .filterProcessesUrl = filterProcessesUrl;
485: }
486:
487: public void setMessageSource(MessageSource messageSource) {
488: this .messages = new MessageSourceAccessor(messageSource);
489: }
490:
491: public void setRememberMeServices(
492: RememberMeServices rememberMeServices) {
493: this .rememberMeServices = rememberMeServices;
494: }
495:
496: protected void successfulAuthentication(HttpServletRequest request,
497: HttpServletResponse response, Authentication authResult)
498: throws IOException {
499: if (logger.isDebugEnabled()) {
500: logger.debug("Authentication success: "
501: + authResult.toString());
502: }
503:
504: SecurityContextHolder.getContext()
505: .setAuthentication(authResult);
506:
507: if (logger.isDebugEnabled()) {
508: logger
509: .debug("Updated SecurityContextHolder to contain the following Authentication: '"
510: + authResult + "'");
511: }
512:
513: String targetUrl = determineTargetUrl(request);
514:
515: if (logger.isDebugEnabled()) {
516: logger
517: .debug("Redirecting to target URL from HTTP Session (or default): "
518: + targetUrl);
519: }
520:
521: onSuccessfulAuthentication(request, response, authResult);
522:
523: rememberMeServices.loginSuccess(request, response, authResult);
524:
525: // Fire event
526: if (this .eventPublisher != null) {
527: eventPublisher
528: .publishEvent(new InteractiveAuthenticationSuccessEvent(
529: authResult, this .getClass()));
530: }
531:
532: sendRedirect(request, response, targetUrl);
533: }
534:
535: protected String determineTargetUrl(HttpServletRequest request) {
536: // Don't attempt to obtain the url from the saved request if
537: // alwaysUsedefaultTargetUrl is set
538: String targetUrl = alwaysUseDefaultTargetUrl ? null
539: : obtainFullRequestUrl(request);
540:
541: if (targetUrl == null) {
542: targetUrl = getDefaultTargetUrl();
543: }
544:
545: return targetUrl;
546: }
547:
548: protected void unsuccessfulAuthentication(
549: HttpServletRequest request, HttpServletResponse response,
550: AuthenticationException failed) throws IOException {
551: SecurityContextHolder.getContext().setAuthentication(null);
552:
553: if (logger.isDebugEnabled()) {
554: logger
555: .debug("Updated SecurityContextHolder to contain null Authentication");
556: }
557:
558: String failureUrl = determineFailureUrl(request, failed);
559:
560: if (logger.isDebugEnabled()) {
561: logger.debug("Authentication request failed: "
562: + failed.toString());
563: }
564:
565: try {
566: request.getSession().setAttribute(
567: ACEGI_SECURITY_LAST_EXCEPTION_KEY, failed);
568: } catch (Exception ignored) {
569: }
570:
571: onUnsuccessfulAuthentication(request, response, failed);
572:
573: rememberMeServices.loginFail(request, response);
574:
575: sendRedirect(request, response, failureUrl);
576: }
577:
578: protected String determineFailureUrl(HttpServletRequest request,
579: AuthenticationException failed) {
580: return exceptionMappings.getProperty(failed.getClass()
581: .getName(), authenticationFailureUrl);
582: }
583:
584: public AuthenticationDetailsSource getAuthenticationDetailsSource() {
585: // Required due to SEC-310
586: return authenticationDetailsSource;
587: }
588:
589: public void setBufferSize(int bufferSize) {
590: this .bufferSize = bufferSize;
591: }
592:
593: public void setUseRelativeContext(boolean useRelativeContext) {
594: this.useRelativeContext = useRelativeContext;
595: }
596:
597: }
|