View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.shiro.web.util;
20  
21  import org.apache.shiro.SecurityUtils;
22  import org.apache.shiro.session.Session;
23  import org.apache.shiro.subject.Subject;
24  import org.apache.shiro.subject.support.DefaultSubjectContext;
25  import org.apache.shiro.util.StringUtils;
26  import org.apache.shiro.web.env.EnvironmentLoader;
27  import org.apache.shiro.web.env.WebEnvironment;
28  import org.apache.shiro.web.filter.AccessControlFilter;
29  import org.slf4j.Logger;
30  import org.slf4j.LoggerFactory;
31  
32  import javax.servlet.ServletContext;
33  import javax.servlet.ServletRequest;
34  import javax.servlet.ServletResponse;
35  import javax.servlet.http.HttpServletRequest;
36  import javax.servlet.http.HttpServletResponse;
37  import java.io.IOException;
38  import java.io.UnsupportedEncodingException;
39  import java.net.URLDecoder;
40  import java.util.Map;
41  
42  /**
43   * Simple utility class for operations used across multiple class hierarchies in the web framework code.
44   * <p/>
45   * Some methods in this class were copied from the Spring Framework so we didn't have to re-invent the wheel,
46   * and in these cases, we have retained all license, copyright and author information.
47   *
48   * @since 0.9
49   */
50  public class WebUtils {
51  
52      //TODO - complete JavaDoc
53  
54      private static final Logger log = LoggerFactory.getLogger(WebUtils.class);
55  
56      public static final String SERVLET_REQUEST_KEY = ServletRequest.class.getName() + "_SHIRO_THREAD_CONTEXT_KEY";
57      public static final String SERVLET_RESPONSE_KEY = ServletResponse.class.getName() + "_SHIRO_THREAD_CONTEXT_KEY";
58  
59      /**
60       * {@link org.apache.shiro.session.Session Session} key used to save a request and later restore it, for example when redirecting to a
61       * requested page after login, equal to {@code shiroSavedRequest}.
62       */
63      public static final String SAVED_REQUEST_KEY = "shiroSavedRequest";
64  
65      /**
66       * Standard Servlet 2.3+ spec request attributes for include URI and paths.
67       * <p>If included via a RequestDispatcher, the current resource will see the
68       * originating request. Its own URI and paths are exposed as request attributes.
69       */
70      public static final String INCLUDE_REQUEST_URI_ATTRIBUTE = "javax.servlet.include.request_uri";
71      public static final String INCLUDE_CONTEXT_PATH_ATTRIBUTE = "javax.servlet.include.context_path";
72      public static final String INCLUDE_SERVLET_PATH_ATTRIBUTE = "javax.servlet.include.servlet_path";
73      public static final String INCLUDE_PATH_INFO_ATTRIBUTE = "javax.servlet.include.path_info";
74      public static final String INCLUDE_QUERY_STRING_ATTRIBUTE = "javax.servlet.include.query_string";
75  
76      /**
77       * Standard Servlet 2.4+ spec request attributes for forward URI and paths.
78       * <p>If forwarded to via a RequestDispatcher, the current resource will see its
79       * own URI and paths. The originating URI and paths are exposed as request attributes.
80       */
81      public static final String FORWARD_REQUEST_URI_ATTRIBUTE = "javax.servlet.forward.request_uri";
82      public static final String FORWARD_CONTEXT_PATH_ATTRIBUTE = "javax.servlet.forward.context_path";
83      public static final String FORWARD_SERVLET_PATH_ATTRIBUTE = "javax.servlet.forward.servlet_path";
84      public static final String FORWARD_PATH_INFO_ATTRIBUTE = "javax.servlet.forward.path_info";
85      public static final String FORWARD_QUERY_STRING_ATTRIBUTE = "javax.servlet.forward.query_string";
86  
87      /**
88       * Default character encoding to use when <code>request.getCharacterEncoding</code>
89       * returns <code>null</code>, according to the Servlet spec.
90       *
91       * @see javax.servlet.ServletRequest#getCharacterEncoding
92       */
93      public static final String DEFAULT_CHARACTER_ENCODING = "ISO-8859-1";
94  
95      /**
96       * Return the path within the web application for the given request.
97       * Detects include request URL if called within a RequestDispatcher include.
98       * <p/>
99       * For example, for a request to URL
100      * <p/>
101      * <code>http://www.somehost.com/myapp/my/url.jsp</code>,
102      * <p/>
103      * for an application deployed to <code>/mayapp</code> (the application's context path), this method would return
104      * <p/>
105      * <code>/my/url.jsp</code>.
106      *
107      * @param request current HTTP request
108      * @return the path within the web application
109      */
110     public static String getPathWithinApplication(HttpServletRequest request) {
111         String contextPath = getContextPath(request);
112         String requestUri = getRequestUri(request);
113         if (StringUtils.startsWithIgnoreCase(requestUri, contextPath)) {
114             // Normal case: URI contains context path.
115             String path = requestUri.substring(contextPath.length());
116             return (StringUtils.hasText(path) ? path : "/");
117         } else {
118             // Special case: rather unusual.
119             return requestUri;
120         }
121     }
122 
123     /**
124      * Return the request URI for the given request, detecting an include request
125      * URL if called within a RequestDispatcher include.
126      * <p>As the value returned by <code>request.getRequestURI()</code> is <i>not</i>
127      * decoded by the servlet container, this method will decode it.
128      * <p>The URI that the web container resolves <i>should</i> be correct, but some
129      * containers like JBoss/Jetty incorrectly include ";" strings like ";jsessionid"
130      * in the URI. This method cuts off such incorrect appendices.
131      *
132      * @param request current HTTP request
133      * @return the request URI
134      */
135     public static String getRequestUri(HttpServletRequest request) {
136         String uri = (String) request.getAttribute(INCLUDE_REQUEST_URI_ATTRIBUTE);
137         if (uri == null) {
138             uri = request.getRequestURI();
139         }
140         return normalize(decodeAndCleanUriString(request, uri));
141     }
142 
143     /**
144      * Normalize a relative URI path that may have relative values ("/./",
145      * "/../", and so on ) it it.  <strong>WARNING</strong> - This method is
146      * useful only for normalizing application-generated paths.  It does not
147      * try to perform security checks for malicious input.
148      * Normalize operations were was happily taken from org.apache.catalina.util.RequestUtil in
149      * Tomcat trunk, r939305
150      *
151      * @param path Relative path to be normalized
152      * @return normalized path
153      */
154     public static String normalize(String path) {
155         return normalize(path, true);
156     }
157 
158     /**
159      * Normalize a relative URI path that may have relative values ("/./",
160      * "/../", and so on ) it it.  <strong>WARNING</strong> - This method is
161      * useful only for normalizing application-generated paths.  It does not
162      * try to perform security checks for malicious input.
163      * Normalize operations were was happily taken from org.apache.catalina.util.RequestUtil in
164      * Tomcat trunk, r939305
165      *
166      * @param path             Relative path to be normalized
167      * @param replaceBackSlash Should '\\' be replaced with '/'
168      * @return normalized path
169      */
170     private static String normalize(String path, boolean replaceBackSlash) {
171 
172         if (path == null)
173             return null;
174 
175         // Create a place for the normalized path
176         String normalized = path;
177 
178         if (replaceBackSlash && normalized.indexOf('\\') >= 0)
179             normalized = normalized.replace('\\', '/');
180 
181         if (normalized.equals("/."))
182             return "/";
183 
184         // Add a leading "/" if necessary
185         if (!normalized.startsWith("/"))
186             normalized = "/" + normalized;
187 
188         // Resolve occurrences of "//" in the normalized path
189         while (true) {
190             int index = normalized.indexOf("//");
191             if (index < 0)
192                 break;
193             normalized = normalized.substring(0, index) +
194                     normalized.substring(index + 1);
195         }
196 
197         // Resolve occurrences of "/./" in the normalized path
198         while (true) {
199             int index = normalized.indexOf("/./");
200             if (index < 0)
201                 break;
202             normalized = normalized.substring(0, index) +
203                     normalized.substring(index + 2);
204         }
205 
206         // Resolve occurrences of "/../" in the normalized path
207         while (true) {
208             int index = normalized.indexOf("/../");
209             if (index < 0)
210                 break;
211             if (index == 0)
212                 return (null);  // Trying to go outside our context
213             int index2 = normalized.lastIndexOf('/', index - 1);
214             normalized = normalized.substring(0, index2) +
215                     normalized.substring(index + 3);
216         }
217 
218         // Return the normalized path that we have completed
219         return (normalized);
220 
221     }
222 
223 
224     /**
225      * Decode the supplied URI string and strips any extraneous portion after a ';'.
226      *
227      * @param request the incoming HttpServletRequest
228      * @param uri     the application's URI string
229      * @return the supplied URI string stripped of any extraneous portion after a ';'.
230      */
231     private static String decodeAndCleanUriString(HttpServletRequest request, String uri) {
232         uri = decodeRequestString(request, uri);
233         int semicolonIndex = uri.indexOf(';');
234         return (semicolonIndex != -1 ? uri.substring(0, semicolonIndex) : uri);
235     }
236 
237     /**
238      * Return the context path for the given request, detecting an include request
239      * URL if called within a RequestDispatcher include.
240      * <p>As the value returned by <code>request.getContextPath()</code> is <i>not</i>
241      * decoded by the servlet container, this method will decode it.
242      *
243      * @param request current HTTP request
244      * @return the context path
245      */
246     public static String getContextPath(HttpServletRequest request) {
247         String contextPath = (String) request.getAttribute(INCLUDE_CONTEXT_PATH_ATTRIBUTE);
248         if (contextPath == null) {
249             contextPath = request.getContextPath();
250         }
251         contextPath = normalize(decodeRequestString(request, contextPath));
252         if ("/".equals(contextPath)) {
253             // the normalize method will return a "/" and includes on Jetty, will also be a "/".
254             contextPath = "";
255         }
256         return contextPath;
257     }
258 
259     /**
260      * Find the Shiro {@link WebEnvironment} for this web application, which is typically loaded via the
261      * {@link org.apache.shiro.web.env.EnvironmentLoaderListener}.
262      * <p/>
263      * This implementation rethrows an exception that happened on environment startup to differentiate between a failed
264      * environment startup and no environment at all.
265      *
266      * @param sc ServletContext to find the web application context for
267      * @return the root WebApplicationContext for this web app
268      * @throws IllegalStateException if the root WebApplicationContext could not be found
269      * @see org.apache.shiro.web.env.EnvironmentLoader#ENVIRONMENT_ATTRIBUTE_KEY
270      * @since 1.2
271      */
272     public static WebEnvironment getRequiredWebEnvironment(ServletContext sc)
273             throws IllegalStateException {
274 
275         WebEnvironment we = getWebEnvironment(sc);
276         if (we == null) {
277             throw new IllegalStateException("No WebEnvironment found: no EnvironmentLoaderListener registered?");
278         }
279         return we;
280     }
281 
282     /**
283      * Find the Shiro {@link WebEnvironment} for this web application, which is typically loaded via
284      * {@link org.apache.shiro.web.env.EnvironmentLoaderListener}.
285      * <p/>
286      * This implementation rethrows an exception that happened on environment startup to differentiate between a failed
287      * environment startup and no environment at all.
288      *
289      * @param sc ServletContext to find the web application context for
290      * @return the root WebApplicationContext for this web app, or <code>null</code> if none
291      * @see org.apache.shiro.web.env.EnvironmentLoader#ENVIRONMENT_ATTRIBUTE_KEY
292      * @since 1.2
293      */
294     public static WebEnvironment getWebEnvironment(ServletContext sc) {
295         return getWebEnvironment(sc, EnvironmentLoader.ENVIRONMENT_ATTRIBUTE_KEY);
296     }
297 
298     /**
299      * Find the Shiro {@link WebEnvironment} for this web application.
300      *
301      * @param sc       ServletContext to find the web application context for
302      * @param attrName the name of the ServletContext attribute to look for
303      * @return the desired WebEnvironment for this web app, or <code>null</code> if none
304      * @since 1.2
305      */
306     public static WebEnvironment getWebEnvironment(ServletContext sc, String attrName) {
307         if (sc == null) {
308             throw new IllegalArgumentException("ServletContext argument must not be null.");
309         }
310         Object attr = sc.getAttribute(attrName);
311         if (attr == null) {
312             return null;
313         }
314         if (attr instanceof RuntimeException) {
315             throw (RuntimeException) attr;
316         }
317         if (attr instanceof Error) {
318             throw (Error) attr;
319         }
320         if (attr instanceof Exception) {
321             throw new IllegalStateException((Exception) attr);
322         }
323         if (!(attr instanceof WebEnvironment)) {
324             throw new IllegalStateException("Context attribute is not of type WebEnvironment: " + attr);
325         }
326         return (WebEnvironment) attr;
327     }
328 
329 
330     /**
331      * Decode the given source string with a URLDecoder. The encoding will be taken
332      * from the request, falling back to the default "ISO-8859-1".
333      * <p>The default implementation uses <code>URLDecoder.decode(input, enc)</code>.
334      *
335      * @param request current HTTP request
336      * @param source  the String to decode
337      * @return the decoded String
338      * @see #DEFAULT_CHARACTER_ENCODING
339      * @see javax.servlet.ServletRequest#getCharacterEncoding
340      * @see java.net.URLDecoder#decode(String, String)
341      * @see java.net.URLDecoder#decode(String)
342      */
343     @SuppressWarnings({"deprecation"})
344     public static String decodeRequestString(HttpServletRequest request, String source) {
345         String enc = determineEncoding(request);
346         try {
347             return URLDecoder.decode(source, enc);
348         } catch (UnsupportedEncodingException ex) {
349             if (log.isWarnEnabled()) {
350                 log.warn("Could not decode request string [" + source + "] with encoding '" + enc +
351                         "': falling back to platform default encoding; exception message: " + ex.getMessage());
352             }
353             return URLDecoder.decode(source);
354         }
355     }
356 
357     /**
358      * Determine the encoding for the given request.
359      * Can be overridden in subclasses.
360      * <p>The default implementation checks the request's
361      * {@link ServletRequest#getCharacterEncoding() character encoding}, and if that
362      * <code>null</code>, falls back to the {@link #DEFAULT_CHARACTER_ENCODING}.
363      *
364      * @param request current HTTP request
365      * @return the encoding for the request (never <code>null</code>)
366      * @see javax.servlet.ServletRequest#getCharacterEncoding()
367      */
368     protected static String determineEncoding(HttpServletRequest request) {
369         String enc = request.getCharacterEncoding();
370         if (enc == null) {
371             enc = DEFAULT_CHARACTER_ENCODING;
372         }
373         return enc;
374     }
375 
376     /*
377      * Returns {@code true} IFF the specified {@code SubjectContext}:
378      * <ol>
379      * <li>A {@link WebSubjectContext} instance</li>
380      * <li>The {@code WebSubjectContext}'s request/response pair are not null</li>
381      * <li>The request is an {@link HttpServletRequest} instance</li>
382      * <li>The response is an {@link HttpServletResponse} instance</li>
383      * </ol>
384      *
385      * @param context the SubjectContext to check to see if it is HTTP compatible.
386      * @return {@code true} IFF the specified context has HTTP request/response objects, {@code false} otherwise.
387      * @since 1.0
388      */
389 
390     public static boolean isWeb(Object requestPairSource) {
391         return requestPairSource instanceof RequestPairSource && isWeb((RequestPairSource) requestPairSource);
392     }
393 
394     public static boolean isHttp(Object requestPairSource) {
395         return requestPairSource instanceof RequestPairSource && isHttp((RequestPairSource) requestPairSource);
396     }
397 
398     public static ServletRequest getRequest(Object requestPairSource) {
399         if (requestPairSource instanceof RequestPairSource) {
400             return ((RequestPairSource) requestPairSource).getServletRequest();
401         }
402         return null;
403     }
404 
405     public static ServletResponse getResponse(Object requestPairSource) {
406         if (requestPairSource instanceof RequestPairSource) {
407             return ((RequestPairSource) requestPairSource).getServletResponse();
408         }
409         return null;
410     }
411 
412     public static HttpServletRequest getHttpRequest(Object requestPairSource) {
413         ServletRequest request = getRequest(requestPairSource);
414         if (request instanceof HttpServletRequest) {
415             return (HttpServletRequest) request;
416         }
417         return null;
418     }
419 
420     public static HttpServletResponse getHttpResponse(Object requestPairSource) {
421         ServletResponse response = getResponse(requestPairSource);
422         if (response instanceof HttpServletResponse) {
423             return (HttpServletResponse) response;
424         }
425         return null;
426     }
427 
428     private static boolean isWeb(RequestPairSource source) {
429         ServletRequest request = source.getServletRequest();
430         ServletResponse response = source.getServletResponse();
431         return request != null && response != null;
432     }
433 
434     private static boolean isHttp(RequestPairSource source) {
435         ServletRequest request = source.getServletRequest();
436         ServletResponse response = source.getServletResponse();
437         return request instanceof HttpServletRequest && response instanceof HttpServletResponse;
438     }
439 
440     /**
441      * Returns {@code true} if a session is allowed to be created for a subject-associated request, {@code false}
442      * otherwise.
443      * <p/>
444      * <b>This method exists for Shiro's internal framework needs and should never be called by Shiro end-users.  It
445      * could be changed/removed at any time.</b>
446      *
447      * @param requestPairSource a {@link RequestPairSource} instance, almost always a
448      *                          {@link org.apache.shiro.web.subject.WebSubject WebSubject} instance.
449      * @return {@code true} if a session is allowed to be created for a subject-associated request, {@code false}
450      *         otherwise.
451      */
452     public static boolean _isSessionCreationEnabled(Object requestPairSource) {
453         if (requestPairSource instanceof RequestPairSource) {
454             RequestPairSource source = (RequestPairSource) requestPairSource;
455             return _isSessionCreationEnabled(source.getServletRequest());
456         }
457         return true; //by default
458     }
459 
460     /**
461      * Returns {@code true} if a session is allowed to be created for a subject-associated request, {@code false}
462      * otherwise.
463      * <p/>
464      * <b>This method exists for Shiro's internal framework needs and should never be called by Shiro end-users.  It
465      * could be changed/removed at any time.</b>
466      *
467      * @param request incoming servlet request.
468      * @return {@code true} if a session is allowed to be created for a subject-associated request, {@code false}
469      *         otherwise.
470      */
471     public static boolean _isSessionCreationEnabled(ServletRequest request) {
472         if (request != null) {
473             Object val = request.getAttribute(DefaultSubjectContext.SESSION_CREATION_ENABLED);
474             if (val != null && val instanceof Boolean) {
475                 return (Boolean) val;
476             }
477         }
478         return true; //by default
479     }
480 
481     /**
482      * A convenience method that merely casts the incoming <code>ServletRequest</code> to an
483      * <code>HttpServletRequest</code>:
484      * <p/>
485      * <code>return (HttpServletRequest)request;</code>
486      * <p/>
487      * Logic could be changed in the future for logging or throwing an meaningful exception in
488      * non HTTP request environments (e.g. Portlet API).
489      *
490      * @param request the incoming ServletRequest
491      * @return the <code>request</code> argument casted to an <code>HttpServletRequest</code>.
492      */
493     public static HttpServletRequest toHttp(ServletRequest request) {
494         return (HttpServletRequest) request;
495     }
496 
497     /**
498      * A convenience method that merely casts the incoming <code>ServletResponse</code> to an
499      * <code>HttpServletResponse</code>:
500      * <p/>
501      * <code>return (HttpServletResponse)response;</code>
502      * <p/>
503      * Logic could be changed in the future for logging or throwing an meaningful exception in
504      * non HTTP request environments (e.g. Portlet API).
505      *
506      * @param response the outgoing ServletResponse
507      * @return the <code>response</code> argument casted to an <code>HttpServletResponse</code>.
508      */
509     public static HttpServletResponse toHttp(ServletResponse response) {
510         return (HttpServletResponse) response;
511     }
512 
513     /**
514      * Redirects the current request to a new URL based on the given parameters.
515      *
516      * @param request          the servlet request.
517      * @param response         the servlet response.
518      * @param url              the URL to redirect the user to.
519      * @param queryParams      a map of parameters that should be set as request parameters for the new request.
520      * @param contextRelative  true if the URL is relative to the servlet context path, or false if the URL is absolute.
521      * @param http10Compatible whether to stay compatible with HTTP 1.0 clients.
522      * @throws java.io.IOException if thrown by response methods.
523      */
524     public static void issueRedirect(ServletRequest request, ServletResponse response, String url, Map queryParams, boolean contextRelative, boolean http10Compatible) throws IOException {
525         RedirectView view = new RedirectView(url, contextRelative, http10Compatible);
526         view.renderMergedOutputModel(queryParams, toHttp(request), toHttp(response));
527     }
528 
529     /**
530      * Redirects the current request to a new URL based on the given parameters and default values
531      * for unspecified parameters.
532      *
533      * @param request  the servlet request.
534      * @param response the servlet response.
535      * @param url      the URL to redirect the user to.
536      * @throws java.io.IOException if thrown by response methods.
537      */
538     public static void issueRedirect(ServletRequest request, ServletResponse response, String url) throws IOException {
539         issueRedirect(request, response, url, null, true, true);
540     }
541 
542     /**
543      * Redirects the current request to a new URL based on the given parameters and default values
544      * for unspecified parameters.
545      *
546      * @param request     the servlet request.
547      * @param response    the servlet response.
548      * @param url         the URL to redirect the user to.
549      * @param queryParams a map of parameters that should be set as request parameters for the new request.
550      * @throws java.io.IOException if thrown by response methods.
551      */
552     public static void issueRedirect(ServletRequest request, ServletResponse response, String url, Map queryParams) throws IOException {
553         issueRedirect(request, response, url, queryParams, true, true);
554     }
555 
556     /**
557      * Redirects the current request to a new URL based on the given parameters and default values
558      * for unspecified parameters.
559      *
560      * @param request         the servlet request.
561      * @param response        the servlet response.
562      * @param url             the URL to redirect the user to.
563      * @param queryParams     a map of parameters that should be set as request parameters for the new request.
564      * @param contextRelative true if the URL is relative to the servlet context path, or false if the URL is absolute.
565      * @throws java.io.IOException if thrown by response methods.
566      */
567     public static void issueRedirect(ServletRequest request, ServletResponse response, String url, Map queryParams, boolean contextRelative) throws IOException {
568         issueRedirect(request, response, url, queryParams, contextRelative, true);
569     }
570 
571     /**
572      * <p>Checks to see if a request param is considered true using a loose matching strategy for
573      * general values that indicate that something is true or enabled, etc.</p>
574      * <p/>
575      * <p>Values that are considered "true" include (case-insensitive): true, t, 1, enabled, y, yes, on.</p>
576      *
577      * @param request   the servlet request
578      * @param paramName @return true if the param value is considered true or false if it isn't.
579      * @return true if the given parameter is considered "true" - false otherwise.
580      */
581     public static boolean isTrue(ServletRequest request, String paramName) {
582         String value = getCleanParam(request, paramName);
583         return value != null &&
584                 (value.equalsIgnoreCase("true") ||
585                         value.equalsIgnoreCase("t") ||
586                         value.equalsIgnoreCase("1") ||
587                         value.equalsIgnoreCase("enabled") ||
588                         value.equalsIgnoreCase("y") ||
589                         value.equalsIgnoreCase("yes") ||
590                         value.equalsIgnoreCase("on"));
591     }
592 
593     /**
594      * Convenience method that returns a request parameter value, first running it through
595      * {@link StringUtils#clean(String)}.
596      *
597      * @param request   the servlet request.
598      * @param paramName the parameter name.
599      * @return the clean param value, or null if the param does not exist or is empty.
600      */
601     public static String getCleanParam(ServletRequest request, String paramName) {
602         return StringUtils.clean(request.getParameter(paramName));
603     }
604 
605     public static void saveRequest(ServletRequest request) {
606         Subject subject = SecurityUtils.getSubject();
607         Session session = subject.getSession();
608         HttpServletRequest httpRequest = toHttp(request);
609         SavedRequest savedRequest = new SavedRequest(httpRequest);
610         session.setAttribute(SAVED_REQUEST_KEY, savedRequest);
611     }
612 
613     public static SavedRequest getAndClearSavedRequest(ServletRequest request) {
614         SavedRequest savedRequest = getSavedRequest(request);
615         if (savedRequest != null) {
616             Subject subject = SecurityUtils.getSubject();
617             Session session = subject.getSession();
618             session.removeAttribute(SAVED_REQUEST_KEY);
619         }
620         return savedRequest;
621     }
622 
623     public static SavedRequest getSavedRequest(ServletRequest request) {
624         SavedRequest savedRequest = null;
625         Subject subject = SecurityUtils.getSubject();
626         Session session = subject.getSession(false);
627         if (session != null) {
628             savedRequest = (SavedRequest) session.getAttribute(SAVED_REQUEST_KEY);
629         }
630         return savedRequest;
631     }
632 
633     /**
634      * Redirects the to the request url from a previously
635      * {@link #saveRequest(javax.servlet.ServletRequest) saved} request, or if there is no saved request, redirects the
636      * end user to the specified {@code fallbackUrl}.  If there is no saved request or fallback url, this method
637      * throws an {@link IllegalStateException}.
638      * <p/>
639      * This method is primarily used to support a common login scenario - if an unauthenticated user accesses a
640      * page that requires authentication, it is expected that request is
641      * {@link #saveRequest(javax.servlet.ServletRequest) saved} first and then redirected to the login page. Then,
642      * after a successful login, this method can be called to redirect them back to their originally requested URL, a
643      * nice usability feature.
644      *
645      * @param request     the incoming request
646      * @param response    the outgoing response
647      * @param fallbackUrl the fallback url to redirect to if there is no saved request available.
648      * @throws IllegalStateException if there is no saved request and the {@code fallbackUrl} is {@code null}.
649      * @throws IOException           if there is an error redirecting
650      * @since 1.0
651      */
652     public static void redirectToSavedRequest(ServletRequest request, ServletResponse response, String fallbackUrl)
653             throws IOException {
654         String successUrl = null;
655         boolean contextRelative = true;
656         SavedRequest savedRequest = WebUtils.getAndClearSavedRequest(request);
657         if (savedRequest != null && savedRequest.getMethod().equalsIgnoreCase(AccessControlFilter.GET_METHOD)) {
658             successUrl = savedRequest.getRequestUrl();
659             contextRelative = false;
660         }
661 
662         if (successUrl == null) {
663             successUrl = fallbackUrl;
664         }
665 
666         if (successUrl == null) {
667             throw new IllegalStateException("Success URL not available via saved request or via the " +
668                     "successUrlFallback method parameter. One of these must be non-null for " +
669                     "issueSuccessRedirect() to work.");
670         }
671 
672         WebUtils.issueRedirect(request, response, successUrl, null, contextRelative);
673     }
674 
675 }