View Javadoc

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 }