Skip to content

Commit 48edf49

Browse files
committed
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
1 parent 566bc6a commit 48edf49

File tree

6 files changed

+370
-3
lines changed

6 files changed

+370
-3
lines changed

config/src/main/java/org/springframework/security/config/web/server/SecurityWebFiltersOrder.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ public enum SecurityWebFiltersOrder {
4848
*/
4949
FORM_LOGIN,
5050
AUTHENTICATION,
51+
/**
52+
* Instance of AnonymousAuthenticationWebFilter
53+
*/
54+
ANONYMOUS_AUTHENTICATION,
5155
OAUTH2_AUTHORIZATION_CODE,
5256
LOGIN_PAGE_GENERATING,
5357
LOGOUT_PAGE_GENERATING,

config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import java.util.Map;
3434
import java.util.Optional;
3535
import java.util.function.Function;
36+
import java.util.UUID;
3637

3738
import reactor.core.publisher.Mono;
3839
import reactor.util.context.Context;
@@ -158,6 +159,9 @@
158159
import org.springframework.web.server.ServerWebExchange;
159160
import org.springframework.web.server.WebFilter;
160161
import org.springframework.web.server.WebFilterChain;
162+
import org.springframework.security.web.server.authentication.AnonymousAuthenticationWebFilter;
163+
import org.springframework.security.core.GrantedAuthority;
164+
import org.springframework.security.core.authority.AuthorityUtils;
161165

162166
/**
163167
* A {@link ServerHttpSecurity} is similar to Spring Security's {@code HttpSecurity} but for WebFlux.
@@ -264,6 +268,8 @@ public class ServerHttpSecurity {
264268

265269
private Throwable built;
266270

271+
private AnonymousSpec anonymous;
272+
267273
/**
268274
* The ServerExchangeMatcher that determines which requests apply to this HttpSecurity instance.
269275
*
@@ -425,6 +431,29 @@ public CorsSpec cors() {
425431
return this.cors;
426432
}
427433

434+
/**
435+
* @since 5.2.0
436+
* @author Ankur Pathak
437+
* Enables and Configures annonymous authentication. Anonymous Authentication is disabled by default.
438+
*
439+
* <pre class="code">
440+
* &#064;Bean
441+
* public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
442+
* http
443+
* // ...
444+
* .anonymous().key("key")
445+
* .authorities("ROLE_ANONYMOUS");
446+
* return http.build();
447+
* }
448+
* </pre>
449+
*/
450+
public AnonymousSpec anonymous(){
451+
if (this.anonymous == null) {
452+
this.anonymous = new AnonymousSpec();
453+
}
454+
return this.anonymous;
455+
}
456+
428457
/**
429458
* Configures CORS support within Spring Security. This ensures that the {@link CorsWebFilter} is place in the
430459
* correct order.
@@ -1356,6 +1385,9 @@ public SecurityWebFilterChain build() {
13561385
if (this.client != null) {
13571386
this.client.configure(this);
13581387
}
1388+
if (this.anonymous != null) {
1389+
this.anonymous.configure(this);
1390+
}
13591391
this.loginPage.configure(this);
13601392
if (this.logout != null) {
13611393
this.logout.configure(this);
@@ -2589,4 +2621,124 @@ public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
25892621
.subscriberContext(Context.of(ServerWebExchange.class, exchange));
25902622
}
25912623
}
2624+
2625+
/**
2626+
* Configures annonymous authentication
2627+
* @author Ankur Pathak
2628+
* @since 5.2.0
2629+
*/
2630+
public final class AnonymousSpec {
2631+
private String key;
2632+
private AnonymousAuthenticationWebFilter authenticationFilter;
2633+
private Object principal = "anonymousUser";
2634+
private List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS");
2635+
2636+
/**
2637+
* Sets the key to identify tokens created for anonymous authentication. Default is a
2638+
* secure randomly generated key.
2639+
*
2640+
* @param key the key to identify tokens created for anonymous authentication. Default
2641+
* is a secure randomly generated key.
2642+
* @return the {@link AnonymousSpec} for further customization of anonymous
2643+
* authentication
2644+
*/
2645+
public AnonymousSpec key(String key) {
2646+
this.key = key;
2647+
return this;
2648+
}
2649+
2650+
/**
2651+
* Sets the principal for {@link Authentication} objects of anonymous users
2652+
*
2653+
* @param principal used for the {@link Authentication} object of anonymous users
2654+
* @return the {@link AnonymousSpec} for further customization of anonymous
2655+
* authentication
2656+
*/
2657+
public AnonymousSpec principal(Object principal) {
2658+
this.principal = principal;
2659+
return this;
2660+
}
2661+
2662+
/**
2663+
* Sets the {@link org.springframework.security.core.Authentication#getAuthorities()}
2664+
* for anonymous users
2665+
*
2666+
* @param authorities Sets the
2667+
* {@link org.springframework.security.core.Authentication#getAuthorities()} for
2668+
* anonymous users
2669+
* @return the {@link AnonymousSpec} for further customization of anonymous
2670+
* authentication
2671+
*/
2672+
public AnonymousSpec authorities(List<GrantedAuthority> authorities) {
2673+
this.authorities = authorities;
2674+
return this;
2675+
}
2676+
2677+
/**
2678+
* Sets the {@link org.springframework.security.core.Authentication#getAuthorities()}
2679+
* for anonymous users
2680+
*
2681+
* @param authorities Sets the
2682+
* {@link org.springframework.security.core.Authentication#getAuthorities()} for
2683+
* anonymous users (i.e. "ROLE_ANONYMOUS")
2684+
* @return the {@link AnonymousSpec} for further customization of anonymous
2685+
* authentication
2686+
*/
2687+
public AnonymousSpec authorities(String... authorities) {
2688+
return authorities(AuthorityUtils.createAuthorityList(authorities));
2689+
}
2690+
2691+
/**
2692+
* Sets the {@link AnonymousAuthenticationWebFilter} used to populate an anonymous user.
2693+
* If this is set, no attributes on the {@link AnonymousSpec} will be set on the
2694+
* {@link AnonymousAuthenticationWebFilter}.
2695+
*
2696+
* @param authenticationFilter the {@link AnonymousAuthenticationWebFilter} used to
2697+
* populate an anonymous user.
2698+
*
2699+
* @return the {@link AnonymousSpec} for further customization of anonymous
2700+
* authentication
2701+
*/
2702+
public AnonymousSpec authenticationFilter(
2703+
AnonymousAuthenticationWebFilter authenticationFilter) {
2704+
this.authenticationFilter = authenticationFilter;
2705+
return this;
2706+
}
2707+
2708+
/**
2709+
* Allows method chaining to continue configuring the {@link ServerHttpSecurity}
2710+
* @return the {@link ServerHttpSecurity} to continue configuring
2711+
*/
2712+
public ServerHttpSecurity and() {
2713+
return ServerHttpSecurity.this;
2714+
}
2715+
2716+
/**
2717+
* Disables anonymous authentication.
2718+
* @return the {@link ServerHttpSecurity} to continue configuring
2719+
*/
2720+
public ServerHttpSecurity disable() {
2721+
ServerHttpSecurity.this.anonymous = null;
2722+
return ServerHttpSecurity.this;
2723+
}
2724+
2725+
protected void configure(ServerHttpSecurity http) {
2726+
if (authenticationFilter == null) {
2727+
authenticationFilter = new AnonymousAuthenticationWebFilter(getKey(), principal,
2728+
authorities);
2729+
}
2730+
http.addFilterAt(authenticationFilter, SecurityWebFiltersOrder.ANONYMOUS_AUTHENTICATION);
2731+
}
2732+
2733+
private String getKey() {
2734+
if (key == null) {
2735+
key = UUID.randomUUID().toString();
2736+
}
2737+
return key;
2738+
}
2739+
2740+
2741+
private AnonymousSpec() {}
2742+
2743+
}
25922744
}

config/src/test/java/org/springframework/security/config/web/server/ServerHttpSecurityTests.java

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,6 @@
3434
import org.mockito.Mock;
3535
import org.mockito.junit.MockitoJUnitRunner;
3636

37-
import org.springframework.security.web.server.context.SecurityContextServerWebExchangeWebFilter;
38-
import org.springframework.web.server.WebFilterChain;
3937
import reactor.core.publisher.Mono;
4038
import reactor.test.publisher.TestPublisher;
4139

@@ -63,6 +61,9 @@
6361
import org.springframework.web.bind.annotation.RestController;
6462
import org.springframework.web.server.ServerWebExchange;
6563
import org.springframework.web.server.WebFilter;
64+
import org.springframework.security.web.server.context.SecurityContextServerWebExchangeWebFilter;
65+
import org.springframework.web.server.WebFilterChain;
66+
import org.springframework.security.web.server.authentication.AnonymousAuthenticationWebFilterTests;
6667

6768
/**
6869
* @author Rob Winch
@@ -216,6 +217,44 @@ public void addFilterBeforeIsApplied(){
216217

217218
}
218219

220+
@Test
221+
public void anonymous(){
222+
SecurityWebFilterChain securityFilterChain = this.http.anonymous().and().build();
223+
WebTestClient client = WebTestClientBuilder.bindToControllerAndWebFilters(AnonymousAuthenticationWebFilterTests.HttpMeController.class,
224+
securityFilterChain).build();
225+
226+
client.get()
227+
.uri("/me")
228+
.exchange()
229+
.expectStatus().isOk()
230+
.expectBody(String.class).isEqualTo("anonymousUser");
231+
232+
}
233+
234+
@Test
235+
public void basicWithAnonymous() {
236+
given(this.authenticationManager.authenticate(any())).willReturn(Mono.just(new TestingAuthenticationToken("rob", "rob", "ROLE_USER", "ROLE_ADMIN")));
237+
238+
this.http.securityContextRepository(new WebSessionServerSecurityContextRepository());
239+
this.http.httpBasic().and().anonymous();
240+
this.http.authenticationManager(this.authenticationManager);
241+
ServerHttpSecurity.AuthorizeExchangeSpec authorize = this.http.authorizeExchange();
242+
authorize.anyExchange().hasAuthority("ROLE_ADMIN");
243+
244+
WebTestClient client = buildClient();
245+
246+
EntityExchangeResult<String> result = client.get()
247+
.uri("/")
248+
.headers(headers -> headers.setBasicAuth("rob", "rob"))
249+
.exchange()
250+
.expectStatus().isOk()
251+
.expectHeader().valueMatches(HttpHeaders.CACHE_CONTROL, ".+")
252+
.expectBody(String.class).consumeWith(b -> assertThat(b.getResponseBody()).isEqualTo("ok"))
253+
.returnResult();
254+
255+
assertThat(result.getResponseCookies().getFirst("SESSION")).isNull();
256+
}
257+
219258
private <T extends WebFilter> Optional<T> getWebFilter(SecurityWebFilterChain filterChain, Class<T> filterClass) {
220259
return (Optional<T>) filterChain.getWebFilters()
221260
.filter(Objects::nonNull)
@@ -242,7 +281,6 @@ Mono<String> pathWithinApplicationFromContext() {
242281
}
243282

244283
private static class TestWebFilter implements WebFilter {
245-
246284
@Override
247285
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
248286
return chain.filter(exchange);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Copyright 2002-2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.web.server.authentication;
18+
19+
import java.util.List;
20+
21+
import reactor.core.publisher.Mono;
22+
23+
import org.springframework.security.authentication.AnonymousAuthenticationToken;
24+
import org.springframework.security.core.Authentication;
25+
import org.springframework.security.core.GrantedAuthority;
26+
import org.springframework.security.core.authority.AuthorityUtils;
27+
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
28+
import org.springframework.security.core.context.SecurityContext;
29+
import org.springframework.security.core.context.SecurityContextImpl;
30+
import org.springframework.util.Assert;
31+
import org.springframework.web.server.ServerWebExchange;
32+
import org.springframework.web.server.WebFilter;
33+
import org.springframework.web.server.WebFilterChain;
34+
35+
/**
36+
* Detects if there is no {@code Authentication} object in the
37+
* {@code ReactiveSecurityContextHolder}, and populates it with one if needed.
38+
*
39+
* @author Ankur Pathak
40+
* @since 5.2.0
41+
*/
42+
public class AnonymousAuthenticationWebFilter implements WebFilter {
43+
// ~ Instance fields
44+
// ================================================================================================
45+
46+
private String key;
47+
private Object principal;
48+
private List<GrantedAuthority> authorities;
49+
50+
/**
51+
* Creates a filter with a principal named "anonymousUser" and the single authority
52+
* "ROLE_ANONYMOUS".
53+
*
54+
* @param key the key to identify tokens created by this filter
55+
*/
56+
public AnonymousAuthenticationWebFilter(String key) {
57+
this(key, "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
58+
}
59+
60+
/**
61+
* @param key key the key to identify tokens created by this filter
62+
* @param principal the principal which will be used to represent anonymous users
63+
* @param authorities the authority list for anonymous users
64+
*/
65+
public AnonymousAuthenticationWebFilter(String key, Object principal,
66+
List<GrantedAuthority> authorities) {
67+
Assert.hasLength(key, "key cannot be null or empty");
68+
Assert.notNull(principal, "Anonymous authentication principal must be set");
69+
Assert.notNull(authorities, "Anonymous authorities must be set");
70+
this.key = key;
71+
this.principal = principal;
72+
this.authorities = authorities;
73+
}
74+
75+
76+
@Override
77+
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
78+
return ReactiveSecurityContextHolder.getContext()
79+
.switchIfEmpty(Mono.defer(() -> {
80+
SecurityContext securityContext = new SecurityContextImpl();
81+
securityContext.setAuthentication(createAuthentication(exchange));
82+
return chain.filter(exchange)
83+
.subscriberContext(ReactiveSecurityContextHolder.withSecurityContext(Mono.just(securityContext)))
84+
.then(Mono.empty());
85+
})).flatMap(securityContext -> chain.filter(exchange));
86+
87+
}
88+
89+
protected Authentication createAuthentication(ServerWebExchange exchange) {
90+
AnonymousAuthenticationToken auth = new AnonymousAuthenticationToken(key,
91+
principal, authorities);
92+
return auth;
93+
}
94+
}

web/src/test/java/org/springframework/security/test/web/reactive/server/WebTestClientBuilder.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ public static Builder bindToWebFilters(SecurityWebFilterChain securityWebFilterC
4343
return bindToWebFilters(new WebFilterChainProxy(securityWebFilterChain));
4444
}
4545

46+
public static Builder bindToControllerAndWebFilters(Class<?> controller, WebFilter... webFilters) {
47+
return WebTestClient.bindToController(controller).webFilter(webFilters).configureClient();
48+
}
49+
50+
public static Builder bindToControllerAndWebFilters(Class<?> controller, SecurityWebFilterChain securityWebFilterChain) {
51+
return bindToControllerAndWebFilters(controller, new WebFilterChainProxy(securityWebFilterChain));
52+
}
53+
4654
@RestController
4755
public static class Http200RestController {
4856
@RequestMapping("/**")
@@ -51,4 +59,5 @@ public String ok() {
5159
return "ok";
5260
}
5361
}
62+
5463
}

0 commit comments

Comments
 (0)