Browse Source

Add Request-based AuthenticationManagerResolvers

Closes gh-6762
Josh Cummings 6 năm trước cách đây
mục cha
commit
08821369a3

+ 146 - 0
web/src/main/java/org/springframework/security/web/authentication/RequestMatcherDelegatingAuthenticationManagerResolver.java

@@ -0,0 +1,146 @@
+/*
+ * Copyright 2002-2022 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.authentication;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.AuthenticationManagerResolver;
+import org.springframework.security.authentication.AuthenticationServiceException;
+import org.springframework.security.authorization.AuthorizationManager;
+import org.springframework.security.web.access.intercept.RequestMatcherDelegatingAuthorizationManager;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.security.web.util.matcher.RequestMatcherEntry;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link AuthenticationManagerResolver} that returns a {@link AuthenticationManager}
+ * instances based upon the type of {@link HttpServletRequest} passed into
+ * {@link #resolve(HttpServletRequest)}.
+ *
+ * @author Josh Cummings
+ * @since 5.7
+ */
+public final class RequestMatcherDelegatingAuthenticationManagerResolver
+		implements AuthenticationManagerResolver<HttpServletRequest> {
+
+	private final List<RequestMatcherEntry<AuthenticationManager>> authenticationManagers;
+
+	private AuthenticationManager defaultAuthenticationManager = (authentication) -> {
+		throw new AuthenticationServiceException("Cannot authenticate " + authentication);
+	};
+
+	/**
+	 * Construct an {@link RequestMatcherDelegatingAuthenticationManagerResolver} based on
+	 * the provided parameters
+	 * @param authenticationManagers a {@link Map} of
+	 * {@link RequestMatcher}/{@link AuthenticationManager} pairs
+	 */
+	RequestMatcherDelegatingAuthenticationManagerResolver(
+			RequestMatcherEntry<AuthenticationManager>... authenticationManagers) {
+		Assert.notEmpty(authenticationManagers, "authenticationManagers cannot be empty");
+		this.authenticationManagers = Arrays.asList(authenticationManagers);
+	}
+
+	/**
+	 * Construct an {@link RequestMatcherDelegatingAuthenticationManagerResolver} based on
+	 * the provided parameters
+	 * @param authenticationManagers a {@link Map} of
+	 * {@link RequestMatcher}/{@link AuthenticationManager} pairs
+	 */
+	RequestMatcherDelegatingAuthenticationManagerResolver(
+			List<RequestMatcherEntry<AuthenticationManager>> authenticationManagers) {
+		Assert.notEmpty(authenticationManagers, "authenticationManagers cannot be empty");
+		this.authenticationManagers = authenticationManagers;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public AuthenticationManager resolve(HttpServletRequest context) {
+		for (RequestMatcherEntry<AuthenticationManager> entry : this.authenticationManagers) {
+			if (entry.getRequestMatcher().matches(context)) {
+				return entry.getEntry();
+			}
+		}
+
+		return this.defaultAuthenticationManager;
+	}
+
+	/**
+	 * Set the default {@link AuthenticationManager} to use when a request does not match
+	 * @param defaultAuthenticationManager the default {@link AuthenticationManager} to
+	 * use
+	 */
+	public void setDefaultAuthenticationManager(AuthenticationManager defaultAuthenticationManager) {
+		Assert.notNull(defaultAuthenticationManager, "defaultAuthenticationManager cannot be null");
+		this.defaultAuthenticationManager = defaultAuthenticationManager;
+	}
+
+	/**
+	 * Creates a builder for {@link RequestMatcherDelegatingAuthorizationManager}.
+	 * @return the new {@link RequestMatcherDelegatingAuthorizationManager.Builder}
+	 * instance
+	 */
+	public static Builder builder() {
+		return new Builder();
+	}
+
+	/**
+	 * A builder for {@link RequestMatcherDelegatingAuthenticationManagerResolver}.
+	 */
+	public static final class Builder {
+
+		private final List<RequestMatcherEntry<AuthenticationManager>> entries = new ArrayList<>();
+
+		private Builder() {
+
+		}
+
+		/**
+		 * Maps a {@link RequestMatcher} to an {@link AuthorizationManager}.
+		 * @param matcher the {@link RequestMatcher} to use
+		 * @param manager the {@link AuthenticationManager} to use
+		 * @return the {@link Builder} for further
+		 * customizationServerWebExchangeDelegatingReactiveAuthenticationManagerResolvers
+		 */
+		public Builder add(RequestMatcher matcher, AuthenticationManager manager) {
+			Assert.notNull(matcher, "matcher cannot be null");
+			Assert.notNull(manager, "manager cannot be null");
+			this.entries.add(new RequestMatcherEntry<>(matcher, manager));
+			return this;
+		}
+
+		/**
+		 * Creates a {@link RequestMatcherDelegatingAuthenticationManagerResolver}
+		 * instance.
+		 * @return the {@link RequestMatcherDelegatingAuthenticationManagerResolver}
+		 * instance
+		 */
+		public RequestMatcherDelegatingAuthenticationManagerResolver build() {
+			return new RequestMatcherDelegatingAuthenticationManagerResolver(this.entries);
+		}
+
+	}
+
+}

+ 151 - 0
web/src/main/java/org/springframework/security/web/server/authentication/ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver.java

@@ -0,0 +1,151 @@
+/*
+ * Copyright 2002-2022 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.server.authentication;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import org.springframework.security.authentication.AuthenticationServiceException;
+import org.springframework.security.authentication.ReactiveAuthenticationManager;
+import org.springframework.security.authentication.ReactiveAuthenticationManagerResolver;
+import org.springframework.security.web.access.intercept.RequestMatcherDelegatingAuthorizationManager;
+import org.springframework.security.web.authentication.RequestMatcherDelegatingAuthenticationManagerResolver;
+import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
+import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcherEntry;
+import org.springframework.util.Assert;
+import org.springframework.web.server.ServerWebExchange;
+
+/**
+ * A {@link ReactiveAuthenticationManagerResolver} that returns a
+ * {@link ReactiveAuthenticationManager} instances based upon the type of
+ * {@link ServerWebExchange} passed into {@link #resolve(ServerWebExchange)}.
+ *
+ * @author Josh Cummings
+ * @since 5.7
+ *
+ */
+public final class ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver
+		implements ReactiveAuthenticationManagerResolver<ServerWebExchange> {
+
+	private final List<ServerWebExchangeMatcherEntry<ReactiveAuthenticationManager>> authenticationManagers;
+
+	private ReactiveAuthenticationManager defaultAuthenticationManager = (authentication) -> Mono
+			.error(new AuthenticationServiceException("Cannot authenticate " + authentication));
+
+	/**
+	 * Construct an
+	 * {@link ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver} based on
+	 * the provided parameters
+	 * @param managers a set of {@link ServerWebExchangeMatcherEntry}s
+	 */
+	ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver(
+			ServerWebExchangeMatcherEntry<ReactiveAuthenticationManager>... managers) {
+		this(Arrays.asList(managers));
+	}
+
+	/**
+	 * Construct an
+	 * {@link ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver} based on
+	 * the provided parameters
+	 * @param managers a {@link List} of {@link ServerWebExchangeMatcherEntry}s
+	 */
+	ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver(
+			List<ServerWebExchangeMatcherEntry<ReactiveAuthenticationManager>> managers) {
+		Assert.notNull(managers, "entries cannot be null");
+		this.authenticationManagers = managers;
+	}
+
+	/**
+	 * {@inheritDoc}
+	 */
+	@Override
+	public Mono<ReactiveAuthenticationManager> resolve(ServerWebExchange exchange) {
+		return Flux.fromIterable(this.authenticationManagers).filterWhen((entry) -> isMatch(exchange, entry)).next()
+				.map(ServerWebExchangeMatcherEntry::getEntry).defaultIfEmpty(this.defaultAuthenticationManager);
+	}
+
+	/**
+	 * Set the default {@link ReactiveAuthenticationManager} to use when a request does
+	 * not match
+	 * @param defaultAuthenticationManager the default
+	 * {@link ReactiveAuthenticationManager} to use
+	 */
+	public void setDefaultAuthenticationManager(ReactiveAuthenticationManager defaultAuthenticationManager) {
+		Assert.notNull(defaultAuthenticationManager, "defaultAuthenticationManager cannot be null");
+		this.defaultAuthenticationManager = defaultAuthenticationManager;
+	}
+
+	/**
+	 * Creates a builder for {@link RequestMatcherDelegatingAuthorizationManager}.
+	 * @return the new {@link RequestMatcherDelegatingAuthorizationManager.Builder}
+	 * instance
+	 */
+	public static Builder builder() {
+		return new Builder();
+	}
+
+	private Mono<Boolean> isMatch(ServerWebExchange exchange,
+			ServerWebExchangeMatcherEntry<ReactiveAuthenticationManager> entry) {
+		ServerWebExchangeMatcher matcher = entry.getMatcher();
+		return matcher.matches(exchange).map(ServerWebExchangeMatcher.MatchResult::isMatch);
+	}
+
+	/**
+	 * A builder for {@link RequestMatcherDelegatingAuthenticationManagerResolver}.
+	 */
+	public static final class Builder {
+
+		private final List<ServerWebExchangeMatcherEntry<ReactiveAuthenticationManager>> entries = new ArrayList<>();
+
+		private Builder() {
+
+		}
+
+		/**
+		 * Maps a {@link ServerWebExchangeMatcher} to an
+		 * {@link ReactiveAuthenticationManager}.
+		 * @param matcher the {@link ServerWebExchangeMatcher} to use
+		 * @param manager the {@link ReactiveAuthenticationManager} to use
+		 * @return the
+		 * {@link RequestMatcherDelegatingAuthenticationManagerResolver.Builder} for
+		 * further customizations
+		 */
+		public ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver.Builder add(
+				ServerWebExchangeMatcher matcher, ReactiveAuthenticationManager manager) {
+			Assert.notNull(matcher, "matcher cannot be null");
+			Assert.notNull(manager, "manager cannot be null");
+			this.entries.add(new ServerWebExchangeMatcherEntry<>(matcher, manager));
+			return this;
+		}
+
+		/**
+		 * Creates a {@link RequestMatcherDelegatingAuthenticationManagerResolver}
+		 * instance.
+		 * @return the {@link RequestMatcherDelegatingAuthenticationManagerResolver}
+		 * instance
+		 */
+		public ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver build() {
+			return new ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver(this.entries);
+		}
+
+	}
+
+}

+ 68 - 0
web/src/test/java/org/springframework/security/web/authentication/RequestMatcherDelegatingAuthenticationManagerResolverTests.java

@@ -0,0 +1,68 @@
+/*
+ * Copyright 2002-2022 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.authentication;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.AuthenticationServiceException;
+import org.springframework.security.authentication.TestingAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.mockito.Mockito.mock;
+
+/**
+ * Tests for {@link RequestMatcherDelegatingAuthenticationManagerResolverTests}
+ *
+ * @author Josh Cummings
+ */
+public class RequestMatcherDelegatingAuthenticationManagerResolverTests {
+
+	private AuthenticationManager one = mock(AuthenticationManager.class);
+
+	private AuthenticationManager two = mock(AuthenticationManager.class);
+
+	@Test
+	public void resolveWhenMatchesThenReturnsAuthenticationManager() {
+		RequestMatcherDelegatingAuthenticationManagerResolver resolver = RequestMatcherDelegatingAuthenticationManagerResolver
+				.builder().add(new AntPathRequestMatcher("/one/**"), this.one)
+				.add(new AntPathRequestMatcher("/two/**"), this.two).build();
+
+		MockHttpServletRequest request = new MockHttpServletRequest("GET", "/one/location");
+		request.setServletPath("/one/location");
+		assertThat(resolver.resolve(request)).isEqualTo(this.one);
+	}
+
+	@Test
+	public void resolveWhenDoesNotMatchThenReturnsDefaultAuthenticationManager() {
+		RequestMatcherDelegatingAuthenticationManagerResolver resolver = RequestMatcherDelegatingAuthenticationManagerResolver
+				.builder().add(new AntPathRequestMatcher("/one/**"), this.one)
+				.add(new AntPathRequestMatcher("/two/**"), this.two).build();
+
+		MockHttpServletRequest request = new MockHttpServletRequest("GET", "/wrong/location");
+		AuthenticationManager authenticationManager = resolver.resolve(request);
+
+		Authentication authentication = new TestingAuthenticationToken("principal", "creds");
+		assertThatExceptionOfType(AuthenticationServiceException.class)
+				.isThrownBy(() -> authenticationManager.authenticate(authentication));
+	}
+
+}

+ 69 - 0
web/src/test/java/org/springframework/security/web/server/authentication/ServerWebExchangeDelegatingReactiveAuthenticationManagerResolverTests.java

@@ -0,0 +1,69 @@
+/*
+ * Copyright 2002-2022 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.server.authentication;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
+import org.springframework.mock.web.server.MockServerWebExchange;
+import org.springframework.security.authentication.AuthenticationServiceException;
+import org.springframework.security.authentication.ReactiveAuthenticationManager;
+import org.springframework.security.authentication.TestingAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.mockito.Mockito.mock;
+
+/**
+ * Tests for {@link ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver}
+ *
+ * @author Josh Cummings
+ */
+public class ServerWebExchangeDelegatingReactiveAuthenticationManagerResolverTests {
+
+	private ReactiveAuthenticationManager one = mock(ReactiveAuthenticationManager.class);
+
+	private ReactiveAuthenticationManager two = mock(ReactiveAuthenticationManager.class);
+
+	@Test
+	public void resolveWhenMatchesThenReturnsReactiveAuthenticationManager() {
+		ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver resolver = ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver
+				.builder().add(new PathPatternParserServerWebExchangeMatcher("/one/**"), this.one)
+				.add(new PathPatternParserServerWebExchangeMatcher("/two/**"), this.two).build();
+
+		MockServerHttpRequest request = MockServerHttpRequest.get("/one/location").build();
+		assertThat(resolver.resolve(MockServerWebExchange.from(request)).block()).isEqualTo(this.one);
+	}
+
+	@Test
+	public void resolveWhenDoesNotMatchThenReturnsDefaultReactiveAuthenticationManager() {
+		ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver resolver = ServerWebExchangeDelegatingReactiveAuthenticationManagerResolver
+				.builder().add(new PathPatternParserServerWebExchangeMatcher("/one/**"), this.one)
+				.add(new PathPatternParserServerWebExchangeMatcher("/two/**"), this.two).build();
+
+		MockServerHttpRequest request = MockServerHttpRequest.get("/wrong/location").build();
+		ReactiveAuthenticationManager authenticationManager = resolver.resolve(MockServerWebExchange.from(request))
+				.block();
+
+		Authentication authentication = new TestingAuthenticationToken("principal", "creds");
+		assertThatExceptionOfType(AuthenticationServiceException.class)
+				.isThrownBy(() -> authenticationManager.authenticate(authentication).block());
+	}
+
+}