This could take advantage of lambdas in Java 8.
This would mean we could configure things in Spring Security without worrying about indents.
.a ( aSpec ->aSpec.foo() .bar())
.b ( bSpec -> bSpec x().y())
Spring Integration and Spring Cloud Gateway both support similar nested builder style DSLs.
I'm really glad Josh raised this one (thanks @mbhave for pointing me at the issue). I've been thinking for some time that we might be able to offer a really nice lambda first configuration API now that we have Java 8 as the baseline. I even started to sketch out some ideas a few months ago, but I had to park them because of other work.
I'm personally of the opinion that trying to add a new lambda DSL on top of the existing spring-security-config might end up overwhelming the user. I wonder if we can create a brand new spring-security-dsl project that's designed from the ground-up to only be configured using lambdas?
The last time I though about this, I was keen to investigate a Customizer style callback interface that you could then use to apply additional security rules via lambdas. This might be controversial, but I also think we should move away from a fluent API and instead insist each security rule is on its own line. This might make the API a little more verbose, but I think it will result in configuration code that's easier to read. I was also inspired by AssertJ and I wondered if we could flip some of the ordering around. Rather than start with the pattern matching, and then define the rule, what if we started with the rule?
Perhaps some sample code would do a better job describing what I mean:
# Existing
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.mvcMatchers("/actuator/beans").hasRole("BEANS")
.requestMatchers(EndpointRequest.to("health", "info")).permitAll()
.requestMatchers(EndpointRequest.toAnyEndpoint().excluding(MappingsEndpoint.class)).hasRole("ACTUATOR")
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
.antMatchers("/foo").permitAll()
.antMatchers("/**").hasRole("USER")
.and()
.cors()
.and()
.httpBasic();
}
# Lambda
@Bean
public HttpSecurityCustomizer httpSecurityCustomizer() {
return (http) -> {
http.requests((request) -> {
request.mustHaveRole("BEANS").whenMatchesMvcPath("/actuator/beans");
request.isPermitted().whenMatches(EndpointRequest.to("health", "info"));
request.mustHaveRole("ACTUATOR").whenMatches(EndpointRequest.toAnyEndpoint().excluding(MappingsEndpoint.class));
request.isPermitted().whenMatches(PathRequest.toStaticResources().atCommonLocations());
request.isPermitted().whenMatchesAntPath("/foo");
request.mustHaveRole("USE").whenMatchesAntPath("/**");
});
http.crossOriginResourceSharing();
http.httpBasic();
}
}
# Existing
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest().permitAll()
.and()
.formLogin().loginPage("/login").failureUrl("/login?error")
.and()
.logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.and()
.exceptionHandling().accessDeniedPage("/access?error");
}
# Lambda
@Bean
public HttpSecurityCustomizer httpSecurityCustomizer() {
return (http) -> {
http.requests((request) -> {
request.isPermitted().whenMatchesAntPath("/login");
request.mustBeFullyAuthenticated();
});
http.formLogin((form) -> {
form.page("/login");
form.failureUrl("/login?error");
});
http.logout((logout) -> {
logout.whenMatchesAntPath("/logout");
});
http.exceptionHandling((exceptions) -> {
exceptions.accessDeniedPage("/access?error");
});
}
}
One thing that still really bugs me about this API sketch is it doesn't feel clear that the request methods are adding items and that the order is important. I think some better method names might help, but it's hard not to make the API too wordy.
Thanks for the input @philwebb.
The approach we have taken has some similarities to your suggestions, although we are continuing to the use the configure method and the HttpSecurity object.
Given this example that you showed:
// Existing
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest().permitAll()
.and()
.formLogin().loginPage("/login").failureUrl("/login?error")
.and()
.logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.and()
.exceptionHandling().accessDeniedPage("/access?error");
}
The new configuration using lambdas would look like this:
// Lambda
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests(requests ->
requests
.antMatchers("/login").permitAll()
.anyRequest().permitAll()
)
.formLogin(form ->
form
.loginPage("/login")
.failureUrl("/login?error")
)
.logout(logout ->
logout
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
)
.exceptionHandling(exceptions ->
exceptions
.accessDeniedPage("/access?error")
);
}
Or equivalently (and closer to your suggestion):
// Alternative Lambda
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests((request) -> {
request.antMatchers("/login").permitAll();
request.anyRequest().permitAll();
});
http.formLogin((form) -> {
form.loginPage("/login");
form.failureUrl("/login?error");
});
http.logout((logout) -> {
logout.logoutRequestMatcher(new AntPathRequestMatcher("/logout"));
});
http.exceptionHandling((exceptions) -> {
exceptions.accessDeniedPage("/access?error");
});
}
I favour the first lambda option with the chaining of security rules, because it is similar to the existing configuration and it looks like a builder pattern, which many users are familiar with.
In regards to the lambda configuration being part of an existing method, I agree that it might be overwhelming to the user to have multiple ways of configuring the same rule.
The alternative of having a separate method or project for the lambda configuration would make adding security rules more straightforward, but the user would still need to choose between the two different methods and know that they serve the same purpose.
I am also concerned about the strange behaviour that could occur if a user were to implement both methods, without knowing that they serve the same purpose.
I favour the first lambda option with the chaining of security rules, because it is similar to the existing configuration and it looks like a builder pattern, which many users are familiar with.
That's certainly true. If you're integrating with the existing API it doesn't make sense to restrict what can be done. I can still use the single line syntax as you suggest.
I am also concerned about the strange behaviour that could occur if a user were to implement both methods, without knowing that they serve the same purpose.
Yeah, mixing would be a nightmare. I think you'd have to fail hard if someone tried to do that. There's certainly pros/cons with the different approaches. My preference was somewhat driven by my desire to try the alternative request.isPermitted().whenMatchesAntPath(...) style method names. I don't think it makes sense of mix those in with the existing API. I'll just be happy if I can get rid of the and() methods and not need to apply so much manual formatting.
I'm looking forward to seeing what you create!
This is so awesome! Congratulations and thank you, @eleftherias !
@philwebb @eleftherias is the same approach viable for webflux config classes? I didn't find an issue raised to that
@L00kian Thank you for mentioning webflux, I have created the issue for it here gh-7107.
This is an example of how to configure HTTP security using lambdas.
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests ->
authorizeRequests
.antMatchers("/css/**", "/index").permitAll()
.antMatchers("/user/**").hasRole("USER")
)
.formLogin(formLogin ->
formLogin
.loginPage("/login")
.failureUrl("/login-error")
);
}
@eleftherias Here's a Kotlin-based SecurityConfiguration class. How do I change it to use lambdas?
@Configuration
class SecurityConfiguration : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
//@formatter:off
http
.authorizeRequests().anyRequest().authenticated()
.and()
.oauth2Login()
.and()
.oauth2ResourceServer().jwt()
http.requiresChannel().anyRequest().requiresSecure(); // <1>
http.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()); // <2>
http.headers()
.contentSecurityPolicy("script-src 'self'; report-uri /csp-report-endpoint/"); // <3>
//@formatter:on
}
}
P.S. I enjoyed learning about this feature on the Bootiful Podcast with @joshlong today!
@mraible here is the same configuration using lambdas
@Configuration
class SecurityConfiguration : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http
.authorizeRequests { requests ->
requests
.anyRequest().authenticated()
}
.oauth2Login { }
.oauth2ResourceServer { oauth2ResourceServer ->
oauth2ResourceServer
.jwt { }
}
.requiresChannel { requiresChannel ->
requiresChannel
.anyRequest().requiresSecure() // <1>
}
.csrf { csrf ->
csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) // <2>
}
.headers { headers ->
headers
.contentSecurityPolicy("script-src 'self'; report-uri /csp-report-endpoint/")// <3>
}
}
}
In this example, I have chained all the configuration options, but you can separate them like you have done in the non-lambda configuration if you prefer.
Additionally, you could emit the -> and use the it keyword, for example:
.authorizeRequests {
it
.anyRequest().authenticated()
}
Also, check out the native Kotlin DSL that we've been working on.
It's currently experimental, but we're planning on integrating it into core Spring Security in the near future.
Most helpful comment
I'm really glad Josh raised this one (thanks @mbhave for pointing me at the issue). I've been thinking for some time that we might be able to offer a really nice lambda first configuration API now that we have Java 8 as the baseline. I even started to sketch out some ideas a few months ago, but I had to park them because of other work.
I'm personally of the opinion that trying to add a new lambda DSL on top of the existing
spring-security-configmight end up overwhelming the user. I wonder if we can create a brand newspring-security-dslproject that's designed from the ground-up to only be configured using lambdas?The last time I though about this, I was keen to investigate a
Customizerstyle callback interface that you could then use to apply additional security rules via lambdas. This might be controversial, but I also think we should move away from a fluent API and instead insist each security rule is on its own line. This might make the API a little more verbose, but I think it will result in configuration code that's easier to read. I was also inspired by AssertJ and I wondered if we could flip some of the ordering around. Rather than start with the pattern matching, and then define the rule, what if we started with the rule?Perhaps some sample code would do a better job describing what I mean:
One thing that still really bugs me about this API sketch is it doesn't feel clear that the
requestmethods are adding items and that the order is important. I think some better method names might help, but it's hard not to make the API too wordy.