Use Spring Expression Language (SpEL) for complex authorization rules with custom security expressions and evaluation contexts.
Spring Security Messaging supports SpEL expressions for authorization decisions, allowing complex rules that go beyond simple role checks. Expression handlers create evaluation contexts that provide access to message properties, authentication details, and custom security methods.
Package: org.springframework.security.messaging.access.expression
Default implementation of SecurityExpressionHandler that uses MessageSecurityExpressionRoot for evaluating security expressions on messages.
/**
* Default SecurityExpressionHandler for Message-based security expressions.
* Creates evaluation contexts with MessageSecurityExpressionRoot.
*
* @param <T> the message payload type
* @since 4.0
*/
public class DefaultMessageSecurityExpressionHandler<T>
extends AbstractSecurityExpressionHandler<Message<T>> {
/**
* Creates evaluation context for the given authentication and message.
* The context includes MessageSecurityExpressionRoot with access to
* message properties and standard security expressions.
*
* @param authentication supplier of authentication to evaluate
* @param message the message being evaluated
* @return evaluation context for SpEL expressions
*/
public EvaluationContext createEvaluationContext(
Supplier<? extends Authentication> authentication,
Message<T> message
);
/**
* Sets the AuthenticationTrustResolver for checking authentication types.
* @deprecated since 7.0, use setAuthorizationManagerFactory instead
*
* @param trustResolver the trust resolver
*/
@Deprecated
public void setTrustResolver(AuthenticationTrustResolver trustResolver);
}Usage Example:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.expression.SecurityExpressionHandler;
import org.springframework.security.messaging.access.expression.DefaultMessageSecurityExpressionHandler;
import org.springframework.messaging.Message;
@Configuration
public class ExpressionConfig {
@Bean
public SecurityExpressionHandler<Message<?>> messageSecurityExpressionHandler() {
DefaultMessageSecurityExpressionHandler<Object> handler =
new DefaultMessageSecurityExpressionHandler<>();
// Customize if needed (e.g., add custom permission evaluator)
handler.setPermissionEvaluator(customPermissionEvaluator());
return handler;
}
private PermissionEvaluator customPermissionEvaluator() {
// Custom permission evaluator implementation
return new CustomPermissionEvaluator();
}
}Expression handler specifically for MessageAuthorizationContext, supporting path variable extraction. Wraps a delegate handler for Message expressions.
/**
* SecurityExpressionHandler for MessageAuthorizationContext that supports
* path variables extracted from destination patterns.
*
* @since 5.8
*/
public final class MessageAuthorizationContextSecurityExpressionHandler
implements SecurityExpressionHandler<MessageAuthorizationContext<?>> {
/**
* Creates handler with default DefaultMessageSecurityExpressionHandler.
*/
public MessageAuthorizationContextSecurityExpressionHandler();
/**
* Creates handler wrapping the specified delegate.
*
* @param expressionHandler delegate handler for Message expressions
*/
public MessageAuthorizationContextSecurityExpressionHandler(
SecurityExpressionHandler<Message<?>> expressionHandler
);
/**
* Returns the expression parser used for parsing SpEL expressions.
*
* @return the expression parser
*/
public ExpressionParser getExpressionParser();
/**
* Creates evaluation context for the given authentication and context.
* The context includes access to both message properties and path variables.
*
* @param authentication the authentication to evaluate
* @param context the message authorization context
* @return evaluation context for SpEL expressions
*/
public EvaluationContext createEvaluationContext(
Authentication authentication,
MessageAuthorizationContext<?> context
);
/**
* Creates evaluation context with authentication supplier.
*
* @param authentication supplier of authentication to evaluate
* @param context the message authorization context
* @return evaluation context for SpEL expressions
*/
public EvaluationContext createEvaluationContext(
Supplier<? extends Authentication> authentication,
MessageAuthorizationContext<?> context
);
}Usage Example:
import org.springframework.security.access.expression.SecurityExpressionHandler;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.messaging.access.expression.DefaultMessageSecurityExpressionHandler;
import org.springframework.security.messaging.access.expression.MessageAuthorizationContextSecurityExpressionHandler;
import org.springframework.security.messaging.access.intercept.MessageAuthorizationContext;
import java.util.function.Supplier;
public class CustomExpressionAuthorizationManager
implements AuthorizationManager<MessageAuthorizationContext<?>> {
private final MessageAuthorizationContextSecurityExpressionHandler expressionHandler;
private final Expression expression;
public CustomExpressionAuthorizationManager(String expressionString) {
DefaultMessageSecurityExpressionHandler<Object> delegate =
new DefaultMessageSecurityExpressionHandler<>();
this.expressionHandler =
new MessageAuthorizationContextSecurityExpressionHandler(delegate);
this.expression = expressionHandler
.getExpressionParser()
.parseExpression(expressionString);
}
@Override
public AuthorizationDecision check(
Supplier<Authentication> authentication,
MessageAuthorizationContext<?> context
) {
EvaluationContext evalContext =
expressionHandler.createEvaluationContext(authentication, context);
boolean granted = expression.getValue(evalContext, Boolean.class);
return new AuthorizationDecision(granted);
}
}
// Usage with path variables
MessageMatcherDelegatingAuthorizationManager authManager =
MessageMatcherDelegatingAuthorizationManager.builder()
// Expression can access path variables via #variables
.simpDestMatchers("/app/user/{userId}/**")
.access(new CustomExpressionAuthorizationManager(
"#variables['userId'] == authentication.name"
))
.build();The root object for message security expressions, providing access to message properties and standard security expression methods.
/**
* Root object for message security SpEL expressions.
* Provides access to message properties and standard security checks.
*
* @param <T> the message payload type
* @since 4.0
*/
public class MessageSecurityExpressionRoot extends SecurityExpressionRoot<Message<T>> {
/**
* The message being evaluated (accessible in expressions as 'message').
*/
public final Message<T> message;
/**
* Creates root with authentication and message.
*
* @param authentication the authentication
* @param message the message
*/
public MessageSecurityExpressionRoot(Authentication authentication, Message<T> message);
/**
* Creates root with authentication supplier and message.
*
* @param authentication supplier of authentication
* @param message the message
* @since 5.8
*/
public MessageSecurityExpressionRoot(
Supplier<? extends Authentication> authentication,
Message<T> message
);
}The expression root provides access to standard security expression methods inherited from SecurityExpressionRoot:
hasRole(String role) - Check if user has rolehasAnyRole(String... roles) - Check if user has any rolehasAuthority(String authority) - Check if user has authorityhasAnyAuthority(String... authorities) - Check if user has any authorityprincipal - The authentication principalauthentication - The Authentication objectpermitAll - Always returns truedenyAll - Always returns falseisAnonymous() - Check if user is anonymousisAuthenticated() - Check if user is authenticatedisRememberMe() - Check if user authenticated via remember-meisFullyAuthenticated() - Check if user is fully authenticated (not remember-me)import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.messaging.access.expression.MessageAuthorizationContextSecurityExpressionHandler;
import org.springframework.security.messaging.access.intercept.MessageAuthorizationContext;
import org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager;
// Custom expression-based authorization manager
AuthorizationManager<MessageAuthorizationContext<?>> expressionAuthManager =
new ExpressionBasedAuthorizationManager(
"hasRole('ADMIN') or #variables['userId'] == authentication.name"
);
MessageMatcherDelegatingAuthorizationManager authManager =
MessageMatcherDelegatingAuthorizationManager.builder()
.simpDestMatchers("/app/user/{userId}/**").access(expressionAuthManager)
.build();// User can only access their own user ID
.simpDestMatchers("/app/user/{userId}/**")
.access(expressionAuthManager("#variables['userId'] == authentication.name"))// Admin or owner can access
.simpDestMatchers("/app/resource/{ownerId}/**")
.access(expressionAuthManager(
"hasRole('ADMIN') or #variables['ownerId'] == authentication.name"
))// Check multiple variables
.simpDestMatchers("/app/team/{teamId}/member/{memberId}/**")
.access(expressionAuthManager(
"#variables['memberId'] == authentication.name and " +
"hasPermission(#variables['teamId'], 'TEAM', 'READ')"
))// Access message headers in expression
.simpDestMatchers("/app/custom/**")
.access(expressionAuthManager(
"message.headers['customHeader'] == 'expectedValue'"
))// Call custom bean methods
.simpDestMatchers("/app/document/{docId}/**")
.access(expressionAuthManager(
"@documentService.canAccess(authentication.name, #variables['docId'])"
))import org.springframework.security.access.expression.AbstractSecurityExpressionHandler;
import org.springframework.security.access.expression.SecurityExpressionRoot;
import org.springframework.security.core.Authentication;
import org.springframework.messaging.Message;
public class CustomMessageSecurityExpressionRoot<T>
extends MessageSecurityExpressionRoot<T> {
public CustomMessageSecurityExpressionRoot(
Authentication authentication,
Message<T> message
) {
super(authentication, message);
}
/**
* Custom expression method: check if user belongs to tenant
*/
public boolean belongsToTenant(String tenantId) {
// Custom logic to check tenant membership
Object principal = getAuthentication().getPrincipal();
if (principal instanceof CustomUserDetails) {
return ((CustomUserDetails) principal).getTenantId().equals(tenantId);
}
return false;
}
/**
* Custom expression method: check if user can access resource
*/
public boolean canAccessResource(String resourceId) {
// Custom authorization logic
return checkResourceAccess(getAuthentication(), resourceId);
}
private boolean checkResourceAccess(Authentication auth, String resourceId) {
// Implementation
return true;
}
}
public class CustomMessageSecurityExpressionHandler<T>
extends DefaultMessageSecurityExpressionHandler<T> {
@Override
protected SecurityExpressionRoot createSecurityExpressionRoot(
Authentication authentication,
Message<T> invocation
) {
CustomMessageSecurityExpressionRoot<T> root =
new CustomMessageSecurityExpressionRoot<>(authentication, invocation);
root.setPermissionEvaluator(getPermissionEvaluator());
root.setTrustResolver(getTrustResolver());
root.setRoleHierarchy(getRoleHierarchy());
return root;
}
}
// Configure custom handler
@Configuration
public class CustomExpressionConfig {
@Bean
public SecurityExpressionHandler<Message<?>> messageSecurityExpressionHandler() {
return new CustomMessageSecurityExpressionHandler<>();
}
}
// Use custom methods in expressions
MessageMatcherDelegatingAuthorizationManager authManager =
MessageMatcherDelegatingAuthorizationManager.builder()
.simpDestMatchers("/app/tenant/{tenantId}/**")
.access(expressionAuthManager("belongsToTenant(#variables['tenantId'])"))
.build();@Service("messageAccessChecker")
public class MessageAccessChecker {
public boolean canAccessDestination(String username, String destination) {
// Custom logic
return checkAccess(username, destination);
}
public boolean hasTeamAccess(String username, String teamId) {
// Custom logic
return checkTeamMembership(username, teamId);
}
private boolean checkAccess(String username, String destination) {
// Implementation
return true;
}
private boolean checkTeamMembership(String username, String teamId) {
// Implementation
return true;
}
}
// Use in expressions with @beanName syntax
MessageMatcherDelegatingAuthorizationManager authManager =
MessageMatcherDelegatingAuthorizationManager.builder()
.simpDestMatchers("/app/team/{teamId}/**")
.access(expressionAuthManager(
"@messageAccessChecker.hasTeamAccess(" +
"authentication.name, #variables['teamId'])"
))
.build();When using MessageAuthorizationContextSecurityExpressionHandler:
authentication - The Authentication objectprincipal - The authentication principalmessage - The Message<?> being evaluated#variables - Map of extracted path variables#root - The MessageSecurityExpressionRootExample:
// Access variables
"#variables['userId'] == authentication.name"
// Access message headers
"message.headers['customHeader'] == 'value'"
// Access principal properties
"principal.email == 'admin@example.com'"
// Combine multiple conditions
"hasRole('USER') and #variables['ownerId'] == authentication.name"import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.expression.SecurityExpressionHandler;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.messaging.access.expression.DefaultMessageSecurityExpressionHandler;
import org.springframework.security.messaging.access.expression.MessageAuthorizationContextSecurityExpressionHandler;
import org.springframework.security.messaging.access.intercept.MessageAuthorizationContext;
import org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager;
import org.springframework.expression.Expression;
import org.springframework.expression.EvaluationContext;
import java.util.function.Supplier;
@Configuration
public class ExpressionBasedSecurityConfig {
@Bean
public MessageMatcherDelegatingAuthorizationManager authorizationManager() {
return MessageMatcherDelegatingAuthorizationManager.builder()
// Simple role check
.simpDestMatchers("/app/admin/**").hasRole("ADMIN")
// Expression: user can access own resources
.simpDestMatchers("/app/user/{userId}/**")
.access(expressionAuthManager(
"#variables['userId'] == authentication.name"
))
// Expression: admin or owner can access
.simpDestMatchers("/app/document/{ownerId}/**")
.access(expressionAuthManager(
"hasRole('ADMIN') or #variables['ownerId'] == authentication.name"
))
// Expression: complex business logic via bean
.simpDestMatchers("/app/team/{teamId}/**")
.access(expressionAuthManager(
"@teamService.isMember(authentication.name, #variables['teamId'])"
))
// Expression: check message header
.simpDestMatchers("/app/custom/**")
.access(expressionAuthManager(
"hasRole('USER') and message.headers['version'] == '2.0'"
))
.anyMessage().denyAll()
.build();
}
private AuthorizationManager<MessageAuthorizationContext<?>> expressionAuthManager(
String expressionString
) {
return new ExpressionAuthorizationManager(expressionString);
}
private static class ExpressionAuthorizationManager
implements AuthorizationManager<MessageAuthorizationContext<?>> {
private final MessageAuthorizationContextSecurityExpressionHandler expressionHandler;
private final Expression expression;
public ExpressionAuthorizationManager(String expressionString) {
DefaultMessageSecurityExpressionHandler<Object> delegate =
new DefaultMessageSecurityExpressionHandler<>();
this.expressionHandler =
new MessageAuthorizationContextSecurityExpressionHandler(delegate);
this.expression = expressionHandler
.getExpressionParser()
.parseExpression(expressionString);
}
@Override
public AuthorizationDecision check(
Supplier<Authentication> authentication,
MessageAuthorizationContext<?> context
) {
EvaluationContext evalContext =
expressionHandler.createEvaluationContext(authentication, context);
Boolean granted = expression.getValue(evalContext, Boolean.class);
return new AuthorizationDecision(Boolean.TRUE.equals(granted));
}
}
}#variables['name']?.) when accessing propertiesEnable debug logging to see expression evaluation:
logging.level.org.springframework.security.messaging.access.expression=DEBUGThis will log:
Expressions may throw exceptions during evaluation. Always handle these gracefully:
public class SafeExpressionAuthorizationManager
implements AuthorizationManager<MessageAuthorizationContext<?>> {
private final Expression expression;
private final MessageAuthorizationContextSecurityExpressionHandler handler;
@Override
public AuthorizationDecision check(
Supplier<Authentication> authentication,
MessageAuthorizationContext<?> context
) {
try {
EvaluationContext evalContext = handler.createEvaluationContext(authentication, context);
Boolean result = expression.getValue(evalContext, Boolean.class);
return new AuthorizationDecision(Boolean.TRUE.equals(result));
} catch (EvaluationException e) {
logger.error("Expression evaluation failed", e);
// Deny access on evaluation failure (fail-secure)
return new AuthorizationDecision(false);
} catch (Exception e) {
logger.error("Unexpected error during expression evaluation", e);
return new AuthorizationDecision(false);
}
}
}Always handle null values in expressions:
// Safe: Check for null before accessing
"#variables['userId'] != null and #variables['userId'] == authentication.name"
// Safe: Use safe navigation
"authentication?.name == #variables['userId']"
// Unsafe: May throw NullPointerException
"#variables['userId'] == authentication.name" // If userId is null, may failNullPointerException: Accessing properties on null objects
?.)SpELParseException: Invalid expression syntax
EvaluationException: Runtime evaluation errors
Causes:
Solutions:
// Add null checks
"#variables['userId'] != null and #variables['userId'] == authentication.name"
// Verify variable extraction
logger.debug("Variables: {}", context.getVariables());
// Test expression in isolation
Expression testExpr = parser.parseExpression("#variables['userId'] == authentication.name");Symptom: @beanName method calls fail
Causes:
Solutions:
// Ensure bean is registered
@Service("myService")
public class MyService { ... }
// Use correct bean name
"@myService.canAccess(authentication.name, #variables['id'])"
// Verify bean resolver is configured
handler.setBeanResolver(applicationContext);Symptom: #variables map is empty
Causes:
Solutions:
// Use path variable syntax
.simpDestMatchers("/app/user/{userId}/**") // Correct
// Not: .simpDestMatchers("/app/user/*/**") // Wrong - no variable extraction