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.context;
17  
18  import org.apache.commons.logging.Log;
19  import org.apache.commons.logging.LogFactory;
20  
21  import org.springframework.beans.factory.InitializingBean;
22  
23  import java.io.IOException;
24  
25  import javax.servlet.Filter;
26  import javax.servlet.FilterChain;
27  import javax.servlet.FilterConfig;
28  import javax.servlet.ServletException;
29  import javax.servlet.ServletRequest;
30  import javax.servlet.ServletResponse;
31  import javax.servlet.http.HttpServletRequest;
32  import javax.servlet.http.HttpSession;
33  
34  
35  /***
36   * <p>
37   * Populates the {@link SecurityContextHolder}</code> with information obtained
38   * from the <code>HttpSession</code>.
39   * </p>
40   * 
41   * <p>
42   * The <code>HttpSession</code> will be queried to retrieve the
43   * <code>SecurityContext</code> that should be stored against the
44   * <code>SecurityContextHolder</code> for the duration of the web request. At
45   * the end of the web request, any updates made to the
46   * <code>SecurityContextHolder</code> will be persisted back to the
47   * <code>HttpSession</code> by this filter.
48   * </p>
49   * 
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
56   * org.acegisecurity.context.SecurityContextImpl}.
57   * </p>
58   * 
59   * <p>
60   * No <code>HttpSession</code> will be created by this filter if one does not
61   * already exist. If at the end of the web request the
62   * <code>HttpSession</code> does not exist, a <code>HttpSession</code> will
63   * <b>only</b> be created if the current contents of the
64   * <code>SecurityContextHolder</code> are not {@link
65   * java.lang.Object#equals(java.lang.Object)} to a <code>new</code> instance
66   * of {@link #setContext(Class)}. This avoids needless
67   * <code>HttpSession</code> creation, but automates the storage of changes
68   * made to the <code>SecurityContextHolder</code>.
69   * </p>
70   * 
71   * <p>
72   * This filter will only execute once per request, to resolve servlet container
73   * (specifically Weblogic) incompatibilities.
74   * </p>
75   * 
76   * <p>
77   * If for whatever reason no <code>HttpSession</code> should <b>ever</b> be
78   * created (eg this filter is only being used with Basic authentication or
79   * similar clients that will never present the same <code>jsessionid</code>
80   * etc), the  {@link #setAllowSessionCreation(boolean)} should be set to
81   * <code>false</code>. Only do this if you really need to conserve server
82   * memory and ensure all classes using the <code>SecurityContextHolder</code> are
83   * designed to have no persistence of the <code>SecurityContext</code> between web
84   * requests.
85   * </p>
86   * 
87   * <p>
88   * This filter MUST be executed BEFORE any authentication procesing mechanisms.
89   * Authentication processing mechanisms (eg BASIC, CAS processing filters etc)
90   * expect the <code>SecurityContextHolder</code> to contain a valid
91   * <code>SecurityContext</code> by the time they execute.
92   * </p>
93   *
94   * @author Ben Alex
95   * @author Patrick Burleson
96   * @version $Id: HttpSessionContextIntegrationFilter.java,v 1.12 2005/11/17 00:55:49 benalex Exp $
97   */
98  public class HttpSessionContextIntegrationFilter implements InitializingBean,
99      Filter {
100     //~ Static fields/initializers =============================================
101 
102     protected static final Log logger = LogFactory.getLog(HttpSessionContextIntegrationFilter.class);
103     private static final String FILTER_APPLIED = "__acegi_session_integration_filter_applied";
104     public static final String ACEGI_SECURITY_CONTEXT_KEY = "ACEGI_SECURITY_CONTEXT";
105 
106     //~ Instance fields ========================================================
107 
108     private Class context = SecurityContextImpl.class;
109     private Object contextObject;
110 
111     /***
112      * Indicates if this filter can create a <code>HttpSession</code> if needed
113      * (sessions are always created sparingly, but setting this value to false
114      * will prohibit sessions from ever being created). Defaults to true.
115      */
116     private boolean allowSessionCreation = true;
117 
118     //~ Methods ================================================================
119 
120     public void setAllowSessionCreation(boolean allowSessionCreation) {
121         this.allowSessionCreation = allowSessionCreation;
122     }
123 
124     public boolean isAllowSessionCreation() {
125         return allowSessionCreation;
126     }
127 
128     public void setContext(Class secureContext) {
129         this.context = secureContext;
130     }
131 
132     public Class getContext() {
133         return context;
134     }
135 
136     public void afterPropertiesSet() throws Exception {
137         if ((this.context == null)
138             || (!SecurityContext.class.isAssignableFrom(this.context))) {
139             throw new IllegalArgumentException(
140                 "context must be defined and implement SecurityContext (typically use org.acegisecurity.context.SecurityContextImpl; existing class is "
141                 + this.context + ")");
142         }
143 
144         this.contextObject = generateNewContext();
145     }
146 
147     /***
148      * Does nothing. We use IoC container lifecycle services instead.
149      */
150     public void destroy() {}
151 
152     public void doFilter(ServletRequest request, ServletResponse response,
153         FilterChain chain) throws IOException, ServletException {
154         if ((request != null) && (request.getAttribute(FILTER_APPLIED) != null)) {
155             // ensure that filter is only applied once per request
156             chain.doFilter(request, response);
157         } else {
158             if (request != null) {
159                 request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
160             }
161 
162             HttpSession httpSession = null;
163             boolean httpSessionExistedAtStartOfRequest = false;
164 
165             try {
166                 httpSession = ((HttpServletRequest) request).getSession(false);
167             } catch (IllegalStateException ignored) {}
168 
169             if (httpSession != null) {
170                 httpSessionExistedAtStartOfRequest = true;
171 
172                 Object contextFromSessionObject = httpSession.getAttribute(ACEGI_SECURITY_CONTEXT_KEY);
173 
174                 if (contextFromSessionObject != null) {
175                     if (contextFromSessionObject instanceof SecurityContext) {
176                         if (logger.isDebugEnabled()) {
177                             logger.debug(
178                                 "Obtained from ACEGI_SECURITY_CONTEXT a valid SecurityContext and set to SecurityContextHolder: '"
179                                 + contextFromSessionObject + "'");
180                         }
181 
182                         SecurityContextHolder.setContext((SecurityContext) contextFromSessionObject);
183                     } else {
184                         if (logger.isWarnEnabled()) {
185                             logger.warn(
186                                 "ACEGI_SECURITY_CONTEXT did not contain a SecurityContext but contained: '"
187                                 + contextFromSessionObject
188                                 + "'; are you improperly modifying the HttpSession directly (you should always use SecurityContextHolder) or using the HttpSession attribute reserved for this class? - new SecurityContext instance associated with SecurityContextHolder");
189                         }
190 
191                         SecurityContextHolder.setContext(generateNewContext());
192                     }
193                 } else {
194                     if (logger.isDebugEnabled()) {
195                         logger.debug(
196                             "HttpSession returned null object for ACEGI_SECURITY_CONTEXT - new SecurityContext instance associated with SecurityContextHolder");
197                     }
198 
199                     SecurityContextHolder.setContext(generateNewContext());
200                 }
201             } else {
202                 if (logger.isDebugEnabled()) {
203                     logger.debug(
204                         "No HttpSession currently exists - new SecurityContext instance associated with SecurityContextHolder");
205                 }
206 
207                 SecurityContextHolder.setContext(generateNewContext());
208             }
209 
210             // Make the HttpSession null, as we want to ensure we don't keep
211             // a reference to the HttpSession laying around in case the
212             // chain.doFilter() invalidates it.
213             httpSession = null;
214 
215             // Proceed with chain
216             int contextWhenChainProceeded = SecurityContextHolder.getContext()
217                                                                  .hashCode();
218 
219             try {
220                 chain.doFilter(request, response);
221             } catch (IOException ioe) {
222                 throw ioe;
223             } catch (ServletException se) {
224                 throw se;
225             } finally {
226                 // do clean up, even if there was an exception
227                 // Store context back to HttpSession
228                 try {
229                     httpSession = ((HttpServletRequest) request).getSession(false);
230                 } catch (IllegalStateException ignored) {}
231 
232                 if ((httpSession == null) && httpSessionExistedAtStartOfRequest) {
233                     if (logger.isDebugEnabled()) {
234                         logger.debug(
235                             "HttpSession is now null, but was not null at start of request; session was invalidated, so do not create a new session");
236                     }
237                 }
238 
239                 // Generate a HttpSession only if we need to
240                 if ((httpSession == null)
241                     && !httpSessionExistedAtStartOfRequest) {
242                     if (!allowSessionCreation) {
243                         if (logger.isDebugEnabled()) {
244                             logger.debug(
245                                 "The HttpSession is currently null, and the HttpSessionContextIntegrationFilter is prohibited from creating a HttpSession (because the allowSessionCreation property is false) - SecurityContext thus not stored for next request");
246                         }
247                     } else if (!contextObject.equals(
248                             SecurityContextHolder.getContext())) {
249                         if (logger.isDebugEnabled()) {
250                             logger.debug(
251                                 "HttpSession being created as SecurityContextHolder contents are non-default");
252                         }
253 
254                         try {
255                             httpSession = ((HttpServletRequest) request)
256                                 .getSession(true);
257                         } catch (IllegalStateException ignored) {}
258                     } else {
259                         if (logger.isDebugEnabled()) {
260                             logger.debug(
261                                 "HttpSession is null, but SecurityContextHolder has not changed from default: ' "
262                                 + SecurityContextHolder.getContext()
263                                 + "'; not creating HttpSession or storing SecurityContextHolder contents");
264                         }
265                     }
266                 }
267 
268                 // If HttpSession exists, store current SecurityContextHolder contents
269                 // but only if SecurityContext has actually changed (see JIRA SEC-37)
270                 if ((httpSession != null)
271                     && (SecurityContextHolder.getContext().hashCode() != contextWhenChainProceeded)) {
272                     httpSession.setAttribute(ACEGI_SECURITY_CONTEXT_KEY,
273                         SecurityContextHolder.getContext());
274 
275                     if (logger.isDebugEnabled()) {
276                         logger.debug("SecurityContext stored to HttpSession: '"
277                             + SecurityContextHolder.getContext() + "'");
278                     }
279                 }
280 
281                 // Remove SecurityContextHolder contents
282                 SecurityContextHolder.setContext(generateNewContext());
283 
284                 if (logger.isDebugEnabled()) {
285                     logger.debug(
286                         "SecurityContextHolder set to new context, as request processing completed");
287                 }
288             }
289         }
290     }
291 
292     public SecurityContext generateNewContext() throws ServletException {
293         try {
294             return (SecurityContext) this.context.newInstance();
295         } catch (InstantiationException ie) {
296             throw new ServletException(ie);
297         } catch (IllegalAccessException iae) {
298             throw new ServletException(iae);
299         }
300     }
301 
302     /***
303      * Does nothing. We use IoC container lifecycle services instead.
304      *
305      * @param filterConfig ignored
306      *
307      * @throws ServletException ignored
308      */
309     public void init(FilterConfig filterConfig) throws ServletException {}
310 }