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        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}