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