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.context;
17
18 import java.io.IOException;
19 import java.lang.reflect.Method;
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 import javax.servlet.http.HttpServletResponseWrapper;
30 import javax.servlet.http.HttpSession;
31
32 import org.apache.commons.logging.Log;
33 import org.apache.commons.logging.LogFactory;
34 import org.springframework.beans.factory.InitializingBean;
35 import org.springframework.util.Assert;
36 import org.springframework.util.ReflectionUtils;
37
38 /**
39 * Populates the {@link SecurityContextHolder} with information obtained from
40 * the <code>HttpSession</code>.
41 * <p/>
42 * <p/>
43 * The <code>HttpSession</code> will be queried to retrieve the
44 * <code>SecurityContext</code> that should be stored against the
45 * <code>SecurityContextHolder</code> for the duration of the web request. At
46 * the end of the web request, any updates made to the
47 * <code>SecurityContextHolder</code> will be persisted back to the
48 * <code>HttpSession</code> by this filter.
49 * </p>
50 * <p/>
51 * If a valid <code>SecurityContext</code> cannot be obtained from the
52 * <code>HttpSession</code> for whatever reason, a fresh
53 * <code>SecurityContext</code> will be created and used instead. The created
54 * object will be of the instance defined by the {@link #setContext(Class)}
55 * method (which defaults to {@link org.acegisecurity.context.SecurityContextImpl}.
56 * </p>
57 * <p/>
58 * No <code>HttpSession</code> will be created by this filter if one does not
59 * already exist. If at the end of the web request the <code>HttpSession</code>
60 * does not exist, a <code>HttpSession</code> will <b>only</b> be created if
61 * the current contents of the <code>SecurityContextHolder</code> are not
62 * {@link java.lang.Object#equals(java.lang.Object)} to a <code>new</code>
63 * instance of {@link #setContext(Class)}. This avoids needless
64 * <code>HttpSession</code> creation, but automates the storage of changes
65 * made to the <code>SecurityContextHolder</code>. There is one exception to
66 * this rule, that is if the {@link #forceEagerSessionCreation} property is
67 * <code>true</code>, in which case sessions will always be created
68 * irrespective of normal session-minimisation logic (the default is
69 * <code>false</code>, as this is resource intensive and not recommended).
70 * </p>
71 * <p/>
72 * This filter will only execute once per request, to resolve servlet container
73 * (specifically Weblogic) incompatibilities.
74 * </p>
75 * <p/>
76 * If for whatever reason no <code>HttpSession</code> should <b>ever</b> be
77 * created (eg this filter is only being used with Basic authentication or
78 * similar clients that will never present the same <code>jsessionid</code>
79 * etc), the {@link #setAllowSessionCreation(boolean)} should be set to
80 * <code>false</code>. Only do this if you really need to conserve server
81 * memory and ensure all classes using the <code>SecurityContextHolder</code>
82 * are designed to have no persistence of the <code>SecurityContext</code>
83 * between web requests. Please note that if {@link #forceEagerSessionCreation}
84 * is <code>true</code>, the <code>allowSessionCreation</code> must also be
85 * <code>true</code> (setting it to <code>false</code> will cause a startup
86 * time error).
87 * </p>
88 * <p/>
89 * This filter MUST be executed BEFORE any authentication processing mechanisms.
90 * Authentication processing mechanisms (eg BASIC, CAS processing filters etc)
91 * expect the <code>SecurityContextHolder</code> to contain a valid
92 * <code>SecurityContext</code> by the time they execute.
93 * </p>
94 *
95 * @author Ben Alex
96 * @author Patrick Burleson
97 * @author Luke Taylor
98 * @author Martin Algesten
99 *
100 * @version $Id: HttpSessionContextIntegrationFilter.java 2004 2007-09-01 14:43:09Z raykrueger $
101 */
102 public class HttpSessionContextIntegrationFilter implements InitializingBean, Filter {
103 //~ Static fields/initializers =====================================================================================
104
105 protected static final Log logger = LogFactory.getLog(HttpSessionContextIntegrationFilter.class);
106
107 static final String FILTER_APPLIED = "__acegi_session_integration_filter_applied";
108
109 public static final String ACEGI_SECURITY_CONTEXT_KEY = "ACEGI_SECURITY_CONTEXT";
110
111 //~ Instance fields ================================================================================================
112
113 private Class context = SecurityContextImpl.class;
114
115 private Object contextObject;
116
117 /**
118 * Indicates if this filter can create a <code>HttpSession</code> if
119 * needed (sessions are always created sparingly, but setting this value to
120 * <code>false</code> will prohibit sessions from ever being created).
121 * Defaults to <code>true</code>. Do not set to <code>false</code> if
122 * you are have set {@link #forceEagerSessionCreation} to <code>true</code>,
123 * as the properties would be in conflict.
124 */
125 private boolean allowSessionCreation = true;
126
127 /**
128 * Indicates if this filter is required to create a <code>HttpSession</code>
129 * for every request before proceeding through the filter chain, even if the
130 * <code>HttpSession</code> would not ordinarily have been created. By
131 * default this is <code>false</code>, which is entirely appropriate for
132 * most circumstances as you do not want a <code>HttpSession</code>
133 * created unless the filter actually needs one. It is envisaged the main
134 * situation in which this property would be set to <code>true</code> is
135 * if using other filters that depend on a <code>HttpSession</code>
136 * already existing, such as those which need to obtain a session ID. This
137 * is only required in specialised cases, so leave it set to
138 * <code>false</code> unless you have an actual requirement and are
139 * conscious of the session creation overhead.
140 */
141 private boolean forceEagerSessionCreation = false;
142
143 /**
144 * Indicates whether the <code>SecurityContext</code> will be cloned from
145 * the <code>HttpSession</code>. The default is to simply reference (ie
146 * the default is <code>false</code>). The default may cause issues if
147 * concurrent threads need to have a different security identity from other
148 * threads being concurrently processed that share the same
149 * <code>HttpSession</code>. In most normal environments this does not
150 * represent an issue, as changes to the security identity in one thread is
151 * allowed to affect the security identitiy in other threads associated with
152 * the same <code>HttpSession</code>. For unusual cases where this is not
153 * permitted, change this value to <code>true</code> and ensure the
154 * {@link #context} is set to a <code>SecurityContext</code> that
155 * implements {@link Cloneable} and overrides the <code>clone()</code>
156 * method.
157 */
158 private boolean cloneFromHttpSession = false;
159
160 public boolean isCloneFromHttpSession() {
161 return cloneFromHttpSession;
162 }
163
164 public void setCloneFromHttpSession(boolean cloneFromHttpSession) {
165 this.cloneFromHttpSession = cloneFromHttpSession;
166 }
167
168 public HttpSessionContextIntegrationFilter() throws ServletException {
169 this.contextObject = generateNewContext();
170 }
171
172 //~ Methods ========================================================================================================
173
174 public void afterPropertiesSet() throws Exception {
175 if ((this.context == null) || (!SecurityContext.class.isAssignableFrom(this.context))) {
176 throw new IllegalArgumentException("context must be defined and implement SecurityContext "
177 + "(typically use org.acegisecurity.context.SecurityContextImpl; existing class is " + this.context
178 + ")");
179 }
180
181 if (forceEagerSessionCreation && !allowSessionCreation) {
182 throw new IllegalArgumentException(
183 "If using forceEagerSessionCreation, you must set allowSessionCreation to also be true");
184 }
185 }
186
187 public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException,
188 ServletException {
189
190 Assert.isInstanceOf(HttpServletRequest.class, req, "ServletRequest must be an instance of HttpServletRequest");
191 Assert.isInstanceOf(HttpServletResponse.class, res, "ServletResponse must be an instance of HttpServletResponse");
192
193 HttpServletRequest request = (HttpServletRequest) req;
194 HttpServletResponse response = (HttpServletResponse) res;
195
196 if (request.getAttribute(FILTER_APPLIED) != null) {
197 // ensure that filter is only applied once per request
198 chain.doFilter(request, response);
199
200 return;
201 }
202
203 HttpSession httpSession = null;
204
205 try {
206 httpSession = request.getSession(forceEagerSessionCreation);
207 }
208 catch (IllegalStateException ignored) {
209 }
210
211 boolean httpSessionExistedAtStartOfRequest = httpSession != null;
212
213 SecurityContext contextBeforeChainExecution = readSecurityContextFromSession(httpSession);
214
215 // Make the HttpSession null, as we don't want to keep a reference to it lying
216 // around in case chain.doFilter() invalidates it.
217 httpSession = null;
218
219 if (contextBeforeChainExecution == null) {
220 contextBeforeChainExecution = generateNewContext();
221
222 if (logger.isDebugEnabled()) {
223 logger.debug("New SecurityContext instance will be associated with SecurityContextHolder");
224 }
225 } else {
226 if (logger.isDebugEnabled()) {
227 logger.debug("Obtained a valid SecurityContext from ACEGI_SECURITY_CONTEXT to "
228 + "associate with SecurityContextHolder: '" + contextBeforeChainExecution + "'");
229 }
230 }
231
232 int contextHashBeforeChainExecution = contextBeforeChainExecution.hashCode();
233 request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
234
235 // Create a wrapper that will eagerly update the session with the security context
236 // if anything in the chain does a sendError() or sendRedirect().
237 // See SEC-398
238
239 OnRedirectUpdateSessionResponseWrapper responseWrapper =
240 new OnRedirectUpdateSessionResponseWrapper( response, request,
241 httpSessionExistedAtStartOfRequest, contextHashBeforeChainExecution );
242
243 // Proceed with chain
244
245 try {
246 // This is the only place in this class where SecurityContextHolder.setContext() is called
247 SecurityContextHolder.setContext(contextBeforeChainExecution);
248
249 chain.doFilter(request, responseWrapper);
250 }
251 finally {
252 // This is the only place in this class where SecurityContextHolder.getContext() is called
253 SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
254
255 // Crucial removal of SecurityContextHolder contents - do this before anything else.
256 SecurityContextHolder.clearContext();
257
258 request.removeAttribute(FILTER_APPLIED);
259
260 // storeSecurityContextInSession() might already be called by the response wrapper
261 // if something in the chain called sendError() or sendRedirect(). This ensures we only call it
262 // once per request.
263 if ( !responseWrapper.isSessionUpdateDone() ) {
264 storeSecurityContextInSession(contextAfterChainExecution, request,
265 httpSessionExistedAtStartOfRequest, contextHashBeforeChainExecution);
266 }
267
268 if (logger.isDebugEnabled()) {
269 logger.debug("SecurityContextHolder now cleared, as request processing completed");
270 }
271 }
272 }
273
274 /**
275 * Gets the security context from the session (if available) and returns it.
276 * <p/>
277 * If the session is null, the context object is null or the context object stored in the session
278 * is not an instance of SecurityContext it will return null.
279 * <p/>
280 * If <tt>cloneFromHttpSession</tt> is set to true, it will attempt to clone the context object
281 * and return the cloned instance.
282 *
283 * @param httpSession the session obtained from the request.
284 */
285 private SecurityContext readSecurityContextFromSession(HttpSession httpSession) {
286 if (httpSession == null) {
287 if (logger.isDebugEnabled()) {
288 logger.debug("No HttpSession currently exists");
289 }
290
291 return null;
292 }
293
294 // Session exists, so try to obtain a context from it.
295
296 Object contextFromSessionObject = httpSession.getAttribute(ACEGI_SECURITY_CONTEXT_KEY);
297
298 if (contextFromSessionObject == null) {
299 if (logger.isDebugEnabled()) {
300 logger.debug("HttpSession returned null object for ACEGI_SECURITY_CONTEXT");
301 }
302
303 return null;
304 }
305
306 // We now have the security context object from the session.
307
308 // Clone if required (see SEC-356)
309 if (cloneFromHttpSession) {
310 Assert.isInstanceOf(Cloneable.class, contextFromSessionObject,
311 "Context must implement Clonable and provide a Object.clone() method");
312 try {
313 Method m = contextFromSessionObject.getClass().getMethod("clone", new Class[]{});
314 if (!m.isAccessible()) {
315 m.setAccessible(true);
316 }
317 contextFromSessionObject = m.invoke(contextFromSessionObject, new Object[]{});
318 }
319 catch (Exception ex) {
320 ReflectionUtils.handleReflectionException(ex);
321 }
322 }
323
324 if (!(contextFromSessionObject instanceof SecurityContext)) {
325 if (logger.isWarnEnabled()) {
326 logger.warn("ACEGI_SECURITY_CONTEXT did not contain a SecurityContext but contained: '"
327 + contextFromSessionObject
328 + "'; are you improperly modifying the HttpSession directly "
329 + "(you should always use SecurityContextHolder) or using the HttpSession attribute "
330 + "reserved for this class?");
331 }
332
333 return null;
334 }
335
336 // Everything OK. The only non-null return from this method.
337
338 return (SecurityContext) contextFromSessionObject;
339 }
340
341 /**
342 * Stores the supplied security context in the session (if available) and if it has changed since it was
343 * set at the start of the request.
344 *
345 * @param securityContext the context object obtained from the SecurityContextHolder after the request has
346 * been processed by the filter chain. SecurityContextHolder.getContext() cannot be used to obtain
347 * the context as it has already been cleared by the time this method is called.
348 * @param request the request object (used to obtain the session, if one exists).
349 * @param httpSessionExistedAtStartOfRequest indicates whether there was a session in place before the
350 * filter chain executed. If this is true, and the session is found to be null, this indicates that it was
351 * invalidated during the request and a new session will now be created.
352 * @param contextHashBeforeChainExecution the hashcode of the context before the filter chain executed.
353 * The context will only be stored if it has a different hashcode, indicating that the context changed
354 * during the request.
355 *
356 */
357 private void storeSecurityContextInSession(SecurityContext securityContext,
358 HttpServletRequest request,
359 boolean httpSessionExistedAtStartOfRequest,
360 int contextHashBeforeChainExecution) {
361 HttpSession httpSession = null;
362
363 try {
364 httpSession = request.getSession(false);
365 }
366 catch (IllegalStateException ignored) {
367 }
368
369 if (httpSession == null) {
370 if (httpSessionExistedAtStartOfRequest) {
371 if (logger.isDebugEnabled()) {
372 logger.debug("HttpSession is now null, but was not null at start of request; "
373 + "session was invalidated, so do not create a new session");
374 }
375 } else {
376 // Generate a HttpSession only if we need to
377
378 if (!allowSessionCreation) {
379 if (logger.isDebugEnabled()) {
380 logger.debug("The HttpSession is currently null, and the "
381 + "HttpSessionContextIntegrationFilter is prohibited from creating an HttpSession "
382 + "(because the allowSessionCreation property is false) - SecurityContext thus not "
383 + "stored for next request");
384 }
385 } else if (!contextObject.equals(securityContext)) {
386 if (logger.isDebugEnabled()) {
387 logger.debug("HttpSession being created as SecurityContextHolder contents are non-default");
388 }
389
390 try {
391 httpSession = request.getSession(true);
392 }
393 catch (IllegalStateException ignored) {
394 }
395 } else {
396 if (logger.isDebugEnabled()) {
397 logger.debug("HttpSession is null, but SecurityContextHolder has not changed from default: ' "
398 + securityContext
399 + "'; not creating HttpSession or storing SecurityContextHolder contents");
400 }
401 }
402 }
403 }
404
405 // If HttpSession exists, store current SecurityContextHolder contents but only if
406 // the SecurityContext has actually changed (see JIRA SEC-37)
407 if (httpSession != null && securityContext.hashCode() != contextHashBeforeChainExecution) {
408 httpSession.setAttribute(ACEGI_SECURITY_CONTEXT_KEY, securityContext);
409
410 if (logger.isDebugEnabled()) {
411 logger.debug("SecurityContext stored to HttpSession: '" + securityContext + "'");
412 }
413 }
414 }
415
416 public SecurityContext generateNewContext() throws ServletException {
417 try {
418 return (SecurityContext) this.context.newInstance();
419 }
420 catch (InstantiationException ie) {
421 throw new ServletException(ie);
422 }
423 catch (IllegalAccessException iae) {
424 throw new ServletException(iae);
425 }
426 }
427
428 /**
429 * Does nothing. We use IoC container lifecycle services instead.
430 *
431 * @param filterConfig ignored
432 * @throws ServletException ignored
433 */
434 public void init(FilterConfig filterConfig) throws ServletException {
435 }
436
437 /**
438 * Does nothing. We use IoC container lifecycle services instead.
439 */
440 public void destroy() {
441 }
442
443 public boolean isAllowSessionCreation() {
444 return allowSessionCreation;
445 }
446
447 public void setAllowSessionCreation(boolean allowSessionCreation) {
448 this.allowSessionCreation = allowSessionCreation;
449 }
450
451 public Class getContext() {
452 return context;
453 }
454
455 public void setContext(Class secureContext) {
456 this.context = secureContext;
457 }
458
459 public boolean isForceEagerSessionCreation() {
460 return forceEagerSessionCreation;
461 }
462
463 public void setForceEagerSessionCreation(boolean forceEagerSessionCreation) {
464 this.forceEagerSessionCreation = forceEagerSessionCreation;
465 }
466
467
468 //~ Inner Classes ==================================================================================================
469
470 /**
471 * Wrapper that is applied to every request to update the <code>HttpSession<code> with
472 * the <code>SecurityContext</code> when a <code>sendError()</code> or <code>sendRedirect</code>
473 * happens. See SEC-398. The class contains the fields needed to call
474 * <code>storeSecurityContextInSession()</code>
475 */
476 private class OnRedirectUpdateSessionResponseWrapper extends HttpServletResponseWrapper {
477
478 HttpServletRequest request;
479 boolean httpSessionExistedAtStartOfRequest;
480 int contextHashBeforeChainExecution;
481
482 // Used to ensure storeSecurityContextInSession() is only
483 // called once.
484 boolean sessionUpdateDone = false;
485
486 /**
487 * Takes the parameters required to call <code>storeSecurityContextInSession()</code> in
488 * addition to the response object we are wrapping.
489 * @see HttpSessionContextIntegrationFilter#storeSecurityContextInSession(SecurityContext, ServletRequest, boolean, int)
490 */
491 public OnRedirectUpdateSessionResponseWrapper(HttpServletResponse response,
492 HttpServletRequest request,
493 boolean httpSessionExistedAtStartOfRequest,
494 int contextHashBeforeChainExecution) {
495 super(response);
496 this.request = request;
497 this.httpSessionExistedAtStartOfRequest = httpSessionExistedAtStartOfRequest;
498 this.contextHashBeforeChainExecution = contextHashBeforeChainExecution;
499 }
500
501 /**
502 * Makes sure the session is updated before calling the
503 * superclass <code>sendError()</code>
504 */
505 public void sendError(int sc) throws IOException {
506 doSessionUpdate();
507 super.sendError(sc);
508 }
509
510 /**
511 * Makes sure the session is updated before calling the
512 * superclass <code>sendError()</code>
513 */
514 public void sendError(int sc, String msg) throws IOException {
515 doSessionUpdate();
516 super.sendError(sc, msg);
517 }
518
519 /**
520 * Makes sure the session is updated before calling the
521 * superclass <code>sendRedirect()</code>
522 */
523 public void sendRedirect(String location) throws IOException {
524 doSessionUpdate();
525 super.sendRedirect(location);
526 }
527
528 /**
529 * Calls <code>storeSecurityContextInSession()</code>
530 */
531 private void doSessionUpdate() {
532 if (sessionUpdateDone) {
533 return;
534 }
535 SecurityContext securityContext = SecurityContextHolder.getContext();
536 storeSecurityContextInSession(securityContext, request,
537 httpSessionExistedAtStartOfRequest, contextHashBeforeChainExecution);
538 sessionUpdateDone = true;
539 }
540
541 /**
542 * Tells if the response wrapper has called
543 * <code>storeSecurityContextInSession()</code>.
544 */
545 public boolean isSessionUpdateDone() {
546 return sessionUpdateDone;
547 }
548
549 }
550
551 }