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 }