001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one
003     * or more contributor license agreements.  See the NOTICE file
004     * distributed with this work for additional information
005     * regarding copyright ownership.  The ASF licenses this file
006     * to you under the Apache License, Version 2.0 (the
007     * "License"); you may not use this file except in compliance
008     * with the License.  You may obtain a copy of the License at
009     *
010     *     http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing,
013     * software distributed under the License is distributed on an
014     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015     * KIND, either express or implied.  See the License for the
016     * specific language governing permissions and limitations
017     * under the License.
018     */
019    package org.apache.shiro.web.util;
020    
021    import org.apache.shiro.SecurityUtils;
022    import org.apache.shiro.session.Session;
023    import org.apache.shiro.subject.Subject;
024    import org.apache.shiro.subject.support.DefaultSubjectContext;
025    import org.apache.shiro.util.StringUtils;
026    import org.apache.shiro.web.env.EnvironmentLoader;
027    import org.apache.shiro.web.env.WebEnvironment;
028    import org.apache.shiro.web.filter.AccessControlFilter;
029    import org.slf4j.Logger;
030    import org.slf4j.LoggerFactory;
031    
032    import javax.servlet.ServletContext;
033    import javax.servlet.ServletRequest;
034    import javax.servlet.ServletResponse;
035    import javax.servlet.http.HttpServletRequest;
036    import javax.servlet.http.HttpServletResponse;
037    import java.io.IOException;
038    import java.io.UnsupportedEncodingException;
039    import java.net.URLDecoder;
040    import java.util.Map;
041    
042    /**
043     * Simple utility class for operations used across multiple class hierarchies in the web framework code.
044     * <p/>
045     * Some methods in this class were copied from the Spring Framework so we didn't have to re-invent the wheel,
046     * and in these cases, we have retained all license, copyright and author information.
047     *
048     * @since 0.9
049     */
050    public class WebUtils {
051    
052        //TODO - complete JavaDoc
053    
054        private static final Logger log = LoggerFactory.getLogger(WebUtils.class);
055    
056        public static final String SERVLET_REQUEST_KEY = ServletRequest.class.getName() + "_SHIRO_THREAD_CONTEXT_KEY";
057        public static final String SERVLET_RESPONSE_KEY = ServletResponse.class.getName() + "_SHIRO_THREAD_CONTEXT_KEY";
058    
059        /**
060         * {@link org.apache.shiro.session.Session Session} key used to save a request and later restore it, for example when redirecting to a
061         * requested page after login, equal to {@code shiroSavedRequest}.
062         */
063        public static final String SAVED_REQUEST_KEY = "shiroSavedRequest";
064    
065        /**
066         * Standard Servlet 2.3+ spec request attributes for include URI and paths.
067         * <p>If included via a RequestDispatcher, the current resource will see the
068         * originating request. Its own URI and paths are exposed as request attributes.
069         */
070        public static final String INCLUDE_REQUEST_URI_ATTRIBUTE = "javax.servlet.include.request_uri";
071        public static final String INCLUDE_CONTEXT_PATH_ATTRIBUTE = "javax.servlet.include.context_path";
072        public static final String INCLUDE_SERVLET_PATH_ATTRIBUTE = "javax.servlet.include.servlet_path";
073        public static final String INCLUDE_PATH_INFO_ATTRIBUTE = "javax.servlet.include.path_info";
074        public static final String INCLUDE_QUERY_STRING_ATTRIBUTE = "javax.servlet.include.query_string";
075    
076        /**
077         * Standard Servlet 2.4+ spec request attributes for forward URI and paths.
078         * <p>If forwarded to via a RequestDispatcher, the current resource will see its
079         * own URI and paths. The originating URI and paths are exposed as request attributes.
080         */
081        public static final String FORWARD_REQUEST_URI_ATTRIBUTE = "javax.servlet.forward.request_uri";
082        public static final String FORWARD_CONTEXT_PATH_ATTRIBUTE = "javax.servlet.forward.context_path";
083        public static final String FORWARD_SERVLET_PATH_ATTRIBUTE = "javax.servlet.forward.servlet_path";
084        public static final String FORWARD_PATH_INFO_ATTRIBUTE = "javax.servlet.forward.path_info";
085        public static final String FORWARD_QUERY_STRING_ATTRIBUTE = "javax.servlet.forward.query_string";
086    
087        /**
088         * Default character encoding to use when <code>request.getCharacterEncoding</code>
089         * returns <code>null</code>, according to the Servlet spec.
090         *
091         * @see javax.servlet.ServletRequest#getCharacterEncoding
092         */
093        public static final String DEFAULT_CHARACTER_ENCODING = "ISO-8859-1";
094    
095        /**
096         * Return the path within the web application for the given request.
097         * Detects include request URL if called within a RequestDispatcher include.
098         * <p/>
099         * 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            if ("/".equals(contextPath)) {
252                // Invalid case, but happens for includes on Jetty: silently adapt it.
253                contextPath = "";
254            }
255            return decodeRequestString(request, contextPath);
256        }
257    
258        /**
259         * Find the Shiro {@link WebEnvironment} for this web application, which is typically loaded via the
260         * {@link org.apache.shiro.web.env.EnvironmentLoaderListener}.
261         * <p/>
262         * This implementation rethrows an exception that happened on environment startup to differentiate between a failed
263         * environment startup and no environment at all.
264         *
265         * @param sc ServletContext to find the web application context for
266         * @return the root WebApplicationContext for this web app
267         * @throws IllegalStateException if the root WebApplicationContext could not be found
268         * @see org.apache.shiro.web.env.EnvironmentLoader#ENVIRONMENT_ATTRIBUTE_KEY
269         * @since 1.2
270         */
271        public static WebEnvironment getRequiredWebEnvironment(ServletContext sc)
272                throws IllegalStateException {
273    
274            WebEnvironment we = getWebEnvironment(sc);
275            if (we == null) {
276                throw new IllegalStateException("No WebEnvironment found: no EnvironmentLoaderListener registered?");
277            }
278            return we;
279        }
280    
281        /**
282         * Find the Shiro {@link WebEnvironment} for this web application, which is typically loaded via
283         * {@link org.apache.shiro.web.env.EnvironmentLoaderListener}.
284         * <p/>
285         * This implementation rethrows an exception that happened on environment startup to differentiate between a failed
286         * environment startup and no environment at all.
287         *
288         * @param sc ServletContext to find the web application context for
289         * @return the root WebApplicationContext for this web app, or <code>null</code> if none
290         * @see org.apache.shiro.web.env.EnvironmentLoader#ENVIRONMENT_ATTRIBUTE_KEY
291         * @since 1.2
292         */
293        public static WebEnvironment getWebEnvironment(ServletContext sc) {
294            return getWebEnvironment(sc, EnvironmentLoader.ENVIRONMENT_ATTRIBUTE_KEY);
295        }
296    
297        /**
298         * Find the Shiro {@link WebEnvironment} for this web application.
299         *
300         * @param sc       ServletContext to find the web application context for
301         * @param attrName the name of the ServletContext attribute to look for
302         * @return the desired WebEnvironment for this web app, or <code>null</code> if none
303         * @since 1.2
304         */
305        public static WebEnvironment getWebEnvironment(ServletContext sc, String attrName) {
306            if (sc == null) {
307                throw new IllegalArgumentException("ServletContext argument must not be null.");
308            }
309            Object attr = sc.getAttribute(attrName);
310            if (attr == null) {
311                return null;
312            }
313            if (attr instanceof RuntimeException) {
314                throw (RuntimeException) attr;
315            }
316            if (attr instanceof Error) {
317                throw (Error) attr;
318            }
319            if (attr instanceof Exception) {
320                throw new IllegalStateException((Exception) attr);
321            }
322            if (!(attr instanceof WebEnvironment)) {
323                throw new IllegalStateException("Context attribute is not of type WebEnvironment: " + attr);
324            }
325            return (WebEnvironment) attr;
326        }
327    
328    
329        /**
330         * Decode the given source string with a URLDecoder. The encoding will be taken
331         * from the request, falling back to the default "ISO-8859-1".
332         * <p>The default implementation uses <code>URLDecoder.decode(input, enc)</code>.
333         *
334         * @param request current HTTP request
335         * @param source  the String to decode
336         * @return the decoded String
337         * @see #DEFAULT_CHARACTER_ENCODING
338         * @see javax.servlet.ServletRequest#getCharacterEncoding
339         * @see java.net.URLDecoder#decode(String, String)
340         * @see java.net.URLDecoder#decode(String)
341         */
342        @SuppressWarnings({"deprecation"})
343        public static String decodeRequestString(HttpServletRequest request, String source) {
344            String enc = determineEncoding(request);
345            try {
346                return URLDecoder.decode(source, enc);
347            } catch (UnsupportedEncodingException ex) {
348                if (log.isWarnEnabled()) {
349                    log.warn("Could not decode request string [" + source + "] with encoding '" + enc +
350                            "': falling back to platform default encoding; exception message: " + ex.getMessage());
351                }
352                return URLDecoder.decode(source);
353            }
354        }
355    
356        /**
357         * Determine the encoding for the given request.
358         * Can be overridden in subclasses.
359         * <p>The default implementation checks the request's
360         * {@link ServletRequest#getCharacterEncoding() character encoding}, and if that
361         * <code>null</code>, falls back to the {@link #DEFAULT_CHARACTER_ENCODING}.
362         *
363         * @param request current HTTP request
364         * @return the encoding for the request (never <code>null</code>)
365         * @see javax.servlet.ServletRequest#getCharacterEncoding()
366         */
367        protected static String determineEncoding(HttpServletRequest request) {
368            String enc = request.getCharacterEncoding();
369            if (enc == null) {
370                enc = DEFAULT_CHARACTER_ENCODING;
371            }
372            return enc;
373        }
374    
375        /*
376         * Returns {@code true} IFF the specified {@code SubjectContext}:
377         * <ol>
378         * <li>A {@link WebSubjectContext} instance</li>
379         * <li>The {@code WebSubjectContext}'s request/response pair are not null</li>
380         * <li>The request is an {@link HttpServletRequest} instance</li>
381         * <li>The response is an {@link HttpServletResponse} instance</li>
382         * </ol>
383         *
384         * @param context the SubjectContext to check to see if it is HTTP compatible.
385         * @return {@code true} IFF the specified context has HTTP request/response objects, {@code false} otherwise.
386         * @since 1.0
387         */
388    
389        public static boolean isWeb(Object requestPairSource) {
390            return requestPairSource instanceof RequestPairSource && isWeb((RequestPairSource) requestPairSource);
391        }
392    
393        public static boolean isHttp(Object requestPairSource) {
394            return requestPairSource instanceof RequestPairSource && isHttp((RequestPairSource) requestPairSource);
395        }
396    
397        public static ServletRequest getRequest(Object requestPairSource) {
398            if (requestPairSource instanceof RequestPairSource) {
399                return ((RequestPairSource) requestPairSource).getServletRequest();
400            }
401            return null;
402        }
403    
404        public static ServletResponse getResponse(Object requestPairSource) {
405            if (requestPairSource instanceof RequestPairSource) {
406                return ((RequestPairSource) requestPairSource).getServletResponse();
407            }
408            return null;
409        }
410    
411        public static HttpServletRequest getHttpRequest(Object requestPairSource) {
412            ServletRequest request = getRequest(requestPairSource);
413            if (request instanceof HttpServletRequest) {
414                return (HttpServletRequest) request;
415            }
416            return null;
417        }
418    
419        public static HttpServletResponse getHttpResponse(Object requestPairSource) {
420            ServletResponse response = getResponse(requestPairSource);
421            if (response instanceof HttpServletResponse) {
422                return (HttpServletResponse) response;
423            }
424            return null;
425        }
426    
427        private static boolean isWeb(RequestPairSource source) {
428            ServletRequest request = source.getServletRequest();
429            ServletResponse response = source.getServletResponse();
430            return request != null && response != null;
431        }
432    
433        private static boolean isHttp(RequestPairSource source) {
434            ServletRequest request = source.getServletRequest();
435            ServletResponse response = source.getServletResponse();
436            return request instanceof HttpServletRequest && response instanceof HttpServletResponse;
437        }
438    
439        /**
440         * Returns {@code true} if a session is allowed to be created for a subject-associated request, {@code false}
441         * otherwise.
442         * <p/>
443         * <b>This method exists for Shiro's internal framework needs and should never be called by Shiro end-users.  It
444         * could be changed/removed at any time.</b>
445         *
446         * @param requestPairSource a {@link RequestPairSource} instance, almost always a
447         *                          {@link org.apache.shiro.web.subject.WebSubject WebSubject} instance.
448         * @return {@code true} if a session is allowed to be created for a subject-associated request, {@code false}
449         *         otherwise.
450         */
451        public static boolean _isSessionCreationEnabled(Object requestPairSource) {
452            if (requestPairSource instanceof RequestPairSource) {
453                RequestPairSource source = (RequestPairSource) requestPairSource;
454                return _isSessionCreationEnabled(source.getServletRequest());
455            }
456            return true; //by default
457        }
458    
459        /**
460         * Returns {@code true} if a session is allowed to be created for a subject-associated request, {@code false}
461         * otherwise.
462         * <p/>
463         * <b>This method exists for Shiro's internal framework needs and should never be called by Shiro end-users.  It
464         * could be changed/removed at any time.</b>
465         *
466         * @param request incoming servlet request.
467         * @return {@code true} if a session is allowed to be created for a subject-associated request, {@code false}
468         *         otherwise.
469         */
470        public static boolean _isSessionCreationEnabled(ServletRequest request) {
471            if (request != null) {
472                Object val = request.getAttribute(DefaultSubjectContext.SESSION_CREATION_ENABLED);
473                if (val != null && val instanceof Boolean) {
474                    return (Boolean) val;
475                }
476            }
477            return true; //by default
478        }
479    
480        /**
481         * A convenience method that merely casts the incoming <code>ServletRequest</code> to an
482         * <code>HttpServletRequest</code>:
483         * <p/>
484         * <code>return (HttpServletRequest)request;</code>
485         * <p/>
486         * Logic could be changed in the future for logging or throwing an meaningful exception in
487         * non HTTP request environments (e.g. Portlet API).
488         *
489         * @param request the incoming ServletRequest
490         * @return the <code>request</code> argument casted to an <code>HttpServletRequest</code>.
491         */
492        public static HttpServletRequest toHttp(ServletRequest request) {
493            return (HttpServletRequest) request;
494        }
495    
496        /**
497         * A convenience method that merely casts the incoming <code>ServletResponse</code> to an
498         * <code>HttpServletResponse</code>:
499         * <p/>
500         * <code>return (HttpServletResponse)response;</code>
501         * <p/>
502         * Logic could be changed in the future for logging or throwing an meaningful exception in
503         * non HTTP request environments (e.g. Portlet API).
504         *
505         * @param response the outgoing ServletResponse
506         * @return the <code>response</code> argument casted to an <code>HttpServletResponse</code>.
507         */
508        public static HttpServletResponse toHttp(ServletResponse response) {
509            return (HttpServletResponse) response;
510        }
511    
512        /**
513         * Redirects the current request to a new URL based on the given parameters.
514         *
515         * @param request          the servlet request.
516         * @param response         the servlet response.
517         * @param url              the URL to redirect the user to.
518         * @param queryParams      a map of parameters that should be set as request parameters for the new request.
519         * @param contextRelative  true if the URL is relative to the servlet context path, or false if the URL is absolute.
520         * @param http10Compatible whether to stay compatible with HTTP 1.0 clients.
521         * @throws java.io.IOException if thrown by response methods.
522         */
523        public static void issueRedirect(ServletRequest request, ServletResponse response, String url, Map queryParams, boolean contextRelative, boolean http10Compatible) throws IOException {
524            RedirectView view = new RedirectView(url, contextRelative, http10Compatible);
525            view.renderMergedOutputModel(queryParams, toHttp(request), toHttp(response));
526        }
527    
528        /**
529         * Redirects the current request to a new URL based on the given parameters and default values
530         * for unspecified parameters.
531         *
532         * @param request  the servlet request.
533         * @param response the servlet response.
534         * @param url      the URL to redirect the user to.
535         * @throws java.io.IOException if thrown by response methods.
536         */
537        public static void issueRedirect(ServletRequest request, ServletResponse response, String url) throws IOException {
538            issueRedirect(request, response, url, null, true, true);
539        }
540    
541        /**
542         * Redirects the current request to a new URL based on the given parameters and default values
543         * for unspecified parameters.
544         *
545         * @param request     the servlet request.
546         * @param response    the servlet response.
547         * @param url         the URL to redirect the user to.
548         * @param queryParams a map of parameters that should be set as request parameters for the new request.
549         * @throws java.io.IOException if thrown by response methods.
550         */
551        public static void issueRedirect(ServletRequest request, ServletResponse response, String url, Map queryParams) throws IOException {
552            issueRedirect(request, response, url, queryParams, true, true);
553        }
554    
555        /**
556         * Redirects the current request to a new URL based on the given parameters and default values
557         * for unspecified parameters.
558         *
559         * @param request         the servlet request.
560         * @param response        the servlet response.
561         * @param url             the URL to redirect the user to.
562         * @param queryParams     a map of parameters that should be set as request parameters for the new request.
563         * @param contextRelative true if the URL is relative to the servlet context path, or false if the URL is absolute.
564         * @throws java.io.IOException if thrown by response methods.
565         */
566        public static void issueRedirect(ServletRequest request, ServletResponse response, String url, Map queryParams, boolean contextRelative) throws IOException {
567            issueRedirect(request, response, url, queryParams, contextRelative, true);
568        }
569    
570        /**
571         * <p>Checks to see if a request param is considered true using a loose matching strategy for
572         * general values that indicate that something is true or enabled, etc.</p>
573         * <p/>
574         * <p>Values that are considered "true" include (case-insensitive): true, t, 1, enabled, y, yes, on.</p>
575         *
576         * @param request   the servlet request
577         * @param paramName @return true if the param value is considered true or false if it isn't.
578         * @return true if the given parameter is considered "true" - false otherwise.
579         */
580        public static boolean isTrue(ServletRequest request, String paramName) {
581            String value = getCleanParam(request, paramName);
582            return value != null &&
583                    (value.equalsIgnoreCase("true") ||
584                            value.equalsIgnoreCase("t") ||
585                            value.equalsIgnoreCase("1") ||
586                            value.equalsIgnoreCase("enabled") ||
587                            value.equalsIgnoreCase("y") ||
588                            value.equalsIgnoreCase("yes") ||
589                            value.equalsIgnoreCase("on"));
590        }
591    
592        /**
593         * Convenience method that returns a request parameter value, first running it through
594         * {@link StringUtils#clean(String)}.
595         *
596         * @param request   the servlet request.
597         * @param paramName the parameter name.
598         * @return the clean param value, or null if the param does not exist or is empty.
599         */
600        public static String getCleanParam(ServletRequest request, String paramName) {
601            return StringUtils.clean(request.getParameter(paramName));
602        }
603    
604        public static void saveRequest(ServletRequest request) {
605            Subject subject = SecurityUtils.getSubject();
606            Session session = subject.getSession();
607            HttpServletRequest httpRequest = toHttp(request);
608            SavedRequest savedRequest = new SavedRequest(httpRequest);
609            session.setAttribute(SAVED_REQUEST_KEY, savedRequest);
610        }
611    
612        public static SavedRequest getAndClearSavedRequest(ServletRequest request) {
613            SavedRequest savedRequest = getSavedRequest(request);
614            if (savedRequest != null) {
615                Subject subject = SecurityUtils.getSubject();
616                Session session = subject.getSession();
617                session.removeAttribute(SAVED_REQUEST_KEY);
618            }
619            return savedRequest;
620        }
621    
622        public static SavedRequest getSavedRequest(ServletRequest request) {
623            SavedRequest savedRequest = null;
624            Subject subject = SecurityUtils.getSubject();
625            Session session = subject.getSession(false);
626            if (session != null) {
627                savedRequest = (SavedRequest) session.getAttribute(SAVED_REQUEST_KEY);
628            }
629            return savedRequest;
630        }
631    
632        /**
633         * Redirects the to the request url from a previously
634         * {@link #saveRequest(javax.servlet.ServletRequest) saved} request, or if there is no saved request, redirects the
635         * end user to the specified {@code fallbackUrl}.  If there is no saved request or fallback url, this method
636         * throws an {@link IllegalStateException}.
637         * <p/>
638         * This method is primarily used to support a common login scenario - if an unauthenticated user accesses a
639         * page that requires authentication, it is expected that request is
640         * {@link #saveRequest(javax.servlet.ServletRequest) saved} first and then redirected to the login page. Then,
641         * after a successful login, this method can be called to redirect them back to their originally requested URL, a
642         * nice usability feature.
643         *
644         * @param request     the incoming request
645         * @param response    the outgoing response
646         * @param fallbackUrl the fallback url to redirect to if there is no saved request available.
647         * @throws IllegalStateException if there is no saved request and the {@code fallbackUrl} is {@code null}.
648         * @throws IOException           if there is an error redirecting
649         * @since 1.0
650         */
651        public static void redirectToSavedRequest(ServletRequest request, ServletResponse response, String fallbackUrl)
652                throws IOException {
653            String successUrl = null;
654            boolean contextRelative = true;
655            SavedRequest savedRequest = WebUtils.getAndClearSavedRequest(request);
656            if (savedRequest != null && savedRequest.getMethod().equalsIgnoreCase(AccessControlFilter.GET_METHOD)) {
657                successUrl = savedRequest.getRequestUrl();
658                contextRelative = false;
659            }
660    
661            if (successUrl == null) {
662                successUrl = fallbackUrl;
663            }
664    
665            if (successUrl == null) {
666                throw new IllegalStateException("Success URL not available via saved request or via the " +
667                        "successUrlFallback method parameter. One of these must be non-null for " +
668                        "issueSuccessRedirect() to work.");
669            }
670    
671            WebUtils.issueRedirect(request, response, successUrl, null, contextRelative);
672        }
673    
674    }