Преглед изворни кода

Adds cookie based RequestCache
fixes spring-projectsgh-8034

Zeeshan Adnan пре 5 година
родитељ
комит
9708a2d63f

+ 131 - 0
web/src/main/java/org/springframework/security/web/savedrequest/CookieRequestCache.java

@@ -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;
+	}
+
+}

+ 168 - 0
web/src/test/java/org/springframework/security/web/savedrequest/CookieRequestCacheTests.java

@@ -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()));
+	}
+
+}

+ 35 - 2
web/src/test/java/org/springframework/security/web/savedrequest/RequestCacheAwareFilterTests.java

@@ -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();
+	}
 }