Add WebExpressionAuthorizationManager.Builder

Closes gh-17504
This commit is contained in:
Josh Cummings 2025-07-09 15:54:45 -06:00
parent c312d18191
commit dadf10899c
No known key found for this signature in database
GPG Key ID: 869B37A20E876129
3 changed files with 181 additions and 1 deletions

View File

@ -996,6 +996,29 @@ Kotlin::
----
======
To migrate several, you can use `WebExpressionAuthorizationManager#withDefaults`:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
WebExpressionAuthorizationManager.Builder authz = WebExpressionAuthorizationManager.withDefaults();
.requestMatchers("/test/**").access(authz.expression("hasRole('ADMIN') && hasRole('USER')"))
.requestMatchers("/test/**").access(authz.expression("permitAll"))
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
var authz = WebExpressionAuthorizationManager.withDefaults()
.requestMatchers("/test/**").access(authz.expression("hasRole('ADMIN') && hasRole('USER')"))
.requestMatchers("/test/**").access(authz.expression("permitAll"))
----
======
If you are referring to a bean in your expression like so: `@webSecurity.check(authentication, request)`, it's recommended that you instead call the bean directly, which will look something like the following:
[tabs]
@ -1019,7 +1042,32 @@ Kotlin::
For complex instructions that include bean references as well as other expressions, it is recommended that you change those to implement `AuthorizationManager` and refer to them by calling `.access(AuthorizationManager)`.
If you are not able to do that, you can configure a javadoc:org.springframework.security.web.access.expression.DefaultHttpSecurityExpressionHandler[] with a bean resolver and supply that to `WebExpressionAuthorizationManager#setExpressionhandler`.
If you are not able to do that, you can publish javadoc:org.springframework.security.web.access.expression.WebExpressionAuthorizationManager$Builder[] as a bean:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
WebExpressionAuthorizationManager.Builder authz() {
return WebExpressionAuthorizationManager.withDefaults();
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun authz(): WebExpressionAuthorizationManager.Builder {
return WebExpressionAuthorizationManager.withDefaults()
}
----
======
Then, expressions passed to that builder will be able to refer to beans.
[[security-matchers]]
== Security Matchers

View File

@ -18,6 +18,9 @@ package org.springframework.security.web.access.expression;
import java.util.function.Supplier;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.security.access.expression.ExpressionUtils;
@ -51,11 +54,20 @@ public final class WebExpressionAuthorizationManager implements AuthorizationMan
this.expression = this.expressionHandler.getExpressionParser().parseExpression(expressionString);
}
private WebExpressionAuthorizationManager(String expressionString,
SecurityExpressionHandler<RequestAuthorizationContext> expressionHandler) {
Assert.hasText(expressionString, "expressionString cannot be empty");
this.expressionHandler = expressionHandler;
this.expression = expressionHandler.getExpressionParser().parseExpression(expressionString);
}
/**
* Sets the {@link SecurityExpressionHandler} to be used. The default is
* {@link DefaultHttpSecurityExpressionHandler}.
* @param expressionHandler the {@link SecurityExpressionHandler} to use
* @deprecated Please use {@link #withDefaults()} or {@link #withExpressionHandler}
*/
@Deprecated
public void setExpressionHandler(SecurityExpressionHandler<RequestAuthorizationContext> expressionHandler) {
Assert.notNull(expressionHandler, "expressionHandler cannot be null");
this.expressionHandler = expressionHandler;
@ -82,4 +94,78 @@ public final class WebExpressionAuthorizationManager implements AuthorizationMan
return "WebExpressionAuthorizationManager[expression='" + this.expression + "']";
}
/**
* Use a {@link DefaultHttpSecurityExpressionHandler} to create
* {@link WebExpressionAuthorizationManager} instances.
*
* <p>
* Note that publishing the {@link Builder} as a bean will allow the default
* expression handler to be configured with a bean provider so that expressions can
* reference beans
* @return a {@link Builder} for constructing
* {@link WebExpressionAuthorizationManager} instances
* @since 7.0
*/
public static Builder withDefaults() {
return new Builder();
}
/**
* Use this {@link SecurityExpressionHandler} to create
* {@link WebExpressionAuthorizationManager} instances
* @param expressionHandler
* @return a {@link Builder} for constructing
* {@link WebExpressionAuthorizationManager} instances
* @since 7.0
*/
public static Builder withExpressionHandler(
SecurityExpressionHandler<RequestAuthorizationContext> expressionHandler) {
return new Builder(expressionHandler);
}
/**
* A {@link Builder} for constructing {@link WebExpressionAuthorizationManager}
* instances.
*
* <p>
* May be reused to create multiple instances.
*
* @author Josh Cummings
* @since 7.0
*/
public static final class Builder implements ApplicationContextAware {
private final SecurityExpressionHandler<RequestAuthorizationContext> expressionHandler;
private final boolean defaultExpressionHandler;
private Builder() {
this.expressionHandler = new DefaultHttpSecurityExpressionHandler();
this.defaultExpressionHandler = true;
}
private Builder(SecurityExpressionHandler<RequestAuthorizationContext> expressionHandler) {
this.expressionHandler = expressionHandler;
this.defaultExpressionHandler = false;
}
/**
* Create a {@link WebExpressionAuthorizationManager} using this
* {@code expression}
* @param expression the expression to evaluate
* @return the resulting {@link AuthorizationManager}
*/
public WebExpressionAuthorizationManager expression(String expression) {
return new WebExpressionAuthorizationManager(expression, this.expressionHandler);
}
@Override
public void setApplicationContext(ApplicationContext context) throws BeansException {
if (this.defaultExpressionHandler) {
((DefaultHttpSecurityExpressionHandler) this.expressionHandler).setApplicationContext(context);
}
}
}
}

View File

@ -18,6 +18,7 @@ package org.springframework.security.web.access.expression;
import org.junit.jupiter.api.Test;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.mock.web.MockHttpServletRequest;
@ -102,4 +103,49 @@ class WebExpressionAuthorizationManagerTests {
assertThat(decision.isGranted()).isFalse();
}
@Test
void authorizeWhenDefaultsThenEvaluatesExpressionsReferencingBeans() {
GenericApplicationContext context = new GenericApplicationContext();
context.registerBean("bean", WebExpressionAuthorizationManagerTests.class, () -> this);
context.refresh();
WebExpressionAuthorizationManager.Builder builder = WebExpressionAuthorizationManager.withDefaults();
builder.setApplicationContext(context);
WebExpressionAuthorizationManager manager = builder
.expression("@bean.class.simpleName.startsWith('WebExpression')");
AuthorizationResult result = manager.authorize(TestAuthentication::authenticatedUser,
new RequestAuthorizationContext(new MockHttpServletRequest()));
assertThat(result.isGranted()).isTrue();
}
@Test
void authorizeWhenDefaultsAsBeanThenEvaluatesExpressionsReferencingBeans() {
GenericApplicationContext context = new GenericApplicationContext();
context.registerBean("bean", WebExpressionAuthorizationManagerTests.class, () -> this);
context.registerBean("builder", WebExpressionAuthorizationManager.Builder.class,
WebExpressionAuthorizationManager::withDefaults);
context.refresh();
WebExpressionAuthorizationManager.Builder builder = context
.getBean(WebExpressionAuthorizationManager.Builder.class);
WebExpressionAuthorizationManager manager = builder
.expression("@bean.class.simpleName.startsWith('WebExpression')");
AuthorizationResult result = manager.authorize(TestAuthentication::authenticatedUser,
new RequestAuthorizationContext(new MockHttpServletRequest()));
assertThat(result.isGranted()).isTrue();
}
@Test
void authorizeWhenExpressionHandlerHasBeanProviderThenEvaluatesExpressionsReferencingBeans() {
GenericApplicationContext context = new GenericApplicationContext();
context.registerBean("bean", WebExpressionAuthorizationManagerTests.class, () -> this);
context.refresh();
DefaultHttpSecurityExpressionHandler expressionHandler = new DefaultHttpSecurityExpressionHandler();
expressionHandler.setApplicationContext(context);
WebExpressionAuthorizationManager manager = WebExpressionAuthorizationManager
.withExpressionHandler(expressionHandler)
.expression("@bean.class.simpleName.startsWith('WebExpression')");
AuthorizationResult result = manager.authorize(TestAuthentication::authenticatedUser,
new RequestAuthorizationContext(new MockHttpServletRequest()));
assertThat(result.isGranted()).isTrue();
}
}