1
2
3
4
5
6
7
8
9
10
11
12
13
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> <property name="exceptionMappings"><br>
103 * <props><br>
104 * <prop> key="org.acegisecurity.BadCredentialsException">/bad_credentials.jsp</prop><br>
105 * </props><br>
106 * </property><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
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
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
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
231 unsuccessfulAuthentication(httpRequest, httpResponse, failed);
232
233 return;
234 }
235
236
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
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
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 }