diff --git a/config/src/main/java/org/springframework/security/config/web/server/SecurityWebFiltersOrder.java b/config/src/main/java/org/springframework/security/config/web/server/SecurityWebFiltersOrder.java
index 6e1f1db678..cfc70becde 100644
--- a/config/src/main/java/org/springframework/security/config/web/server/SecurityWebFiltersOrder.java
+++ b/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,
diff --git a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java
index ca75c30d3c..d469d8627f 100644
--- a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java
+++ b/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.
+ *
+ *
+ * @Bean
+ * public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
+ * http
+ * // ...
+ * .anonymous().key("key")
+ * .authorities("ROLE_ANONYMOUS");
+ * return http.build();
+ * }
+ *
+ */
+ 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 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 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() {}
+
+ }
}
diff --git a/config/src/test/java/org/springframework/security/config/web/server/ServerHttpSecurityTests.java b/config/src/test/java/org/springframework/security/config/web/server/ServerHttpSecurityTests.java
index eb10e23464..720312d493 100644
--- a/config/src/test/java/org/springframework/security/config/web/server/ServerHttpSecurityTests.java
+++ b/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 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 Optional getWebFilter(SecurityWebFilterChain filterChain, Class filterClass) {
return (Optional) filterChain.getWebFilters()
.filter(Objects::nonNull)
@@ -242,7 +281,6 @@ public class ServerHttpSecurityTests {
}
private static class TestWebFilter implements WebFilter {
-
@Override
public Mono filter(ServerWebExchange exchange, WebFilterChain chain) {
return chain.filter(exchange);
diff --git a/web/src/main/java/org/springframework/security/web/server/authentication/AnonymousAuthenticationWebFilter.java b/web/src/main/java/org/springframework/security/web/server/authentication/AnonymousAuthenticationWebFilter.java
new file mode 100644
index 0000000000..15414d2f28
--- /dev/null
+++ b/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 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 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 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;
+ }
+}
diff --git a/web/src/test/java/org/springframework/security/test/web/reactive/server/WebTestClientBuilder.java b/web/src/test/java/org/springframework/security/test/web/reactive/server/WebTestClientBuilder.java
index 2ed8654b8b..84580c698e 100644
--- a/web/src/test/java/org/springframework/security/test/web/reactive/server/WebTestClientBuilder.java
+++ b/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";
}
}
+
}
diff --git a/web/src/test/java/org/springframework/security/web/server/authentication/AnonymousAuthenticationWebFilterTests.java b/web/src/test/java/org/springframework/security/web/server/authentication/AnonymousAuthenticationWebFilterTests.java
new file mode 100644
index 0000000000..86ceb18ac9
--- /dev/null
+++ b/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 me(ServerWebExchange exchange) {
+ return ReactiveSecurityContextHolder
+ .getContext()
+ .map(SecurityContext::getAuthentication)
+ .map(Authentication::getPrincipal)
+ .ofType(String.class);
+ }
+ }
+}