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 */
019package org.apache.shiro.web.util;
020
021import org.apache.shiro.SecurityUtils;
022import org.apache.shiro.session.Session;
023import org.apache.shiro.subject.Subject;
024import org.apache.shiro.subject.support.DefaultSubjectContext;
025import org.apache.shiro.util.StringUtils;
026import org.apache.shiro.web.env.EnvironmentLoader;
027import org.apache.shiro.web.env.WebEnvironment;
028import org.apache.shiro.web.filter.AccessControlFilter;
029import org.slf4j.Logger;
030import org.slf4j.LoggerFactory;
031
032import javax.servlet.ServletContext;
033import javax.servlet.ServletRequest;
034import javax.servlet.ServletResponse;
035import javax.servlet.http.HttpServletRequest;
036import javax.servlet.http.HttpServletResponse;
037import java.io.IOException;
038import java.io.UnsupportedEncodingException;
039import java.net.URLDecoder;
040import 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 */
050public 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        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}