What difference does anyExchange().authenticated()
really make? Consider this MRE:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>security-mre</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>security-mre</name>
<description>security-mre</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
package com.example.securitymre.controller;
import com.example.securitymre.data.Hello;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/hello")
public Hello getHello() {
return new Hello();
}
}
package com.example.securitymre.data;
import lombok.Getter;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@Getter
public class Hello {
private final String message = "Hello!";
}
I hardly wrote any code, no security config, and yet
curl -i localhost:8080/hello
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="Realm"
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 0
Referrer-Policy: no-referrer
content-length: 0
I'm already barred from using my application unauthenticated
Experiment #2
package com.example.securitymre.data;
import lombok.Getter;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@Getter
public class Hello {
private String message = "Hello!";
public Hello(String message) {
this.message = message;
}
}
package com.example.securitymre.controller;
import com.example.securitymre.data.Hello;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/hello")
public Hello getHello() {
return new Hello();
}
@GetMapping("/secured-hello")
public Hello getSecuredHello() {
return new Hello("Secured hello!");
}
}
package com.example.securitymre.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain securityFilterChain(ServerHttpSecurity security) {
return security
.authorizeExchange(authorizeExchangeSpec -> authorizeExchangeSpec
.pathMatchers("/hello").permitAll()
.anyExchange()/* by "any" we mean GET /secured-hello */.authenticated()
)
.build();
}
}
curl -i localhost:8080/hello
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 20
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 0
Referrer-Policy: no-referrer
{"message":"Hello!"}
curl -i localhost:8080/secured-hello
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="Realm"
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 0
Referrer-Policy: no-referrer
content-length: 0
Or without anyExchange().authenticated()
:
package com.example.securitymre.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain securityFilterChain(ServerHttpSecurity security) {
return security
.authorizeExchange(authorizeExchangeSpec -> authorizeExchangeSpec
.pathMatchers("/hello").permitAll()
)
.build();
}
}
curl -i localhost:8080/hello
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 20
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 0
Referrer-Policy: no-referrer
{"message":"Hello!"}
curl -i localhost:8080/secured-hello
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="Realm"
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 0
Referrer-Policy: no-referrer
content-length: 0
Literally, zero difference
I don't claim to have a lot of experience, but in each project I worked on, had that anyRequest().authenticated()
line. Now it appears it's some default, and it doesn't have any practical purpose
Am I missing something? What does that line do, exactly?
TLDR: It doesn't do anything since it's called for you anyway
ServerHttpSecurity
is basically a container for configuration properties that are then used to build a WebFilter
(or more specifically, to create a SecurityWebFilterChain
which is then wrapped in a WebFilterChainProxy
object which is a WebFilter
subtype). ServerHttpSecurity
has a bunch of nested classes that encapsulate certain aspects of that configuration. One of them is AuthorizeExchangeSpec
whose purpose in life is to supply an AuthorizationManager
to AuthorizationFilter
's constructor, call it, and add the filter instance to ServerHttpSecurity
(the outer class) as an authorization filter. Here's the code that does it:
// ServerHttpSecurity.AuthorizeExchangeSpec
protected void configure(ServerHttpSecurity http) {
Assert.state(this.matcher == null,
() -> "The matcher " + this.matcher + " does not have an access rule defined");
ReactiveAuthorizationManager<ServerWebExchange> manager = this.managerBldr.build();
ObservationRegistry registry = getBeanOrDefault(ObservationRegistry.class, ObservationRegistry.NOOP);
if (!registry.isNoop()) {
manager = new ObservationReactiveAuthorizationManager<>(registry, manager);
}
AuthorizationWebFilter result = new AuthorizationWebFilter(manager);
http.addFilterAt(result, SecurityWebFiltersOrder.AUTHORIZATION);
}
That manager is treated a bit like a map. When you call all those pathMatchers().hasAnyRole()
, anyExchange().authenticated()
, etc., you're basically putting key-value pairs into that AuthenticationManager
(or rather, its builder). The "keys" are ServerWebExchangeMatcher
s which are nothing but glorified Predicate<ServerWebExchange>
(in effect). That anyExchange()
call is for all intents and purposes equivalent to serverWebExchange -> true
// ServerWebExchangeMatchers
/*
maybe I'm stupid, but aren't this anonymous subclass's equals() and hashcode()
exactly the same as if it was a lambda, i.e. this == o?
*/
@SuppressWarnings("Convert2Lambda")
public static ServerWebExchangeMatcher anyExchange() {
// we don't use a lambda to ensure a unique equals and hashcode
// which otherwise can cause problems with adding multiple entries to an ordered
// LinkedHashMap
return new ServerWebExchangeMatcher() {
@Override
public Mono<MatchResult> matches(ServerWebExchange exchange) {
return ServerWebExchangeMatcher.MatchResult.match();
}
};
}
The "values" are ReactiveAuthorizationManager
s (yes, the big authorization manager that we construct is sort of made up of tiny authorization managers inside) which are effectively Predicate<Authentication>
public class AuthenticatedReactiveAuthorizationManager<T> implements ReactiveAuthorizationManager<T> {
private AuthenticationTrustResolver authTrustResolver = new AuthenticationTrustResolverImpl();
AuthenticatedReactiveAuthorizationManager() {
}
@Override
public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, T object) {
return authentication.filter(this::isNotAnonymous)
.map(this::getAuthorizationDecision)
.defaultIfEmpty(new AuthorizationDecision(false));
}
private AuthorizationDecision getAuthorizationDecision(Authentication authentication) {
return new AuthorizationDecision(authentication.isAuthenticated());
}
public class AuthorizationDecision {
private final boolean granted;
public AuthorizationDecision(boolean granted) {
this.granted = granted;
}
public boolean isGranted() {
return this.granted;
}
So when anyExchange().authenticated()
is invoked, the following method is invoked with AuthenticatedReactiveAuthorizationManager.authenticated()
as its argument. Basically, it adds a key-value pair that says, "Whatever the request, greenlight it only if its authentication isAuthenticated()
". By the way, I could trick that authorization filter by simply registering another filter bean that simply toggles that flag
// ServerHttpSecurity.AuthorizeExchangeSpec
public AuthorizeExchangeSpec authenticated() {
return access(AuthenticatedReactiveAuthorizationManager.authenticated());
}
// ServerHttpSecurity.AuthorizeExchangeSpec
public AuthorizeExchangeSpec access(ReactiveAuthorizationManager<AuthorizationContext> manager) {
AuthorizeExchangeSpec.this.managerBldr
.add(new ServerWebExchangeMatcherEntry<>(AuthorizeExchangeSpec.this.matcher, manager));
AuthorizeExchangeSpec.this.matcher = null;
return AuthorizeExchangeSpec.this;
}
Now, why is the behavior the same even without any explicit configuration at all?
First, there's an out-of-the-box configuration called WebFluxSecurityConfiguration
that supplies a WebFliterChainProxy
built from a default ServerHttpSecurity
. Here it is. Notice it doesn't call authorizeExchange()
which means authorizeExchange
stays null
(I checked it with the debugger)
// ServerHttpSecurityConfiguration
/*
By the way, do you know that this bean's name is "org.springframework.security.config.annotation.web.reactive.HttpSecurityConfiguration.httpSecurity"
even though it's declared in a **Server**HttpSecurityConfiguration? I guess it's due to a copy-paste from the old HttpSecurity
*/
@Bean(HTTPSECURITY_BEAN_NAME)
@Scope("prototype")
ServerHttpSecurity httpSecurity() {
ContextAwareServerHttpSecurity http = new ContextAwareServerHttpSecurity();
// @formatter:off
return http.authenticationManager(authenticationManager())
.headers().and()
.logout().and();
// @formatter:on
}
// ServerHttpSecurity
@Deprecated(since = "6.1", forRemoval = true)
public AuthorizeExchangeSpec authorizeExchange() {
if (this.authorizeExchange == null) {
this.authorizeExchange = new AuthorizeExchangeSpec();
}
return this.authorizeExchange;
}
// ServerHttpSecurity
public ServerHttpSecurity authorizeExchange(Customizer<AuthorizeExchangeSpec> authorizeExchangeCustomizer) {
if (this.authorizeExchange == null) {
this.authorizeExchange = new AuthorizeExchangeSpec();
}
authorizeExchangeCustomizer.customize(this.authorizeExchange);
return this;
}
Second, when they do retrieve that bean from the context, they keep configuring it which includes calling those exact methods, anyRequest().authenticated()
The fact that they retrieve a bean from the context (which should be completely initialized by that point) and still keep configuring it, spreading defaults all over the codebase instead of keeping them in one place makes me unhappy
@Bean(SPRING_SECURITY_WEBFILTERCHAINFILTER_BEAN_NAME)
@Order(WEB_FILTER_CHAIN_FILTER_ORDER)
WebFilterChainProxy springSecurityWebFilterChainFilter() {
WebFilterChainProxy proxy = new WebFilterChainProxy(getSecurityWebFilterChains());
if (!this.observationRegistry.isNoop()) {
proxy.setFilterChainDecorator(new ObservationWebFilterChainDecorator(this.observationRegistry));
}
return proxy;
}
@Bean(name = AbstractView.REQUEST_DATA_VALUE_PROCESSOR_BEAN_NAME)
CsrfRequestDataValueProcessor requestDataValueProcessor() {
return new CsrfRequestDataValueProcessor();
}
@Bean
static BeanFactoryPostProcessor conversionServicePostProcessor() {
return new RsaKeyConversionServicePostProcessor();
}
private List<SecurityWebFilterChain> getSecurityWebFilterChains() {
List<SecurityWebFilterChain> result = this.securityWebFilterChains;
if (ObjectUtils.isEmpty(result)) {
return Arrays.asList(springSecurityFilterChain());
}
return result;
}
private SecurityWebFilterChain springSecurityFilterChain() {
ServerHttpSecurity http = this.context.getBean(ServerHttpSecurity.class); // get default ServerHttpSecurity bean...
return springSecurityFilterChain(http);
}
/**
* The default {@link ServerHttpSecurity} configuration.
* @param http
* @return
*/
private SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.authorizeExchange().anyExchange().authenticated(); // ...and keep configuring it! Wtf is this?
if (isOAuth2Present && OAuth2ClasspathGuard.shouldConfigure(this.context)) {
OAuth2ClasspathGuard.configure(this.context, http);
}
else {
http.httpBasic();
http.formLogin();
}
SecurityWebFilterChain result = http.build();
return result;
}
I could mention WebHttpHandlerBuilder
(which asked the context for all WebFilter
beans, including our WebFilterChainProxy
), ConfigurationClassPostProcessor
(which put the bean definition of that default ServerHttpSecurity
to the context's bean factory in the first place), but I guess that would be too much of a diversion