Adds cookie based RequestCache

fixes spring-projectsgh-8034
This commit is contained in:
Zeeshan Adnan 2020-06-04 21:28:28 +06:00 committed by Eleftheria Stein-Kousathana
parent d3dc8b055b
commit 9708a2d63f
3 changed files with 334 additions and 2 deletions

View File

@ -0,0 +1,131 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.savedrequest;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.WebUtils;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Base64;
/**
* An Implementation of {@code RequestCache} which saves the original request URI in a cookie.
*
* @author Zeeshan Adnan
*/
public class CookieRequestCache implements RequestCache {
private RequestMatcher requestMatcher = AnyRequestMatcher.INSTANCE;
protected final Log logger = LogFactory.getLog(this.getClass());
private static final String COOKIE_NAME = "REDIRECT_URI";
private static final int COOKIE_MAX_AGE = -1;
@Override
public void saveRequest(HttpServletRequest request, HttpServletResponse response) {
if (requestMatcher.matches(request)) {
String redirectUrl = UrlUtils.buildFullRequestUrl(request);
Cookie savedCookie = new Cookie(COOKIE_NAME, encodeCookie(redirectUrl));
savedCookie.setMaxAge(COOKIE_MAX_AGE);
savedCookie.setSecure(request.isSecure());
savedCookie.setPath(request.getContextPath());
savedCookie.setHttpOnly(true);
response.addCookie(savedCookie);
} else {
logger.debug("Request not saved as configured RequestMatcher did not match");
}
}
@Override
public SavedRequest getRequest(HttpServletRequest request, HttpServletResponse response) {
Cookie savedRequestCookie = WebUtils.getCookie(request, COOKIE_NAME);
if (savedRequestCookie != null) {
String originalURI = decodeCookie(savedRequestCookie.getValue());
UriComponents uriComponents = UriComponentsBuilder.fromUriString(originalURI).build();
DefaultSavedRequest.Builder builder = new DefaultSavedRequest.Builder();
int port = uriComponents.getPort();
if (port == -1) {
if ("https".equalsIgnoreCase(uriComponents.getScheme())) {
port = 443;
} else {
port = 80;
}
}
return builder.setScheme(uriComponents.getScheme())
.setServerName(uriComponents.getHost())
.setRequestURI(uriComponents.getPath())
.setQueryString(uriComponents.getQuery())
.setServerPort(port)
.build();
}
return null;
}
@Override
public HttpServletRequest getMatchingRequest(HttpServletRequest request, HttpServletResponse response) {
SavedRequest savedRequest = getRequest(request, response);
if (savedRequest != null) {
removeRequest(request, response);
return new SavedRequestAwareWrapper(savedRequest, request);
}
return null;
}
@Override
public void removeRequest(HttpServletRequest request, HttpServletResponse response) {
Cookie removeSavedRequestCookie = new Cookie(COOKIE_NAME, "");
removeSavedRequestCookie.setSecure(request.isSecure());
removeSavedRequestCookie.setHttpOnly(true);
removeSavedRequestCookie.setPath(request.getContextPath());
removeSavedRequestCookie.setMaxAge(0);
response.addCookie(removeSavedRequestCookie);
}
private static String encodeCookie(String cookieValue) {
return Base64.getEncoder().encodeToString(cookieValue.getBytes());
}
private static String decodeCookie(String encodedCookieValue) {
return new String(Base64.getDecoder().decode(encodedCookieValue.getBytes()));
}
/**
* Allows selective use of saved requests for a subset of requests. By default any
* request will be cached by the {@code saveRequest} method.
* <p>
* If set, only matching requests will be cached.
*
* @param requestMatcher a request matching strategy which defines which requests
* should be cached.
*/
public void setRequestMatcher(RequestMatcher requestMatcher) {
Assert.notNull(requestMatcher, "requestMatcher should not be null");
this.requestMatcher = requestMatcher;
}
}

View File

@ -0,0 +1,168 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.savedrequest;
import org.junit.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import java.util.Base64;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* @author Zeeshan Adnan
*/
public class CookieRequestCacheTests {
private static final String DEFAULT_COOKIE_NAME = "REDIRECT_URI";
@Test
public void saveRequestWhenMatchesThenSavedRequestInACookieOnResponse() {
CookieRequestCache cookieRequestCache = new CookieRequestCache();
MockHttpServletRequest request = requestToSave();
MockHttpServletResponse response = new MockHttpServletResponse();
cookieRequestCache.saveRequest(request, response);
Cookie savedCookie = response.getCookie(DEFAULT_COOKIE_NAME);
assertThat(savedCookie).isNotNull();
String redirectUrl = decodeCookie(savedCookie.getValue());
assertThat(redirectUrl).isEqualTo("https://abc.com/destination?param1=a&param2=b&param3=1122");
assertThat(savedCookie.getMaxAge()).isEqualTo(-1);
assertThat(savedCookie.getPath()).isEqualTo(request.getContextPath());
assertThat(savedCookie.isHttpOnly()).isTrue();
assertThat(savedCookie.getSecure()).isTrue();
}
@Test
public void setRequestMatcherWhenRequestMatcherIsSetNullThenThrowsIllegalArgumentException() {
CookieRequestCache cookieRequestCache = new CookieRequestCache();
assertThatThrownBy(() -> cookieRequestCache.setRequestMatcher(null))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
public void getMatchingRequestWhenRequestMatcherDefinedThenReturnsCorrectSubsetOfCachedRequests() {
CookieRequestCache cookieRequestCache = new CookieRequestCache();
cookieRequestCache.setRequestMatcher(request -> request.getRequestURI().equals("/expected-destination"));
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/destination");
MockHttpServletResponse response = new MockHttpServletResponse();
cookieRequestCache.saveRequest(request, response);
SavedRequest savedRequest = cookieRequestCache.getRequest(request, response);
assertThat(savedRequest).isNull();
HttpServletRequest matchingRequest = cookieRequestCache.getMatchingRequest(request, response);
assertThat(matchingRequest).isNull();
}
@Test
public void getRequestWhenRequestIsWithoutCookiesThenReturnsNullSavedRequest() {
CookieRequestCache cookieRequestCache = new CookieRequestCache();
SavedRequest savedRequest = cookieRequestCache.getRequest(new MockHttpServletRequest(), new MockHttpServletResponse());
assertThat(savedRequest).isNull();
}
@Test
public void getRequestWhenRequestDoesNotContainSavedRequestCookieThenReturnsNull() {
CookieRequestCache cookieRequestCache = new CookieRequestCache();
MockHttpServletRequest request = new MockHttpServletRequest();
request.setCookies(new Cookie("abc_cookie", "value"));
SavedRequest savedRequest = cookieRequestCache.getRequest(request, new MockHttpServletResponse());
assertThat(savedRequest).isNull();
}
@Test
public void getRequestWhenRequestContainsSavedRequestCookieThenReturnsSaveRequest() {
CookieRequestCache cookieRequestCache = new CookieRequestCache();
MockHttpServletRequest request = new MockHttpServletRequest();
String redirectUrl = "https://abc.com/destination?param1=a&param2=b&param3=1122";
request.setCookies(new Cookie(DEFAULT_COOKIE_NAME, encodeCookie(redirectUrl)));
SavedRequest savedRequest = cookieRequestCache.getRequest(request, new MockHttpServletResponse());
assertThat(savedRequest).isNotNull();
assertThat(savedRequest.getRedirectUrl()).isEqualTo(redirectUrl);
}
@Test
public void matchingRequestWhenRequestDoesNotContainSavedRequestCookieThenReturnsNull() {
CookieRequestCache cookieRequestCache = new CookieRequestCache();
MockHttpServletResponse response = new MockHttpServletResponse();
HttpServletRequest matchingRequest = cookieRequestCache.getMatchingRequest(new MockHttpServletRequest(), response);
assertThat(matchingRequest).isNull();
assertThat(response.getCookie(DEFAULT_COOKIE_NAME)).isNull();
}
@Test
public void matchingRequestWhenRequestContainsSavedRequestCookieThenSetsAnExpiredCookieInResponse() {
CookieRequestCache cookieRequestCache = new CookieRequestCache();
MockHttpServletRequest request = new MockHttpServletRequest();
String redirectUrl = "https://abc.com/destination?param1=a&param2=b&param3=1122";
request.setCookies(new Cookie(DEFAULT_COOKIE_NAME, encodeCookie(redirectUrl)));
MockHttpServletResponse response = new MockHttpServletResponse();
cookieRequestCache.getMatchingRequest(request, response);
Cookie expiredCookie = response.getCookie(DEFAULT_COOKIE_NAME);
assertThat(expiredCookie).isNotNull();
assertThat(expiredCookie.getValue()).isEmpty();
assertThat(expiredCookie.getMaxAge()).isZero();
}
@Test
public void removeRequestWhenInvokedThenSetsAnExpiredCookieOnResponse() {
CookieRequestCache cookieRequestCache = new CookieRequestCache();
MockHttpServletResponse response = new MockHttpServletResponse();
cookieRequestCache.removeRequest(new MockHttpServletRequest(), response);
Cookie expiredCookie = response.getCookie(DEFAULT_COOKIE_NAME);
assertThat(expiredCookie).isNotNull();
assertThat(expiredCookie.getValue()).isEmpty();
assertThat(expiredCookie.getMaxAge()).isZero();
}
private MockHttpServletRequest requestToSave() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setServerPort(443);
request.setSecure(true);
request.setScheme("https");
request.setServerName("abc.com");
request.setRequestURI("/destination");
request.setQueryString("param1=a&param2=b&param3=1122");
return request;
}
private static String encodeCookie(String cookieValue) {
return Base64.getEncoder().encodeToString(cookieValue.getBytes());
}
private static String decodeCookie(String encodedCookieValue) {
return new String(Base64.getDecoder().decode(encodedCookieValue.getBytes()));
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -22,10 +22,13 @@ import org.springframework.mock.web.MockFilterChain;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import javax.servlet.http.Cookie;
import java.util.Base64;
public class RequestCacheAwareFilterTests {
@Test
public void savedRequestIsRemovedAfterMatch() throws Exception {
public void doFilterWhenHttpSessionRequestCacheConfiguredThenSavedRequestRemovedAfterMatch() throws Exception {
RequestCacheAwareFilter filter = new RequestCacheAwareFilter();
HttpSessionRequestCache cache = new HttpSessionRequestCache();
@ -40,4 +43,34 @@ public class RequestCacheAwareFilterTests {
assertThat(request.getSession().getAttribute(
HttpSessionRequestCache.SAVED_REQUEST)).isNull();
}
@Test
public void doFilterWhenCookieRequestCacheConfiguredThenExpiredSavedRequestCookieSetAfterMatch() throws Exception {
CookieRequestCache cache = new CookieRequestCache();
RequestCacheAwareFilter filter = new RequestCacheAwareFilter(cache);
MockHttpServletRequest request = new MockHttpServletRequest();
request.setServerName("abc.com");
request.setRequestURI("/destination");
request.setScheme("https");
request.setServerPort(443);
request.setSecure(true);
String encodedRedirectUrl = Base64.getEncoder().encodeToString("https://abc.com/destination".getBytes());
Cookie savedRequest = new Cookie("REDIRECT_URI", encodedRedirectUrl);
savedRequest.setMaxAge(-1);
savedRequest.setSecure(request.isSecure());
savedRequest.setPath("/");
savedRequest.setHttpOnly(true);
request.setCookies(savedRequest);
MockHttpServletResponse response = new MockHttpServletResponse();
filter.doFilter(request, response, new MockFilterChain());
Cookie expiredCookie = response.getCookie("REDIRECT_URI");
assertThat(expiredCookie).isNotNull();
assertThat(expiredCookie.getValue()).isEmpty();
assertThat(expiredCookie.getMaxAge()).isZero();
}
}