1
0
mirror of synced 2026-05-22 21:33:16 +00:00

Add MessageExpressionAuthorizationManager

Closes gh-12650

Signed-off-by: wonderfulrosemari <whwlsgur1419@naver.com>
This commit is contained in:
wonderfulrosemari
2026-03-04 10:05:54 +09:00
committed by Josh Cummings
parent b1f4deafe6
commit 07297e7a80
4 changed files with 262 additions and 69 deletions
@@ -23,9 +23,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
import org.jspecify.annotations.Nullable;
import org.w3c.dom.Element;
import org.springframework.beans.BeansException;
@@ -44,25 +42,18 @@ import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.beans.factory.xml.BeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.beans.factory.xml.XmlReaderContext;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.messaging.Message;
import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.messaging.simp.annotation.support.SimpAnnotationMethodMessageHandler;
import org.springframework.security.access.expression.ExpressionUtils;
import org.springframework.security.access.expression.SecurityExpressionHandler;
import org.springframework.security.access.vote.ConsensusBased;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.config.Elements;
import org.springframework.security.config.http.MessageMatcherFactoryBean;
import org.springframework.security.config.web.messaging.PathPatternMessageMatcherBuilderFactoryBean;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.messaging.access.expression.ExpressionBasedMessageSecurityMetadataSourceFactory;
import org.springframework.security.messaging.access.expression.MessageAuthorizationContextSecurityExpressionHandler;
import org.springframework.security.messaging.access.expression.MessageExpressionAuthorizationManager;
import org.springframework.security.messaging.access.expression.MessageExpressionVoter;
import org.springframework.security.messaging.access.intercept.AuthorizationChannelInterceptor;
import org.springframework.security.messaging.access.intercept.ChannelSecurityInterceptor;
@@ -75,7 +66,6 @@ import org.springframework.security.messaging.util.matcher.SimpMessageTypeMatche
import org.springframework.security.messaging.web.csrf.XorCsrfChannelInterceptor;
import org.springframework.security.messaging.web.socket.server.CsrfTokenHandshakeInterceptor;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.PathMatcher;
import org.springframework.util.StringUtils;
import org.springframework.util.xml.DomUtils;
@@ -219,9 +209,15 @@ public final class WebSocketMessageBrokerSecurityBeanDefinitionParser implements
String messageType = interceptMessage.getAttribute(TYPE_ATTR);
BeanDefinition matcher = createMatcher(matcherPattern, messageType, parserContext, interceptMessage);
BeanDefinitionBuilder authorizationManager = BeanDefinitionBuilder
.rootBeanDefinition(ExpressionBasedAuthorizationManager.class);
.rootBeanDefinition(MessageExpressionAuthorizationManager.class);
if (StringUtils.hasText(expressionHandlerRef)) {
authorizationManager.addConstructorArgReference(expressionHandlerRef);
BeanDefinitionBuilder authorizationManagerBuilder = BeanDefinitionBuilder
.rootBeanDefinition(MessageExpressionAuthorizationManager.class);
authorizationManagerBuilder.setFactoryMethod("withSecurityExpressionHandler");
authorizationManagerBuilder.addConstructorArgReference(expressionHandlerRef);
String authorizationManagerBuilderRef = context
.registerWithGeneratedName(authorizationManagerBuilder.getBeanDefinition());
authorizationManager.setFactoryMethodOnBean("expression", authorizationManagerBuilderRef);
}
authorizationManager.addConstructorArgValue(accessExpression);
matcherToExpression.put(matcher, authorizationManager.getBeanDefinition());
@@ -439,35 +435,6 @@ public final class WebSocketMessageBrokerSecurityBeanDefinitionParser implements
}
private static final class ExpressionBasedAuthorizationManager
implements AuthorizationManager<MessageAuthorizationContext<?>> {
private final SecurityExpressionHandler<MessageAuthorizationContext<?>> expressionHandler;
private final Expression expression;
private ExpressionBasedAuthorizationManager(String expression) {
this(new MessageAuthorizationContextSecurityExpressionHandler(), expression);
}
private ExpressionBasedAuthorizationManager(
SecurityExpressionHandler<MessageAuthorizationContext<?>> expressionHandler, String expression) {
Assert.notNull(expressionHandler, "expressionHandler cannot be null");
Assert.notNull(expression, "expression cannot be null");
this.expressionHandler = expressionHandler;
this.expression = this.expressionHandler.getExpressionParser().parseExpression(expression);
}
@Override
public AuthorizationResult authorize(Supplier<? extends @Nullable Authentication> authentication,
MessageAuthorizationContext<?> object) {
EvaluationContext context = this.expressionHandler.createEvaluationContext(authentication, object);
boolean granted = ExpressionUtils.evaluateAsBoolean(this.expression, context);
return new AuthorizationDecision(granted);
}
}
private static class MessageMatcherDelegatingAuthorizationManagerFactory {
private static AuthorizationManager<Message<?>> createMessageMatcherDelegatingAuthorizationManager(
@@ -212,32 +212,10 @@ This will ensure that:
If you are migrating from an older version of Spring Security, your destination matchers may include SpEL expressions.
It's recommended that these be changed to using concrete implementations of `AuthorizationManager` since this is independently testable.
However, to ease migration, you can also use a class like the following:
However, to ease migration, you can use
`org.springframework.security.messaging.access.expression.MessageExpressionAuthorizationManager`.
[source,java]
----
public final class MessageExpressionAuthorizationManager implements AuthorizationManager<MessageAuthorizationContext<?>> {
private SecurityExpressionHandler<Message<?>> expressionHandler = new DefaultMessageSecurityExpressionHandler();
private Expression expression;
public MessageExpressionAuthorizationManager(String expressionString) {
Assert.hasText(expressionString, "expressionString cannot be empty");
this.expression = this.expressionHandler.getExpressionParser().parseExpression(expressionString);
}
@Override
public AuthorizationResult authorize(Supplier<Authentication> authentication, MessageAuthorizationContext<?> context) {
EvaluationContext ctx = this.expressionHandler.createEvaluationContext(authentication, context.getMessage());
boolean granted = ExpressionUtils.evaluateAsBoolean(this.expression, ctx);
return new ExpressionAuthorizationDecision(granted, this.expression);
}
}
----
And specify an instance for each matcher that you cannot get migrate:
And specify an instance for each matcher that you cannot yet migrate:
[tabs]
======
@@ -252,7 +230,8 @@ public class WebSocketSecurityConfig {
public AuthorizationManager<Message<?>> messageAuthorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) {
messages
// ...
.simpSubscribeDestMatchers("/topic/friends/{friend}").access(new MessageExpressionAuthorizationManager("#friends == 'john"));
.simpSubscribeDestMatchers("/topic/friends/{friend}")
.access(new MessageExpressionAuthorizationManager("#friend == 'john'"));
// ...
return messages.build();
@@ -269,7 +248,8 @@ open class WebSocketSecurityConfig {
fun messageAuthorizationManager(messages: MessageMatcherDelegatingAuthorizationManager.Builder): AuthorizationManager<Message<?> {
messages
// ..
.simpSubscribeDestMatchers("/topic/friends/{friends}").access(MessageExpressionAuthorizationManager("#friends == 'john"))
.simpSubscribeDestMatchers("/topic/friends/{friend}")
.access(MessageExpressionAuthorizationManager("#friend == 'john'"))
// ...
return messages.build()
@@ -0,0 +1,137 @@
/*
* Copyright 2004-present 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
*
* https://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.messaging.access.expression;
import java.util.function.Supplier;
import org.jspecify.annotations.Nullable;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.security.access.expression.ExpressionUtils;
import org.springframework.security.access.expression.SecurityExpressionHandler;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.authorization.ExpressionAuthorizationDecision;
import org.springframework.security.core.Authentication;
import org.springframework.security.messaging.access.intercept.MessageAuthorizationContext;
import org.springframework.util.Assert;
/**
* An expression-based {@link AuthorizationManager} that determines the access by
* evaluating the provided expression.
*
* @since 7.1
*/
public final class MessageExpressionAuthorizationManager
implements AuthorizationManager<MessageAuthorizationContext<?>> {
private final SecurityExpressionHandler<MessageAuthorizationContext<?>> expressionHandler;
private final Expression expression;
/**
* Creates an instance.
* @param expressionString the raw expression string to parse
*/
public MessageExpressionAuthorizationManager(String expressionString) {
this(new MessageAuthorizationContextSecurityExpressionHandler(), expressionString);
}
private MessageExpressionAuthorizationManager(
SecurityExpressionHandler<MessageAuthorizationContext<?>> expressionHandler, String expressionString) {
Assert.notNull(expressionHandler, "expressionHandler cannot be null");
Assert.hasText(expressionString, "expressionString cannot be empty");
this.expressionHandler = expressionHandler;
this.expression = expressionHandler.getExpressionParser().parseExpression(expressionString);
}
/**
* Use a {@link MessageAuthorizationContextSecurityExpressionHandler} to create
* {@link MessageExpressionAuthorizationManager} instances.
* @return a {@link Builder} for constructing
* {@link MessageExpressionAuthorizationManager} instances
* @since 7.1
*/
public static Builder withDefaults() {
return new Builder(new MessageAuthorizationContextSecurityExpressionHandler());
}
/**
* Use this {@link SecurityExpressionHandler} to create
* {@link MessageExpressionAuthorizationManager} instances.
* @param expressionHandler the expression handler to use
* @return a {@link Builder} for constructing
* {@link MessageExpressionAuthorizationManager} instances
* @since 7.1
*/
public static Builder withSecurityExpressionHandler(
SecurityExpressionHandler<MessageAuthorizationContext<?>> expressionHandler) {
Assert.notNull(expressionHandler, "expressionHandler cannot be null");
return new Builder(expressionHandler);
}
/**
* Determines the access by evaluating the provided expression.
* @param authentication the {@link Supplier} of the {@link Authentication} to check
* @param context the {@link MessageAuthorizationContext} to check
* @return an {@link ExpressionAuthorizationDecision} based on the evaluated
* expression
*/
@Override
public AuthorizationResult authorize(Supplier<? extends @Nullable Authentication> authentication,
MessageAuthorizationContext<?> context) {
EvaluationContext ctx = this.expressionHandler.createEvaluationContext(authentication, context);
boolean granted = ExpressionUtils.evaluateAsBoolean(this.expression, ctx);
return new ExpressionAuthorizationDecision(granted, this.expression);
}
@Override
public String toString() {
return "MessageExpressionAuthorizationManager[expression='" + this.expression + "']";
}
/**
* A {@link Builder} for constructing {@link MessageExpressionAuthorizationManager}
* instances.
*
* <p>
* May be reused to create multiple instances.
*
* @since 7.1
*/
public static final class Builder {
private final SecurityExpressionHandler<MessageAuthorizationContext<?>> expressionHandler;
private Builder(SecurityExpressionHandler<MessageAuthorizationContext<?>> expressionHandler) {
this.expressionHandler = expressionHandler;
}
/**
* Create a {@link MessageExpressionAuthorizationManager} using this
* {@code expression}.
* @param expression the expression to evaluate
* @return the resulting {@link AuthorizationManager}
*/
public MessageExpressionAuthorizationManager expression(String expression) {
return new MessageExpressionAuthorizationManager(this.expressionHandler, expression);
}
}
}
@@ -0,0 +1,109 @@
/*
* Copyright 2004-present 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
*
* https://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.messaging.access.expression;
import org.junit.jupiter.api.Test;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.security.access.expression.SecurityExpressionHandler;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.messaging.access.intercept.MessageAuthorizationContext;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link MessageExpressionAuthorizationManager}.
*/
class MessageExpressionAuthorizationManagerTests {
@Test
void instantiateWhenExpressionStringNullThenIllegalArgumentException() {
assertThatIllegalArgumentException().isThrownBy(() -> new MessageExpressionAuthorizationManager(null))
.withMessage("expressionString cannot be empty");
}
@Test
void instantiateWhenExpressionStringEmptyThenIllegalArgumentException() {
assertThatIllegalArgumentException().isThrownBy(() -> new MessageExpressionAuthorizationManager(""))
.withMessage("expressionString cannot be empty");
}
@Test
void instantiateWhenExpressionStringBlankThenIllegalArgumentException() {
assertThatIllegalArgumentException().isThrownBy(() -> new MessageExpressionAuthorizationManager(" "))
.withMessage("expressionString cannot be empty");
}
@Test
void withSecurityExpressionHandlerWhenNullThenIllegalArgumentException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> MessageExpressionAuthorizationManager.withSecurityExpressionHandler(null))
.withMessage("expressionHandler cannot be null");
}
@Test
void instantiateWhenExpressionHandlerNotSetThenDefaultUsed() {
MessageExpressionAuthorizationManager manager = new MessageExpressionAuthorizationManager("hasRole('ADMIN')");
assertThat(manager).extracting("expressionHandler")
.isInstanceOf(MessageAuthorizationContextSecurityExpressionHandler.class);
}
@Test
void withSecurityExpressionHandlerWhenNotNullThenVerifyExpressionHandler() {
String expressionString = "hasRole('ADMIN')";
SecurityExpressionHandler<MessageAuthorizationContext<?>> expressionHandler = mock(
SecurityExpressionHandler.class);
ExpressionParser expressionParser = mock(ExpressionParser.class);
Expression expression = mock(Expression.class);
given(expressionHandler.getExpressionParser()).willReturn(expressionParser);
given(expressionParser.parseExpression(expressionString)).willReturn(expression);
MessageExpressionAuthorizationManager manager = MessageExpressionAuthorizationManager
.withSecurityExpressionHandler(expressionHandler)
.expression(expressionString);
assertThat(manager).extracting("expressionHandler").isEqualTo(expressionHandler);
assertThat(manager).extracting("expression").isEqualTo(expression);
verify(expressionParser).parseExpression(expressionString);
}
@Test
void authorizeWhenExpressionHasRoleAdminAndRoleAdminThenGrantedDecision() {
MessageExpressionAuthorizationManager manager = new MessageExpressionAuthorizationManager("hasRole('ADMIN')");
TestingAuthenticationToken authentication = new TestingAuthenticationToken("admin", "password", "ROLE_ADMIN");
AuthorizationResult result = manager.authorize(() -> authentication,
new MessageAuthorizationContext<>(new GenericMessage<>("message")));
assertThat(result).isNotNull();
assertThat(result.isGranted()).isTrue();
}
@Test
void authorizeWhenExpressionHasRoleAdminAndRoleUserThenDeniedDecision() {
MessageExpressionAuthorizationManager manager = new MessageExpressionAuthorizationManager("hasRole('ADMIN')");
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user", "password", "ROLE_USER");
AuthorizationResult result = manager.authorize(() -> authentication,
new MessageAuthorizationContext<>(new GenericMessage<>("message")));
assertThat(result).isNotNull();
assertThat(result.isGranted()).isFalse();
}
}