浏览代码

Added support for Anonymous Authentication

1. Created new WebFilter AnonymousAuthenticationWebFilter to
for anonymous authentication
2. Created class AnonymousSpec, method anonymous to configure
anonymous authentication in ServerHttpSecurity
3. Added ANONYMOUS_AUTHENTICATION order after AUTHENTICATION for
anonymous authentication in SecurityWebFiltersOrder
4. Added tests for anonymous authentication in
AnonymousAuthenticationWebFilterTests and ServerHttpSecurityTests
5. Added support for Controller in WebTestClientBuilder

Fixes: gh-5934
Ankur Pathak 6 年之前
父节点
当前提交
2b369cfe98

+ 4 - 0
config/src/main/java/org/springframework/security/config/web/server/SecurityWebFiltersOrder.java

@@ -48,6 +48,10 @@ public enum SecurityWebFiltersOrder {
 	 */
 	FORM_LOGIN,
 	AUTHENTICATION,
+	/**
+	 * Instance of AnonymousAuthenticationWebFilter
+	 */
+	ANONYMOUS_AUTHENTICATION,
 	OAUTH2_AUTHORIZATION_CODE,
 	LOGIN_PAGE_GENERATING,
 	LOGOUT_PAGE_GENERATING,

+ 152 - 0
config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java

@@ -33,6 +33,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.function.Function;
+import java.util.UUID;
 
 import reactor.core.publisher.Mono;
 import reactor.util.context.Context;
@@ -158,6 +159,9 @@ import org.springframework.web.cors.reactive.DefaultCorsProcessor;
 import org.springframework.web.server.ServerWebExchange;
 import org.springframework.web.server.WebFilter;
 import org.springframework.web.server.WebFilterChain;
+import org.springframework.security.web.server.authentication.AnonymousAuthenticationWebFilter;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.AuthorityUtils;
 
 /**
  * A {@link ServerHttpSecurity} is similar to Spring Security's {@code HttpSecurity} but for WebFlux.
@@ -264,6 +268,8 @@ public class ServerHttpSecurity {
 
 	private Throwable built;
 
+	private AnonymousSpec anonymous;
+
 	/**
 	 * The ServerExchangeMatcher that determines which requests apply to this HttpSecurity instance.
 	 *
@@ -425,6 +431,29 @@ public class ServerHttpSecurity {
 		return this.cors;
 	}
 
+	/**
+	 * @since 5.2.0
+	 * @author Ankur Pathak
+	 * Enables and Configures annonymous authentication. Anonymous Authentication is disabled by default.
+	 *
+	 * <pre class="code">
+	 *  &#064;Bean
+	 *  public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
+	 *      http
+	 *          // ...
+	 *          .anonymous().key("key")
+	 *          .authorities("ROLE_ANONYMOUS");
+	 *      return http.build();
+	 *  }
+	 * </pre>
+	 */
+	public AnonymousSpec anonymous(){
+		if (this.anonymous == null) {
+			this.anonymous = new AnonymousSpec();
+		}
+		return this.anonymous;
+	}
+
 	/**
 	 * Configures CORS support within Spring Security. This ensures that the {@link CorsWebFilter} is place in the
 	 * correct order.
@@ -1356,6 +1385,9 @@ public class ServerHttpSecurity {
 		if (this.client != null) {
 			this.client.configure(this);
 		}
+		if (this.anonymous != null) {
+			this.anonymous.configure(this);
+		}
 		this.loginPage.configure(this);
 		if (this.logout != null) {
 			this.logout.configure(this);
@@ -2589,4 +2621,124 @@ public class ServerHttpSecurity {
 					.subscriberContext(Context.of(ServerWebExchange.class, exchange));
 		}
 	}
+
+	/**
+	 * Configures annonymous authentication
+	 * @author Ankur Pathak
+	 * @since 5.2.0
+	 */
+	public final class AnonymousSpec {
+		private String key;
+		private AnonymousAuthenticationWebFilter authenticationFilter;
+		private Object principal = "anonymousUser";
+		private List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS");
+
+		/**
+		 * Sets the key to identify tokens created for anonymous authentication. Default is a
+		 * secure randomly generated key.
+		 *
+		 * @param key the key to identify tokens created for anonymous authentication. Default
+		 * is a secure randomly generated key.
+		 * @return the {@link AnonymousSpec} for further customization of anonymous
+		 * authentication
+		 */
+		public AnonymousSpec key(String key) {
+			this.key = key;
+			return this;
+		}
+
+		/**
+		 * Sets the principal for {@link Authentication} objects of anonymous users
+		 *
+		 * @param principal used for the {@link Authentication} object of anonymous users
+		 * @return the {@link AnonymousSpec} for further customization of anonymous
+		 * authentication
+		 */
+		public AnonymousSpec principal(Object principal) {
+			this.principal = principal;
+			return this;
+		}
+
+		/**
+		 * Sets the {@link org.springframework.security.core.Authentication#getAuthorities()}
+		 * for anonymous users
+		 *
+		 * @param authorities Sets the
+		 * {@link org.springframework.security.core.Authentication#getAuthorities()} for
+		 * anonymous users
+		 * @return the {@link AnonymousSpec} for further customization of anonymous
+		 * authentication
+		 */
+		public AnonymousSpec authorities(List<GrantedAuthority> authorities) {
+			this.authorities = authorities;
+			return this;
+		}
+
+		/**
+		 * Sets the {@link org.springframework.security.core.Authentication#getAuthorities()}
+		 * for anonymous users
+		 *
+		 * @param authorities Sets the
+		 * {@link org.springframework.security.core.Authentication#getAuthorities()} for
+		 * anonymous users (i.e. "ROLE_ANONYMOUS")
+		 * @return the {@link AnonymousSpec} for further customization of anonymous
+		 * authentication
+		 */
+		public AnonymousSpec authorities(String... authorities) {
+			return authorities(AuthorityUtils.createAuthorityList(authorities));
+		}
+
+		/**
+		 * Sets the {@link AnonymousAuthenticationWebFilter} used to populate an anonymous user.
+		 * If this is set, no attributes on the {@link AnonymousSpec} will be set on the
+		 * {@link AnonymousAuthenticationWebFilter}.
+		 *
+		 * @param authenticationFilter the {@link AnonymousAuthenticationWebFilter} used to
+		 * populate an anonymous user.
+		 *
+		 * @return the {@link AnonymousSpec} for further customization of anonymous
+		 * authentication
+		 */
+		public AnonymousSpec authenticationFilter(
+				AnonymousAuthenticationWebFilter authenticationFilter) {
+			this.authenticationFilter = authenticationFilter;
+			return this;
+		}
+
+		/**
+		 * Allows method chaining to continue configuring the {@link ServerHttpSecurity}
+		 * @return the {@link ServerHttpSecurity} to continue configuring
+		 */
+		public ServerHttpSecurity and() {
+			return ServerHttpSecurity.this;
+		}
+
+		/**
+		 * Disables anonymous authentication.
+		 * @return the {@link ServerHttpSecurity} to continue configuring
+		 */
+		public ServerHttpSecurity disable() {
+			ServerHttpSecurity.this.anonymous = null;
+			return ServerHttpSecurity.this;
+		}
+
+		protected void configure(ServerHttpSecurity http) {
+			if (authenticationFilter == null) {
+				authenticationFilter = new AnonymousAuthenticationWebFilter(getKey(), principal,
+						authorities);
+			}
+			http.addFilterAt(authenticationFilter, SecurityWebFiltersOrder.ANONYMOUS_AUTHENTICATION);
+		}
+
+		private String getKey() {
+			if (key == null) {
+				key = UUID.randomUUID().toString();
+			}
+			return key;
+		}
+
+
+		private AnonymousSpec() {}
+
+	}
 }

+ 41 - 3
config/src/test/java/org/springframework/security/config/web/server/ServerHttpSecurityTests.java

@@ -34,8 +34,6 @@ import org.junit.runner.RunWith;
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnitRunner;
 
-import org.springframework.security.web.server.context.SecurityContextServerWebExchangeWebFilter;
-import org.springframework.web.server.WebFilterChain;
 import reactor.core.publisher.Mono;
 import reactor.test.publisher.TestPublisher;
 
@@ -63,6 +61,9 @@ import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RestController;
 import org.springframework.web.server.ServerWebExchange;
 import org.springframework.web.server.WebFilter;
+import org.springframework.security.web.server.context.SecurityContextServerWebExchangeWebFilter;
+import org.springframework.web.server.WebFilterChain;
+import org.springframework.security.web.server.authentication.AnonymousAuthenticationWebFilterTests;
 
 /**
  * @author Rob Winch
@@ -216,6 +217,44 @@ public class ServerHttpSecurityTests {
 
 	}
 
+	@Test
+	public void anonymous(){
+		SecurityWebFilterChain securityFilterChain = this.http.anonymous().and().build();
+		WebTestClient client = WebTestClientBuilder.bindToControllerAndWebFilters(AnonymousAuthenticationWebFilterTests.HttpMeController.class,
+				securityFilterChain).build();
+
+		client.get()
+				.uri("/me")
+				.exchange()
+				.expectStatus().isOk()
+				.expectBody(String.class).isEqualTo("anonymousUser");
+
+	}
+
+	@Test
+	public void basicWithAnonymous() {
+		given(this.authenticationManager.authenticate(any())).willReturn(Mono.just(new TestingAuthenticationToken("rob", "rob", "ROLE_USER", "ROLE_ADMIN")));
+
+		this.http.securityContextRepository(new WebSessionServerSecurityContextRepository());
+		this.http.httpBasic().and().anonymous();
+		this.http.authenticationManager(this.authenticationManager);
+		ServerHttpSecurity.AuthorizeExchangeSpec authorize = this.http.authorizeExchange();
+		authorize.anyExchange().hasAuthority("ROLE_ADMIN");
+
+		WebTestClient client = buildClient();
+
+		EntityExchangeResult<String> result = client.get()
+				.uri("/")
+				.headers(headers -> headers.setBasicAuth("rob", "rob"))
+				.exchange()
+				.expectStatus().isOk()
+				.expectHeader().valueMatches(HttpHeaders.CACHE_CONTROL, ".+")
+				.expectBody(String.class).consumeWith(b -> assertThat(b.getResponseBody()).isEqualTo("ok"))
+				.returnResult();
+
+		assertThat(result.getResponseCookies().getFirst("SESSION")).isNull();
+	}
+
 	private <T extends WebFilter> Optional<T> getWebFilter(SecurityWebFilterChain filterChain, Class<T> filterClass) {
 		return (Optional<T>) filterChain.getWebFilters()
 				.filter(Objects::nonNull)
@@ -242,7 +281,6 @@ public class ServerHttpSecurityTests {
 	}
 
 	private static class TestWebFilter implements WebFilter {
-
 		@Override
 		public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
 			return chain.filter(exchange);

+ 94 - 0
web/src/main/java/org/springframework/security/web/server/authentication/AnonymousAuthenticationWebFilter.java

@@ -0,0 +1,94 @@
+/*
+ * Copyright 2002-2018 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
+ *
+ *      http://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.List;
+
+import reactor.core.publisher.Mono;
+
+import org.springframework.security.authentication.AnonymousAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.core.context.ReactiveSecurityContextHolder;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextImpl;
+import org.springframework.util.Assert;
+import org.springframework.web.server.ServerWebExchange;
+import org.springframework.web.server.WebFilter;
+import org.springframework.web.server.WebFilterChain;
+
+/**
+ * Detects if there is no {@code Authentication} object in the
+ * {@code ReactiveSecurityContextHolder}, and populates it with one if needed.
+ *
+ * @author Ankur Pathak
+ * @since 5.2.0
+ */
+public class AnonymousAuthenticationWebFilter implements WebFilter {
+	// ~ Instance fields
+	// ================================================================================================
+
+	private String key;
+	private Object principal;
+	private List<GrantedAuthority> authorities;
+
+	/**
+	 * Creates a filter with a principal named "anonymousUser" and the single authority
+	 * "ROLE_ANONYMOUS".
+	 *
+	 * @param key the key to identify tokens created by this filter
+	 */
+	public AnonymousAuthenticationWebFilter(String key) {
+		this(key, "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
+	}
+
+	/**
+	 * @param key         key the key to identify tokens created by this filter
+	 * @param principal   the principal which will be used to represent anonymous users
+	 * @param authorities the authority list for anonymous users
+	 */
+	public AnonymousAuthenticationWebFilter(String key, Object principal,
+											List<GrantedAuthority> authorities) {
+		Assert.hasLength(key, "key cannot be null or empty");
+		Assert.notNull(principal, "Anonymous authentication principal must be set");
+		Assert.notNull(authorities, "Anonymous authorities must be set");
+		this.key = key;
+		this.principal = principal;
+		this.authorities = authorities;
+	}
+
+
+	@Override
+	public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
+		return ReactiveSecurityContextHolder.getContext()
+				.switchIfEmpty(Mono.defer(() -> {
+						SecurityContext securityContext = new SecurityContextImpl();
+						securityContext.setAuthentication(createAuthentication(exchange));
+						return chain.filter(exchange)
+								.subscriberContext(ReactiveSecurityContextHolder.withSecurityContext(Mono.just(securityContext)))
+								.then(Mono.empty());
+				})).flatMap(securityContext -> chain.filter(exchange));
+
+	}
+
+	protected Authentication createAuthentication(ServerWebExchange exchange) {
+		AnonymousAuthenticationToken auth = new AnonymousAuthenticationToken(key,
+				principal, authorities);
+		return auth;
+	}
+}

+ 9 - 0
web/src/test/java/org/springframework/security/test/web/reactive/server/WebTestClientBuilder.java

@@ -43,6 +43,14 @@ public class WebTestClientBuilder {
 		return bindToWebFilters(new WebFilterChainProxy(securityWebFilterChain));
 	}
 
+	public static Builder bindToControllerAndWebFilters(Class<?> controller, WebFilter... webFilters) {
+		return WebTestClient.bindToController(controller).webFilter(webFilters).configureClient();
+	}
+
+	public static Builder bindToControllerAndWebFilters(Class<?> controller, SecurityWebFilterChain securityWebFilterChain) {
+		return bindToControllerAndWebFilters(controller, new WebFilterChainProxy(securityWebFilterChain));
+	}
+
 	@RestController
 	public static class Http200RestController {
 		@RequestMapping("/**")
@@ -51,4 +59,5 @@ public class WebTestClientBuilder {
 			return "ok";
 		}
 	}
+
 }

+ 70 - 0
web/src/test/java/org/springframework/security/web/server/authentication/AnonymousAuthenticationWebFilterTests.java

@@ -0,0 +1,70 @@
+/*
+ * Copyright 2002-2018 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
+ *
+ *      http://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.UUID;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import reactor.core.publisher.Mono;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.ReactiveSecurityContextHolder;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.test.web.reactive.server.WebTestClient;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.server.ServerWebExchange;
+import org.springframework.security.test.web.reactive.server.WebTestClientBuilder;
+
+/**
+ * @author Ankur Pathak
+ * @since 5.2.0
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class AnonymousAuthenticationWebFilterTests {
+
+	@Test
+	public void anonymousAuthenticationFilterWorking() {
+
+		WebTestClient client = WebTestClientBuilder.bindToControllerAndWebFilters(HttpMeController.class,
+				new AnonymousAuthenticationWebFilter(UUID.randomUUID().toString()))
+				.build();
+
+		client.get()
+				.uri("/me")
+				.exchange()
+				.expectStatus().isOk()
+				.expectBody(String.class).isEqualTo("anonymousUser");
+	}
+
+	@RestController
+	@RequestMapping("/me")
+	public static class HttpMeController {
+		@GetMapping
+		public Mono<String> me(ServerWebExchange exchange) {
+			return ReactiveSecurityContextHolder
+					.getContext()
+					.map(SecurityContext::getAuthentication)
+					.map(Authentication::getPrincipal)
+					.ofType(String.class);
+		}
+	}
+}