20 changed files with 12 additions and 2005 deletions
@ -1,43 +0,0 @@ |
|||||||
/* |
|
||||||
* Copyright 2002-2022 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.config.annotation.web.configuration; |
|
||||||
|
|
||||||
import org.springframework.context.annotation.ImportSelector; |
|
||||||
import org.springframework.core.type.AnnotationMetadata; |
|
||||||
import org.springframework.util.ClassUtils; |
|
||||||
|
|
||||||
/** |
|
||||||
* Used by {@link EnableWebSecurity} to conditionally import
|
|
||||||
* {@link org.springframework.security.config.annotation.web.socket.WebSocketMessageBrokerSecurityConfiguration} |
|
||||||
* when the AbstractWebSocketHandler is present on the classpath. |
|
||||||
* |
|
||||||
* @author Josh Cummings |
|
||||||
* @since 5.7 |
|
||||||
*/ |
|
||||||
class SpringWebSocketImportSelector implements ImportSelector { |
|
||||||
|
|
||||||
@Override |
|
||||||
public String[] selectImports(AnnotationMetadata importingClassMetadata) { |
|
||||||
if (!ClassUtils.isPresent("org.springframework.web.socket.handler.AbstractWebSocketHandler", |
|
||||||
getClass().getClassLoader())) { |
|
||||||
return new String[0]; |
|
||||||
} |
|
||||||
return new String[] { |
|
||||||
"org.springframework.security.config.annotation.web.socket.WebSocketMessageBrokerSecurityConfiguration" }; |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
@ -1,354 +0,0 @@ |
|||||||
/* |
|
||||||
* Copyright 2002-2022 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.config.annotation.web.messaging; |
|
||||||
|
|
||||||
import java.util.ArrayList; |
|
||||||
import java.util.List; |
|
||||||
import java.util.function.Supplier; |
|
||||||
|
|
||||||
import org.springframework.context.ApplicationContext; |
|
||||||
import org.springframework.messaging.Message; |
|
||||||
import org.springframework.messaging.simp.SimpMessageType; |
|
||||||
import org.springframework.messaging.simp.annotation.support.SimpAnnotationMethodMessageHandler; |
|
||||||
import org.springframework.security.authorization.AuthenticatedAuthorizationManager; |
|
||||||
import org.springframework.security.authorization.AuthorityAuthorizationManager; |
|
||||||
import org.springframework.security.authorization.AuthorizationDecision; |
|
||||||
import org.springframework.security.authorization.AuthorizationManager; |
|
||||||
import org.springframework.security.messaging.access.intercept.MessageAuthorizationContext; |
|
||||||
import org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager; |
|
||||||
import org.springframework.security.messaging.util.matcher.MessageMatcher; |
|
||||||
import org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher; |
|
||||||
import org.springframework.security.messaging.util.matcher.SimpMessageTypeMatcher; |
|
||||||
import org.springframework.util.AntPathMatcher; |
|
||||||
import org.springframework.util.Assert; |
|
||||||
import org.springframework.util.PathMatcher; |
|
||||||
|
|
||||||
/** |
|
||||||
* Allows mapping security constraints using {@link MessageMatcher} to authorization |
|
||||||
* managers. |
|
||||||
* |
|
||||||
* @author Josh Cummings |
|
||||||
* @since 5.7 |
|
||||||
*/ |
|
||||||
public final class AuthorizationManagerMessageMatcherRegistry { |
|
||||||
|
|
||||||
private final MessageMatcherDelegatingAuthorizationManager.Builder builder = MessageMatcherDelegatingAuthorizationManager |
|
||||||
.builder(); |
|
||||||
|
|
||||||
private final ApplicationContext context; |
|
||||||
|
|
||||||
private PathMatcher pathMatcher; |
|
||||||
|
|
||||||
public AuthorizationManagerMessageMatcherRegistry(ApplicationContext context) { |
|
||||||
this.context = context; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Maps any {@link Message} to a security expression. |
|
||||||
* @return the Expression to associate |
|
||||||
*/ |
|
||||||
public AuthorizationManagerMessageMatcherRegistry.Constraint anyMessage() { |
|
||||||
return matchers(MessageMatcher.ANY_MESSAGE); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Maps any {@link Message} that has a null SimpMessageHeaderAccessor destination |
|
||||||
* header (i.e. CONNECT, CONNECT_ACK, HEARTBEAT, UNSUBSCRIBE, DISCONNECT, |
|
||||||
* DISCONNECT_ACK, OTHER) |
|
||||||
* @return the Expression to associate |
|
||||||
*/ |
|
||||||
public AuthorizationManagerMessageMatcherRegistry.Constraint nullDestMatcher() { |
|
||||||
return matchers(SimpDestinationMessageMatcher.NULL_DESTINATION_MATCHER); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Maps a {@link List} of {@link SimpDestinationMessageMatcher} instances. |
|
||||||
* @param typesToMatch the {@link SimpMessageType} instance to match on |
|
||||||
* @return the {@link Constraint} associated to the matchers. |
|
||||||
*/ |
|
||||||
public AuthorizationManagerMessageMatcherRegistry.Constraint simpTypeMatchers(SimpMessageType... typesToMatch) { |
|
||||||
MessageMatcher<?>[] typeMatchers = new MessageMatcher<?>[typesToMatch.length]; |
|
||||||
for (int i = 0; i < typesToMatch.length; i++) { |
|
||||||
SimpMessageType typeToMatch = typesToMatch[i]; |
|
||||||
typeMatchers[i] = new SimpMessageTypeMatcher(typeToMatch); |
|
||||||
} |
|
||||||
return matchers(typeMatchers); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Maps a {@link List} of {@link SimpDestinationMessageMatcher} instances without |
|
||||||
* regard to the {@link SimpMessageType}. If no destination is found on the Message, |
|
||||||
* then the Matcher returns false. |
|
||||||
* @param patterns the patterns to create |
|
||||||
* {@link org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher} |
|
||||||
* from. |
|
||||||
*/ |
|
||||||
public Constraint simpDestMatchers(String... patterns) { |
|
||||||
return simpDestMatchers(null, patterns); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Maps a {@link List} of {@link SimpDestinationMessageMatcher} instances that match |
|
||||||
* on {@code SimpMessageType.MESSAGE}. If no destination is found on the Message, then |
|
||||||
* the Matcher returns false. |
|
||||||
* @param patterns the patterns to create |
|
||||||
* {@link org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher} |
|
||||||
* from. |
|
||||||
*/ |
|
||||||
public Constraint simpMessageDestMatchers(String... patterns) { |
|
||||||
return simpDestMatchers(SimpMessageType.MESSAGE, patterns); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Maps a {@link List} of {@link SimpDestinationMessageMatcher} instances that match |
|
||||||
* on {@code SimpMessageType.SUBSCRIBE}. If no destination is found on the Message, |
|
||||||
* then the Matcher returns false. |
|
||||||
* @param patterns the patterns to create |
|
||||||
* {@link org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher} |
|
||||||
* from. |
|
||||||
*/ |
|
||||||
public Constraint simpSubscribeDestMatchers(String... patterns) { |
|
||||||
return simpDestMatchers(SimpMessageType.SUBSCRIBE, patterns); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Maps a {@link List} of {@link SimpDestinationMessageMatcher} instances. If no |
|
||||||
* destination is found on the Message, then the Matcher returns false. |
|
||||||
* @param type the {@link SimpMessageType} to match on. If null, the |
|
||||||
* {@link SimpMessageType} is not considered for matching. |
|
||||||
* @param patterns the patterns to create |
|
||||||
* {@link org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher} |
|
||||||
* from. |
|
||||||
* @return the {@link Constraint} that is associated to the {@link MessageMatcher} |
|
||||||
*/ |
|
||||||
private Constraint simpDestMatchers(SimpMessageType type, String... patterns) { |
|
||||||
List<MessageMatcher<?>> matchers = new ArrayList<>(patterns.length); |
|
||||||
for (String pattern : patterns) { |
|
||||||
Supplier<MessageMatcher<Object>> supplier = new PathMatcherMessageMatcherBuilder(pattern, type); |
|
||||||
MessageMatcher<?> matcher = new SupplierMessageMatcher(supplier); |
|
||||||
matchers.add(matcher); |
|
||||||
} |
|
||||||
return new Constraint(matchers); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* The {@link PathMatcher} to be used with the |
|
||||||
* {@link MessageSecurityMetadataSourceRegistry#simpDestMatchers(String...)}. The |
|
||||||
* default is to use the default constructor of {@link AntPathMatcher}. |
|
||||||
* @param pathMatcher the {@link PathMatcher} to use. Cannot be null. |
|
||||||
* @return the {@link MessageSecurityMetadataSourceRegistry} for further |
|
||||||
* customization. |
|
||||||
*/ |
|
||||||
public AuthorizationManagerMessageMatcherRegistry simpDestPathMatcher(PathMatcher pathMatcher) { |
|
||||||
Assert.notNull(pathMatcher, "pathMatcher cannot be null"); |
|
||||||
this.pathMatcher = pathMatcher; |
|
||||||
return this; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Maps a {@link List} of {@link MessageMatcher} instances to a security expression. |
|
||||||
* @param matchers the {@link MessageMatcher} instances to map. |
|
||||||
* @return The {@link Constraint} that is associated to the {@link MessageMatcher} |
|
||||||
* instances |
|
||||||
*/ |
|
||||||
public Constraint matchers(MessageMatcher<?>... matchers) { |
|
||||||
List<MessageMatcher<?>> builders = new ArrayList<>(matchers.length); |
|
||||||
for (MessageMatcher<?> matcher : matchers) { |
|
||||||
builders.add(matcher); |
|
||||||
} |
|
||||||
return new Constraint(builders); |
|
||||||
} |
|
||||||
|
|
||||||
public AuthorizationManager<Message<?>> build() { |
|
||||||
return this.builder.build(); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Represents the security constraint to be applied to the {@link MessageMatcher} |
|
||||||
* instances. |
|
||||||
*/ |
|
||||||
public final class Constraint { |
|
||||||
|
|
||||||
private final List<? extends MessageMatcher<?>> messageMatchers; |
|
||||||
|
|
||||||
/** |
|
||||||
* Creates a new instance |
|
||||||
* @param messageMatchers the {@link MessageMatcher} instances to map to this |
|
||||||
* constraint |
|
||||||
*/ |
|
||||||
private Constraint(List<? extends MessageMatcher<?>> messageMatchers) { |
|
||||||
Assert.notEmpty(messageMatchers, "messageMatchers cannot be null or empty"); |
|
||||||
this.messageMatchers = messageMatchers; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Shortcut for specifying {@link Message} instances require a particular role. If |
|
||||||
* you do not want to have "ROLE_" automatically inserted see |
|
||||||
* {@link #hasAuthority(String)}. |
|
||||||
* @param role the role to require (i.e. USER, ADMIN, etc). Note, it should not |
|
||||||
* start with "ROLE_" as this is automatically inserted. |
|
||||||
* @return the {@link MessageMatcherDelegatingAuthorizationManager.Builder} for |
|
||||||
* further customization |
|
||||||
*/ |
|
||||||
public AuthorizationManagerMessageMatcherRegistry hasRole(String role) { |
|
||||||
return access(AuthorityAuthorizationManager.hasRole(role)); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Shortcut for specifying {@link Message} instances require any of a number of |
|
||||||
* roles. If you do not want to have "ROLE_" automatically inserted see |
|
||||||
* {@link #hasAnyAuthority(String...)} |
|
||||||
* @param roles the roles to require (i.e. USER, ADMIN, etc). Note, it should not |
|
||||||
* start with "ROLE_" as this is automatically inserted. |
|
||||||
* @return the {@link MessageMatcherDelegatingAuthorizationManager.Builder} for |
|
||||||
* further customization |
|
||||||
*/ |
|
||||||
public AuthorizationManagerMessageMatcherRegistry hasAnyRole(String... roles) { |
|
||||||
return access(AuthorityAuthorizationManager.hasAnyRole(roles)); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Specify that {@link Message} instances require a particular authority. |
|
||||||
* @param authority the authority to require (i.e. ROLE_USER, ROLE_ADMIN, etc). |
|
||||||
* @return the {@link MessageMatcherDelegatingAuthorizationManager.Builder} for |
|
||||||
* further customization |
|
||||||
*/ |
|
||||||
public AuthorizationManagerMessageMatcherRegistry hasAuthority(String authority) { |
|
||||||
return access(AuthorityAuthorizationManager.hasAuthority(authority)); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Specify that {@link Message} instances requires any of a number authorities. |
|
||||||
* @param authorities the requests require at least one of the authorities (i.e. |
|
||||||
* "ROLE_USER","ROLE_ADMIN" would mean either "ROLE_USER" or "ROLE_ADMIN" is |
|
||||||
* required). |
|
||||||
* @return the {@link MessageMatcherDelegatingAuthorizationManager.Builder} for |
|
||||||
* further customization |
|
||||||
*/ |
|
||||||
public AuthorizationManagerMessageMatcherRegistry hasAnyAuthority(String... authorities) { |
|
||||||
return access(AuthorityAuthorizationManager.hasAnyAuthority(authorities)); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Specify that Messages are allowed by anyone. |
|
||||||
* @return the {@link MessageMatcherDelegatingAuthorizationManager.Builder} for |
|
||||||
* further customization |
|
||||||
*/ |
|
||||||
public AuthorizationManagerMessageMatcherRegistry permitAll() { |
|
||||||
return access((authentication, context) -> new AuthorizationDecision(true)); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Specify that Messages are not allowed by anyone. |
|
||||||
* @return the {@link MessageMatcherDelegatingAuthorizationManager.Builder} for |
|
||||||
* further customization |
|
||||||
*/ |
|
||||||
public AuthorizationManagerMessageMatcherRegistry denyAll() { |
|
||||||
return access((authorization, context) -> new AuthorizationDecision(false)); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Specify that Messages are allowed by any authenticated user. |
|
||||||
* @return the {@link MessageMatcherDelegatingAuthorizationManager.Builder} for |
|
||||||
* further customization |
|
||||||
*/ |
|
||||||
public AuthorizationManagerMessageMatcherRegistry authenticated() { |
|
||||||
return access(AuthenticatedAuthorizationManager.authenticated()); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Allows specifying that Messages are secured by an arbitrary expression |
|
||||||
* @param authorizationManager the {@link AuthorizationManager} to secure the |
|
||||||
* destinations |
|
||||||
* @return the {@link MessageMatcherDelegatingAuthorizationManager.Builder} for |
|
||||||
* further customization |
|
||||||
*/ |
|
||||||
public AuthorizationManagerMessageMatcherRegistry access( |
|
||||||
AuthorizationManager<MessageAuthorizationContext<?>> authorizationManager) { |
|
||||||
for (MessageMatcher<?> messageMatcher : this.messageMatchers) { |
|
||||||
AuthorizationManagerMessageMatcherRegistry.this.builder.add(messageMatcher, authorizationManager); |
|
||||||
} |
|
||||||
return AuthorizationManagerMessageMatcherRegistry.this; |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
private static final class SupplierMessageMatcher implements MessageMatcher<Object> { |
|
||||||
|
|
||||||
private final Supplier<MessageMatcher<Object>> supplier; |
|
||||||
|
|
||||||
private volatile MessageMatcher<Object> delegate; |
|
||||||
|
|
||||||
SupplierMessageMatcher(Supplier<MessageMatcher<Object>> supplier) { |
|
||||||
this.supplier = supplier; |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public boolean matches(Message<?> message) { |
|
||||||
if (this.delegate == null) { |
|
||||||
synchronized (this.supplier) { |
|
||||||
if (this.delegate == null) { |
|
||||||
this.delegate = this.supplier.get(); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
return this.delegate.matches(message); |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
private final class PathMatcherMessageMatcherBuilder implements Supplier<MessageMatcher<Object>> { |
|
||||||
|
|
||||||
private final String pattern; |
|
||||||
|
|
||||||
private final SimpMessageType type; |
|
||||||
|
|
||||||
private PathMatcherMessageMatcherBuilder(String pattern, SimpMessageType type) { |
|
||||||
this.pattern = pattern; |
|
||||||
this.type = type; |
|
||||||
} |
|
||||||
|
|
||||||
private PathMatcher resolvePathMatcher() { |
|
||||||
if (AuthorizationManagerMessageMatcherRegistry.this.pathMatcher != null) { |
|
||||||
return AuthorizationManagerMessageMatcherRegistry.this.pathMatcher; |
|
||||||
} |
|
||||||
if (AuthorizationManagerMessageMatcherRegistry.this.context |
|
||||||
.getBeanNamesForType(SimpAnnotationMethodMessageHandler.class).length > 0) { |
|
||||||
return AuthorizationManagerMessageMatcherRegistry.this.context |
|
||||||
.getBean(SimpAnnotationMethodMessageHandler.class).getPathMatcher(); |
|
||||||
} |
|
||||||
return new AntPathMatcher(); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public MessageMatcher<Object> get() { |
|
||||||
PathMatcher pathMatcher = resolvePathMatcher(); |
|
||||||
if (this.type == null) { |
|
||||||
return new SimpDestinationMessageMatcher(this.pattern, pathMatcher); |
|
||||||
} |
|
||||||
if (SimpMessageType.MESSAGE == this.type) { |
|
||||||
return SimpDestinationMessageMatcher.createMessageMatcher(this.pattern, pathMatcher); |
|
||||||
} |
|
||||||
if (SimpMessageType.SUBSCRIBE == this.type) { |
|
||||||
return SimpDestinationMessageMatcher.createSubscribeMatcher(this.pattern, pathMatcher); |
|
||||||
} |
|
||||||
throw new IllegalStateException(this.type + " is not supported since it does not have a destination"); |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
@ -1,34 +0,0 @@ |
|||||||
/* |
|
||||||
* Copyright 2002-2022 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.config.annotation.web.socket; |
|
||||||
|
|
||||||
import org.springframework.context.ApplicationContext; |
|
||||||
import org.springframework.context.annotation.Bean; |
|
||||||
import org.springframework.context.annotation.Configuration; |
|
||||||
import org.springframework.context.annotation.Scope; |
|
||||||
import org.springframework.security.config.annotation.web.messaging.AuthorizationManagerMessageMatcherRegistry; |
|
||||||
|
|
||||||
@Configuration(proxyBeanMethods = false) |
|
||||||
final class MessageMatcherAuthorizationManagerConfiguration { |
|
||||||
|
|
||||||
@Bean |
|
||||||
@Scope("prototype") |
|
||||||
AuthorizationManagerMessageMatcherRegistry authorizationManagerMessageMatcherRegistry(ApplicationContext context) { |
|
||||||
return new AuthorizationManagerMessageMatcherRegistry(context); |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
@ -1,211 +0,0 @@ |
|||||||
/* |
|
||||||
* Copyright 2002-2022 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.config.annotation.web.socket; |
|
||||||
|
|
||||||
import java.util.ArrayList; |
|
||||||
import java.util.Arrays; |
|
||||||
import java.util.List; |
|
||||||
import java.util.Map; |
|
||||||
import java.util.function.Consumer; |
|
||||||
|
|
||||||
import org.springframework.beans.factory.SmartInitializingSingleton; |
|
||||||
import org.springframework.beans.factory.annotation.Autowired; |
|
||||||
import org.springframework.context.ApplicationContext; |
|
||||||
import org.springframework.context.annotation.Import; |
|
||||||
import org.springframework.core.Ordered; |
|
||||||
import org.springframework.core.annotation.Order; |
|
||||||
import org.springframework.messaging.Message; |
|
||||||
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; |
|
||||||
import org.springframework.messaging.simp.config.ChannelRegistration; |
|
||||||
import org.springframework.messaging.support.ChannelInterceptor; |
|
||||||
import org.springframework.security.authorization.AuthenticatedAuthorizationManager; |
|
||||||
import org.springframework.security.authorization.AuthorizationManager; |
|
||||||
import org.springframework.security.authorization.SpringAuthorizationEventPublisher; |
|
||||||
import org.springframework.security.messaging.access.intercept.AuthorizationChannelInterceptor; |
|
||||||
import org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager; |
|
||||||
import org.springframework.security.messaging.context.AuthenticationPrincipalArgumentResolver; |
|
||||||
import org.springframework.security.messaging.context.SecurityContextChannelInterceptor; |
|
||||||
import org.springframework.security.messaging.util.matcher.MessageMatcher; |
|
||||||
import org.springframework.security.messaging.web.csrf.CsrfChannelInterceptor; |
|
||||||
import org.springframework.security.messaging.web.socket.server.CsrfTokenHandshakeInterceptor; |
|
||||||
import org.springframework.util.Assert; |
|
||||||
import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; |
|
||||||
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; |
|
||||||
import org.springframework.web.socket.server.HandshakeInterceptor; |
|
||||||
import org.springframework.web.socket.server.support.WebSocketHttpRequestHandler; |
|
||||||
import org.springframework.web.socket.sockjs.SockJsService; |
|
||||||
import org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler; |
|
||||||
import org.springframework.web.socket.sockjs.transport.TransportHandlingSockJsService; |
|
||||||
|
|
||||||
/** |
|
||||||
* Allows configuring WebSocket Authorization. |
|
||||||
* |
|
||||||
* <p> |
|
||||||
* For example: |
|
||||||
* </p> |
|
||||||
* |
|
||||||
* <pre> |
|
||||||
* @Configuration |
|
||||||
* @EnableWebSecurity |
|
||||||
* public class WebSocketSecurityConfig { |
|
||||||
* |
|
||||||
* @Bean |
|
||||||
* AuthorizationManager<Message<?>> (AuthorizationManagerMessageMatcherRegistry messages) { |
|
||||||
* messages.simpDestMatchers("/user/queue/errors").permitAll() |
|
||||||
* .simpDestMatchers("/admin/**").hasRole("ADMIN").anyMessage() |
|
||||||
* .authenticated(); |
|
||||||
* |
|
||||||
* return messages.build(); |
|
||||||
* } |
|
||||||
* } |
|
||||||
* </pre> |
|
||||||
* |
|
||||||
* @author Josh Cummings |
|
||||||
* @since 5.7 |
|
||||||
*/ |
|
||||||
@Order(Ordered.HIGHEST_PRECEDENCE + 100) |
|
||||||
@Import(MessageMatcherAuthorizationManagerConfiguration.class) |
|
||||||
final class WebSocketMessageBrokerSecurityConfiguration |
|
||||||
implements WebSocketMessageBrokerConfigurer, SmartInitializingSingleton { |
|
||||||
|
|
||||||
private static final String SIMPLE_URL_HANDLER_MAPPING_BEAN_NAME = "stompWebSocketHandlerMapping"; |
|
||||||
|
|
||||||
private static final AuthorizationManager<Message<?>> ANY_MESSAGE_AUTHENTICATED = MessageMatcherDelegatingAuthorizationManager |
|
||||||
.builder().add(MessageMatcher.ANY_MESSAGE, AuthenticatedAuthorizationManager.authenticated()).build(); |
|
||||||
|
|
||||||
private ChannelInterceptor securityContextChannelInterceptor = new SecurityContextChannelInterceptor(); |
|
||||||
|
|
||||||
private ChannelInterceptor csrfChannelInterceptor = new CsrfChannelInterceptor(); |
|
||||||
|
|
||||||
private AuthorizationChannelInterceptor authorizationChannelInterceptor = new AuthorizationChannelInterceptor( |
|
||||||
ANY_MESSAGE_AUTHENTICATED); |
|
||||||
|
|
||||||
private Consumer<List<ChannelInterceptor>> interceptorsCustomizer = (interceptors) -> { |
|
||||||
}; |
|
||||||
|
|
||||||
private ApplicationContext context; |
|
||||||
|
|
||||||
private AbstractSecurityWebSocketMessageBrokerConfigurer configurer; |
|
||||||
|
|
||||||
WebSocketMessageBrokerSecurityConfiguration(ApplicationContext context) { |
|
||||||
this.context = context; |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) { |
|
||||||
if (this.configurer != null) { |
|
||||||
return; |
|
||||||
} |
|
||||||
argumentResolvers.add(new AuthenticationPrincipalArgumentResolver()); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public void configureClientInboundChannel(ChannelRegistration registration) { |
|
||||||
if (this.configurer != null) { |
|
||||||
return; |
|
||||||
} |
|
||||||
this.authorizationChannelInterceptor |
|
||||||
.setAuthorizationEventPublisher(new SpringAuthorizationEventPublisher(this.context)); |
|
||||||
List<ChannelInterceptor> interceptors = new ArrayList<>(Arrays.asList(this.securityContextChannelInterceptor, |
|
||||||
this.csrfChannelInterceptor, this.authorizationChannelInterceptor)); |
|
||||||
this.interceptorsCustomizer.accept(interceptors); |
|
||||||
registration.interceptors(interceptors.toArray(new ChannelInterceptor[0])); |
|
||||||
} |
|
||||||
|
|
||||||
@Autowired(required = false) |
|
||||||
void setSecurityContextChannelInterceptor(SecurityContextChannelInterceptor interceptor) { |
|
||||||
this.securityContextChannelInterceptor = interceptor; |
|
||||||
} |
|
||||||
|
|
||||||
@Autowired(required = false) |
|
||||||
void setCsrfChannelInterceptor(CsrfChannelInterceptor csrfChannelInterceptor) { |
|
||||||
this.csrfChannelInterceptor = csrfChannelInterceptor; |
|
||||||
} |
|
||||||
|
|
||||||
@Autowired(required = false) |
|
||||||
void setAuthorizationManager(AuthorizationManager<Message<?>> authorizationManager) { |
|
||||||
this.authorizationChannelInterceptor = new AuthorizationChannelInterceptor(authorizationManager); |
|
||||||
} |
|
||||||
|
|
||||||
@Autowired(required = false) |
|
||||||
void setInterceptorsCustomizer(Consumer<List<ChannelInterceptor>> interceptorsCustomizer) { |
|
||||||
this.interceptorsCustomizer = interceptorsCustomizer; |
|
||||||
} |
|
||||||
|
|
||||||
@Autowired(required = false) |
|
||||||
@Deprecated |
|
||||||
void setAbstractSecurityWebSocketMessageBrokerConfigurer( |
|
||||||
AbstractSecurityWebSocketMessageBrokerConfigurer configurer) { |
|
||||||
this.configurer = configurer; |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public void afterSingletonsInstantiated() { |
|
||||||
if (this.configurer != null) { |
|
||||||
return; |
|
||||||
} |
|
||||||
SimpleUrlHandlerMapping mapping = getBeanOrNull(SIMPLE_URL_HANDLER_MAPPING_BEAN_NAME, |
|
||||||
SimpleUrlHandlerMapping.class); |
|
||||||
if (mapping == null) { |
|
||||||
return; |
|
||||||
} |
|
||||||
configureCsrf(mapping); |
|
||||||
} |
|
||||||
|
|
||||||
private <T> T getBeanOrNull(String name, Class<T> type) { |
|
||||||
Map<String, T> beans = this.context.getBeansOfType(type); |
|
||||||
return beans.get(name); |
|
||||||
} |
|
||||||
|
|
||||||
private void configureCsrf(SimpleUrlHandlerMapping mapping) { |
|
||||||
Map<String, Object> mappings = mapping.getHandlerMap(); |
|
||||||
for (Object object : mappings.values()) { |
|
||||||
if (object instanceof SockJsHttpRequestHandler) { |
|
||||||
setHandshakeInterceptors((SockJsHttpRequestHandler) object); |
|
||||||
} |
|
||||||
else if (object instanceof WebSocketHttpRequestHandler) { |
|
||||||
setHandshakeInterceptors((WebSocketHttpRequestHandler) object); |
|
||||||
} |
|
||||||
else { |
|
||||||
throw new IllegalStateException( |
|
||||||
"Bean " + SIMPLE_URL_HANDLER_MAPPING_BEAN_NAME + " is expected to contain mappings to either a " |
|
||||||
+ "SockJsHttpRequestHandler or a WebSocketHttpRequestHandler but got " + object); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private void setHandshakeInterceptors(SockJsHttpRequestHandler handler) { |
|
||||||
SockJsService sockJsService = handler.getSockJsService(); |
|
||||||
Assert.state(sockJsService instanceof TransportHandlingSockJsService, |
|
||||||
() -> "sockJsService must be instance of TransportHandlingSockJsService got " + sockJsService); |
|
||||||
TransportHandlingSockJsService transportHandlingSockJsService = (TransportHandlingSockJsService) sockJsService; |
|
||||||
List<HandshakeInterceptor> handshakeInterceptors = transportHandlingSockJsService.getHandshakeInterceptors(); |
|
||||||
List<HandshakeInterceptor> interceptorsToSet = new ArrayList<>(handshakeInterceptors.size() + 1); |
|
||||||
interceptorsToSet.add(new CsrfTokenHandshakeInterceptor()); |
|
||||||
interceptorsToSet.addAll(handshakeInterceptors); |
|
||||||
transportHandlingSockJsService.setHandshakeInterceptors(interceptorsToSet); |
|
||||||
} |
|
||||||
|
|
||||||
private void setHandshakeInterceptors(WebSocketHttpRequestHandler handler) { |
|
||||||
List<HandshakeInterceptor> handshakeInterceptors = handler.getHandshakeInterceptors(); |
|
||||||
List<HandshakeInterceptor> interceptorsToSet = new ArrayList<>(handshakeInterceptors.size() + 1); |
|
||||||
interceptorsToSet.add(new CsrfTokenHandshakeInterceptor()); |
|
||||||
interceptorsToSet.addAll(handshakeInterceptors); |
|
||||||
handler.setHandshakeInterceptors(interceptorsToSet); |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
@ -1,176 +0,0 @@ |
|||||||
/* |
|
||||||
* Copyright 2002-2022 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.config.annotation.web.socket; |
|
||||||
|
|
||||||
import java.util.HashMap; |
|
||||||
|
|
||||||
import org.junit.jupiter.api.AfterEach; |
|
||||||
import org.junit.jupiter.api.BeforeEach; |
|
||||||
import org.junit.jupiter.api.Test; |
|
||||||
|
|
||||||
import org.springframework.context.annotation.Bean; |
|
||||||
import org.springframework.context.annotation.Configuration; |
|
||||||
import org.springframework.messaging.Message; |
|
||||||
import org.springframework.messaging.MessageChannel; |
|
||||||
import org.springframework.messaging.MessageDeliveryException; |
|
||||||
import org.springframework.messaging.handler.annotation.MessageMapping; |
|
||||||
import org.springframework.messaging.simp.SimpMessageHeaderAccessor; |
|
||||||
import org.springframework.messaging.simp.SimpMessageType; |
|
||||||
import org.springframework.messaging.simp.config.MessageBrokerRegistry; |
|
||||||
import org.springframework.messaging.support.GenericMessage; |
|
||||||
import org.springframework.mock.web.MockServletConfig; |
|
||||||
import org.springframework.security.access.AccessDeniedException; |
|
||||||
import org.springframework.security.authentication.TestingAuthenticationToken; |
|
||||||
import org.springframework.security.authorization.AuthorizationManager; |
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; |
|
||||||
import org.springframework.security.config.annotation.web.messaging.AuthorizationManagerMessageMatcherRegistry; |
|
||||||
import org.springframework.security.core.annotation.AuthenticationPrincipal; |
|
||||||
import org.springframework.security.web.csrf.CsrfToken; |
|
||||||
import org.springframework.security.web.csrf.DefaultCsrfToken; |
|
||||||
import org.springframework.stereotype.Controller; |
|
||||||
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; |
|
||||||
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; |
|
||||||
import org.springframework.web.socket.config.annotation.StompEndpointRegistry; |
|
||||||
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; |
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType; |
|
||||||
|
|
||||||
public class WebSocketMessageBrokerSecurityConfigurationDocTests { |
|
||||||
|
|
||||||
AnnotationConfigWebApplicationContext context; |
|
||||||
|
|
||||||
TestingAuthenticationToken messageUser; |
|
||||||
|
|
||||||
CsrfToken token; |
|
||||||
|
|
||||||
String sessionAttr; |
|
||||||
|
|
||||||
@BeforeEach |
|
||||||
public void setup() { |
|
||||||
this.token = new DefaultCsrfToken("header", "param", "token"); |
|
||||||
this.sessionAttr = "sessionAttr"; |
|
||||||
this.messageUser = new TestingAuthenticationToken("user", "pass", "ROLE_USER"); |
|
||||||
} |
|
||||||
|
|
||||||
@AfterEach |
|
||||||
public void cleanup() { |
|
||||||
if (this.context != null) { |
|
||||||
this.context.close(); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
public void securityMappings() { |
|
||||||
loadConfig(WebSocketSecurityConfig.class); |
|
||||||
clientInboundChannel().send(message("/user/queue/errors", SimpMessageType.SUBSCRIBE)); |
|
||||||
assertThatExceptionOfType(MessageDeliveryException.class) |
|
||||||
.isThrownBy(() -> clientInboundChannel().send(message("/denyAll", SimpMessageType.MESSAGE))) |
|
||||||
.withCauseInstanceOf(AccessDeniedException.class); |
|
||||||
} |
|
||||||
|
|
||||||
private void loadConfig(Class<?>... configs) { |
|
||||||
this.context = new AnnotationConfigWebApplicationContext(); |
|
||||||
this.context.register(configs); |
|
||||||
this.context.register(WebSocketConfig.class, SyncExecutorConfig.class); |
|
||||||
this.context.setServletConfig(new MockServletConfig()); |
|
||||||
this.context.refresh(); |
|
||||||
} |
|
||||||
|
|
||||||
private MessageChannel clientInboundChannel() { |
|
||||||
return this.context.getBean("clientInboundChannel", MessageChannel.class); |
|
||||||
} |
|
||||||
|
|
||||||
private Message<String> message(String destination, SimpMessageType type) { |
|
||||||
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(type); |
|
||||||
return message(headers, destination); |
|
||||||
} |
|
||||||
|
|
||||||
private Message<String> message(SimpMessageHeaderAccessor headers, String destination) { |
|
||||||
headers.setSessionId("123"); |
|
||||||
headers.setSessionAttributes(new HashMap<>()); |
|
||||||
if (destination != null) { |
|
||||||
headers.setDestination(destination); |
|
||||||
} |
|
||||||
if (this.messageUser != null) { |
|
||||||
headers.setUser(this.messageUser); |
|
||||||
} |
|
||||||
return new GenericMessage<>("hi", headers.getMessageHeaders()); |
|
||||||
} |
|
||||||
|
|
||||||
@Controller |
|
||||||
static class MyController { |
|
||||||
|
|
||||||
@MessageMapping("/authentication") |
|
||||||
void authentication(@AuthenticationPrincipal String un) { |
|
||||||
// ... do something ...
|
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
@Configuration |
|
||||||
@EnableWebSecurity |
|
||||||
static class WebSocketSecurityConfig { |
|
||||||
|
|
||||||
@Bean |
|
||||||
AuthorizationManager<Message<?>> authorizationManager(AuthorizationManagerMessageMatcherRegistry messages) { |
|
||||||
messages.nullDestMatcher().authenticated() |
|
||||||
// <1>
|
|
||||||
.simpSubscribeDestMatchers("/user/queue/errors").permitAll() |
|
||||||
// <2>
|
|
||||||
.simpDestMatchers("/app/**").hasRole("USER") |
|
||||||
// <3>
|
|
||||||
.simpSubscribeDestMatchers("/user/**", "/topic/friends/*").hasRole("USER") // <4>
|
|
||||||
.simpTypeMatchers(SimpMessageType.MESSAGE, SimpMessageType.SUBSCRIBE).denyAll() // <5>
|
|
||||||
.anyMessage().denyAll(); // <6>
|
|
||||||
return messages.build(); |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
@Configuration |
|
||||||
@EnableWebSocketMessageBroker |
|
||||||
static class WebSocketConfig implements WebSocketMessageBrokerConfigurer { |
|
||||||
|
|
||||||
@Override |
|
||||||
public void registerStompEndpoints(StompEndpointRegistry registry) { |
|
||||||
registry.addEndpoint("/chat").withSockJS(); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public void configureMessageBroker(MessageBrokerRegistry registry) { |
|
||||||
registry.enableSimpleBroker("/queue/", "/topic/"); |
|
||||||
registry.setApplicationDestinationPrefixes("/permitAll", "/denyAll"); |
|
||||||
} |
|
||||||
|
|
||||||
@Bean |
|
||||||
MyController myController() { |
|
||||||
return new MyController(); |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
@Configuration |
|
||||||
static class SyncExecutorConfig { |
|
||||||
|
|
||||||
@Bean |
|
||||||
static SyncExecutorSubscribableChannelPostProcessor postProcessor() { |
|
||||||
return new SyncExecutorSubscribableChannelPostProcessor(); |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
@ -1,796 +0,0 @@ |
|||||||
/* |
|
||||||
* Copyright 2002-2022 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.config.annotation.web.socket; |
|
||||||
|
|
||||||
import java.util.HashMap; |
|
||||||
import java.util.List; |
|
||||||
import java.util.Map; |
|
||||||
import java.util.function.Consumer; |
|
||||||
import java.util.stream.Collectors; |
|
||||||
import java.util.stream.Stream; |
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest; |
|
||||||
|
|
||||||
import org.junit.jupiter.api.AfterEach; |
|
||||||
import org.junit.jupiter.api.BeforeEach; |
|
||||||
import org.junit.jupiter.api.Test; |
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired; |
|
||||||
import org.springframework.context.ApplicationContext; |
|
||||||
import org.springframework.context.annotation.Bean; |
|
||||||
import org.springframework.context.annotation.Configuration; |
|
||||||
import org.springframework.context.annotation.Import; |
|
||||||
import org.springframework.core.MethodParameter; |
|
||||||
import org.springframework.http.server.ServerHttpRequest; |
|
||||||
import org.springframework.http.server.ServerHttpResponse; |
|
||||||
import org.springframework.messaging.Message; |
|
||||||
import org.springframework.messaging.MessageChannel; |
|
||||||
import org.springframework.messaging.MessageDeliveryException; |
|
||||||
import org.springframework.messaging.handler.annotation.MessageMapping; |
|
||||||
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; |
|
||||||
import org.springframework.messaging.simp.SimpMessageHeaderAccessor; |
|
||||||
import org.springframework.messaging.simp.SimpMessageType; |
|
||||||
import org.springframework.messaging.simp.config.MessageBrokerRegistry; |
|
||||||
import org.springframework.messaging.support.AbstractMessageChannel; |
|
||||||
import org.springframework.messaging.support.ChannelInterceptor; |
|
||||||
import org.springframework.messaging.support.GenericMessage; |
|
||||||
import org.springframework.mock.web.MockHttpServletRequest; |
|
||||||
import org.springframework.mock.web.MockHttpServletResponse; |
|
||||||
import org.springframework.mock.web.MockServletConfig; |
|
||||||
import org.springframework.security.access.AccessDeniedException; |
|
||||||
import org.springframework.security.authentication.TestingAuthenticationToken; |
|
||||||
import org.springframework.security.authorization.AuthorizationDecision; |
|
||||||
import org.springframework.security.authorization.AuthorizationManager; |
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; |
|
||||||
import org.springframework.security.config.annotation.web.messaging.AuthorizationManagerMessageMatcherRegistry; |
|
||||||
import org.springframework.security.config.annotation.web.messaging.MessageSecurityMetadataSourceRegistry; |
|
||||||
import org.springframework.security.core.Authentication; |
|
||||||
import org.springframework.security.core.annotation.AuthenticationPrincipal; |
|
||||||
import org.springframework.security.messaging.access.intercept.AuthorizationChannelInterceptor; |
|
||||||
import org.springframework.security.messaging.access.intercept.ChannelSecurityInterceptor; |
|
||||||
import org.springframework.security.messaging.access.intercept.MessageAuthorizationContext; |
|
||||||
import org.springframework.security.messaging.context.SecurityContextChannelInterceptor; |
|
||||||
import org.springframework.security.messaging.web.csrf.CsrfChannelInterceptor; |
|
||||||
import org.springframework.security.web.csrf.CsrfToken; |
|
||||||
import org.springframework.security.web.csrf.DefaultCsrfToken; |
|
||||||
import org.springframework.security.web.csrf.MissingCsrfTokenException; |
|
||||||
import org.springframework.stereotype.Controller; |
|
||||||
import org.springframework.test.util.ReflectionTestUtils; |
|
||||||
import org.springframework.util.AntPathMatcher; |
|
||||||
import org.springframework.web.HttpRequestHandler; |
|
||||||
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; |
|
||||||
import org.springframework.web.servlet.HandlerMapping; |
|
||||||
import org.springframework.web.socket.WebSocketHandler; |
|
||||||
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; |
|
||||||
import org.springframework.web.socket.config.annotation.StompEndpointRegistry; |
|
||||||
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; |
|
||||||
import org.springframework.web.socket.server.HandshakeFailureException; |
|
||||||
import org.springframework.web.socket.server.HandshakeHandler; |
|
||||||
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor; |
|
||||||
import org.springframework.web.socket.sockjs.transport.handler.SockJsWebSocketHandler; |
|
||||||
import org.springframework.web.socket.sockjs.transport.session.WebSocketServerSockJsSession; |
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat; |
|
||||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType; |
|
||||||
import static org.assertj.core.api.Assertions.fail; |
|
||||||
|
|
||||||
public class WebSocketMessageBrokerSecurityConfigurationTests { |
|
||||||
|
|
||||||
AnnotationConfigWebApplicationContext context; |
|
||||||
|
|
||||||
TestingAuthenticationToken messageUser; |
|
||||||
|
|
||||||
CsrfToken token; |
|
||||||
|
|
||||||
String sessionAttr; |
|
||||||
|
|
||||||
@BeforeEach |
|
||||||
public void setup() { |
|
||||||
this.token = new DefaultCsrfToken("header", "param", "token"); |
|
||||||
this.sessionAttr = "sessionAttr"; |
|
||||||
this.messageUser = new TestingAuthenticationToken("user", "pass", "ROLE_USER"); |
|
||||||
} |
|
||||||
|
|
||||||
@AfterEach |
|
||||||
public void cleanup() { |
|
||||||
if (this.context != null) { |
|
||||||
this.context.close(); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
public void simpleRegistryMappings() { |
|
||||||
loadConfig(SockJsSecurityConfig.class); |
|
||||||
clientInboundChannel().send(message("/permitAll")); |
|
||||||
assertThatExceptionOfType(MessageDeliveryException.class) |
|
||||||
.isThrownBy(() -> clientInboundChannel().send(message("/denyAll"))) |
|
||||||
.withCauseInstanceOf(AccessDeniedException.class); |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
public void annonymousSupported() { |
|
||||||
loadConfig(SockJsSecurityConfig.class); |
|
||||||
this.messageUser = null; |
|
||||||
clientInboundChannel().send(message("/permitAll")); |
|
||||||
} |
|
||||||
|
|
||||||
// gh-3797
|
|
||||||
@Test |
|
||||||
public void beanResolver() { |
|
||||||
loadConfig(SockJsSecurityConfig.class); |
|
||||||
this.messageUser = null; |
|
||||||
clientInboundChannel().send(message("/beanResolver")); |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
public void addsAuthenticationPrincipalResolver() { |
|
||||||
loadConfig(SockJsSecurityConfig.class); |
|
||||||
MessageChannel messageChannel = clientInboundChannel(); |
|
||||||
Message<String> message = message("/permitAll/authentication"); |
|
||||||
messageChannel.send(message); |
|
||||||
assertThat(this.context.getBean(MyController.class).authenticationPrincipal) |
|
||||||
.isEqualTo((String) this.messageUser.getPrincipal()); |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
public void addsAuthenticationPrincipalResolverWhenNoAuthorization() { |
|
||||||
loadConfig(NoInboundSecurityConfig.class); |
|
||||||
MessageChannel messageChannel = clientInboundChannel(); |
|
||||||
Message<String> message = message("/permitAll/authentication"); |
|
||||||
messageChannel.send(message); |
|
||||||
assertThat(this.context.getBean(MyController.class).authenticationPrincipal) |
|
||||||
.isEqualTo((String) this.messageUser.getPrincipal()); |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
public void addsCsrfProtectionWhenNoAuthorization() { |
|
||||||
loadConfig(NoInboundSecurityConfig.class); |
|
||||||
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT); |
|
||||||
Message<?> message = message(headers, "/authentication"); |
|
||||||
MessageChannel messageChannel = clientInboundChannel(); |
|
||||||
assertThatExceptionOfType(MessageDeliveryException.class).isThrownBy(() -> messageChannel.send(message)) |
|
||||||
.withCauseInstanceOf(MissingCsrfTokenException.class); |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
public void csrfProtectionForConnect() { |
|
||||||
loadConfig(SockJsSecurityConfig.class); |
|
||||||
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT); |
|
||||||
Message<?> message = message(headers, "/authentication"); |
|
||||||
MessageChannel messageChannel = clientInboundChannel(); |
|
||||||
assertThatExceptionOfType(MessageDeliveryException.class).isThrownBy(() -> messageChannel.send(message)) |
|
||||||
.withCauseInstanceOf(MissingCsrfTokenException.class); |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
public void csrfProtectionDisabledForConnect() { |
|
||||||
loadConfig(CsrfDisabledSockJsSecurityConfig.class); |
|
||||||
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT); |
|
||||||
Message<?> message = message(headers, "/permitAll/connect"); |
|
||||||
MessageChannel messageChannel = clientInboundChannel(); |
|
||||||
messageChannel.send(message); |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
public void csrfProtectionDefinedByBean() { |
|
||||||
loadConfig(SockJsProxylessSecurityConfig.class); |
|
||||||
MessageChannel messageChannel = clientInboundChannel(); |
|
||||||
Stream<Class<? extends ChannelInterceptor>> interceptors = ((AbstractMessageChannel) messageChannel) |
|
||||||
.getInterceptors().stream().map(ChannelInterceptor::getClass); |
|
||||||
assertThat(interceptors).contains(CsrfChannelInterceptor.class); |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
public void messagesConnectUseCsrfTokenHandshakeInterceptor() throws Exception { |
|
||||||
loadConfig(SockJsSecurityConfig.class); |
|
||||||
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT); |
|
||||||
Message<?> message = message(headers, "/authentication"); |
|
||||||
MockHttpServletRequest request = sockjsHttpRequest("/chat"); |
|
||||||
HttpRequestHandler handler = handler(request); |
|
||||||
handler.handleRequest(request, new MockHttpServletResponse()); |
|
||||||
assertHandshake(request); |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
public void messagesConnectUseCsrfTokenHandshakeInterceptorMultipleMappings() throws Exception { |
|
||||||
loadConfig(SockJsSecurityConfig.class); |
|
||||||
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT); |
|
||||||
Message<?> message = message(headers, "/authentication"); |
|
||||||
MockHttpServletRequest request = sockjsHttpRequest("/other"); |
|
||||||
HttpRequestHandler handler = handler(request); |
|
||||||
handler.handleRequest(request, new MockHttpServletResponse()); |
|
||||||
assertHandshake(request); |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
public void messagesConnectWebSocketUseCsrfTokenHandshakeInterceptor() throws Exception { |
|
||||||
loadConfig(WebSocketSecurityConfig.class); |
|
||||||
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT); |
|
||||||
Message<?> message = message(headers, "/authentication"); |
|
||||||
MockHttpServletRequest request = websocketHttpRequest("/websocket"); |
|
||||||
HttpRequestHandler handler = handler(request); |
|
||||||
handler.handleRequest(request, new MockHttpServletResponse()); |
|
||||||
assertHandshake(request); |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
public void msmsRegistryCustomPatternMatcher() { |
|
||||||
loadConfig(MsmsRegistryCustomPatternMatcherConfig.class); |
|
||||||
clientInboundChannel().send(message("/app/a.b")); |
|
||||||
assertThatExceptionOfType(MessageDeliveryException.class) |
|
||||||
.isThrownBy(() -> clientInboundChannel().send(message("/app/a.b.c"))) |
|
||||||
.withCauseInstanceOf(AccessDeniedException.class); |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
public void overrideMsmsRegistryCustomPatternMatcher() { |
|
||||||
loadConfig(OverrideMsmsRegistryCustomPatternMatcherConfig.class); |
|
||||||
clientInboundChannel().send(message("/app/a/b")); |
|
||||||
assertThatExceptionOfType(MessageDeliveryException.class) |
|
||||||
.isThrownBy(() -> clientInboundChannel().send(message("/app/a/b/c"))) |
|
||||||
.withCauseInstanceOf(AccessDeniedException.class); |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
public void defaultPatternMatcher() { |
|
||||||
loadConfig(DefaultPatternMatcherConfig.class); |
|
||||||
clientInboundChannel().send(message("/app/a/b")); |
|
||||||
assertThatExceptionOfType(MessageDeliveryException.class) |
|
||||||
.isThrownBy(() -> clientInboundChannel().send(message("/app/a/b/c"))) |
|
||||||
.withCauseInstanceOf(AccessDeniedException.class); |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
public void customExpression() { |
|
||||||
loadConfig(CustomExpressionConfig.class); |
|
||||||
clientInboundChannel().send(message("/denyRob")); |
|
||||||
this.messageUser = new TestingAuthenticationToken("rob", "password", "ROLE_USER"); |
|
||||||
assertThatExceptionOfType(MessageDeliveryException.class) |
|
||||||
.isThrownBy(() -> clientInboundChannel().send(message("/denyRob"))) |
|
||||||
.withCauseInstanceOf(AccessDeniedException.class); |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
public void channelSecurityInterceptorUsesMetadataSourceBeanWhenProxyingDisabled() { |
|
||||||
loadConfig(SockJsProxylessSecurityConfig.class); |
|
||||||
AbstractMessageChannel messageChannel = clientInboundChannel(); |
|
||||||
AuthorizationManager<Message<?>> authorizationManager = this.context.getBean(AuthorizationManager.class); |
|
||||||
for (ChannelInterceptor interceptor : messageChannel.getInterceptors()) { |
|
||||||
if (interceptor instanceof AuthorizationChannelInterceptor) { |
|
||||||
assertThat(ReflectionTestUtils.getField(interceptor, "preSendAuthorizationManager")) |
|
||||||
.isSameAs(authorizationManager); |
|
||||||
return; |
|
||||||
} |
|
||||||
} |
|
||||||
fail("did not find AuthorizationChannelInterceptor"); |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
public void securityContextChannelInterceptorDefinedByBean() { |
|
||||||
loadConfig(SockJsProxylessSecurityConfig.class); |
|
||||||
MessageChannel messageChannel = clientInboundChannel(); |
|
||||||
Stream<Class<? extends ChannelInterceptor>> interceptors = ((AbstractMessageChannel) messageChannel) |
|
||||||
.getInterceptors().stream().map(ChannelInterceptor::getClass); |
|
||||||
assertThat(interceptors).contains(SecurityContextChannelInterceptor.class); |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
public void inboundChannelSecurityDefinedByBean() { |
|
||||||
loadConfig(SockJsProxylessSecurityConfig.class); |
|
||||||
MessageChannel messageChannel = clientInboundChannel(); |
|
||||||
Stream<Class<? extends ChannelInterceptor>> interceptors = ((AbstractMessageChannel) messageChannel) |
|
||||||
.getInterceptors().stream().map(ChannelInterceptor::getClass); |
|
||||||
assertThat(interceptors).contains(AuthorizationChannelInterceptor.class); |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
public void usingLegacyThenNewFiltersNotUsed() { |
|
||||||
loadConfig(UsingLegacyConfigurerConfig.class); |
|
||||||
AbstractMessageChannel messageChannel = clientInboundChannel(); |
|
||||||
List<Class<? extends ChannelInterceptor>> interceptors = messageChannel.getInterceptors().stream() |
|
||||||
.map(ChannelInterceptor::getClass).collect(Collectors.toList()); |
|
||||||
assertThat(interceptors).contains(ChannelSecurityInterceptor.class); |
|
||||||
assertThat(interceptors).doesNotContain(AuthorizationChannelInterceptor.class); |
|
||||||
} |
|
||||||
|
|
||||||
private void assertHandshake(HttpServletRequest request) { |
|
||||||
TestHandshakeHandler handshakeHandler = this.context.getBean(TestHandshakeHandler.class); |
|
||||||
assertThat(handshakeHandler.attributes.get(CsrfToken.class.getName())).isSameAs(this.token); |
|
||||||
assertThat(handshakeHandler.attributes.get(this.sessionAttr)) |
|
||||||
.isEqualTo(request.getSession().getAttribute(this.sessionAttr)); |
|
||||||
} |
|
||||||
|
|
||||||
private HttpRequestHandler handler(HttpServletRequest request) throws Exception { |
|
||||||
HandlerMapping handlerMapping = this.context.getBean(HandlerMapping.class); |
|
||||||
return (HttpRequestHandler) handlerMapping.getHandler(request).getHandler(); |
|
||||||
} |
|
||||||
|
|
||||||
private MockHttpServletRequest websocketHttpRequest(String mapping) { |
|
||||||
MockHttpServletRequest request = sockjsHttpRequest(mapping); |
|
||||||
request.setRequestURI(mapping); |
|
||||||
return request; |
|
||||||
} |
|
||||||
|
|
||||||
private MockHttpServletRequest sockjsHttpRequest(String mapping) { |
|
||||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", ""); |
|
||||||
request.setMethod("GET"); |
|
||||||
request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "/289/tpyx6mde/websocket"); |
|
||||||
request.setRequestURI(mapping + "/289/tpyx6mde/websocket"); |
|
||||||
request.getSession().setAttribute(this.sessionAttr, "sessionValue"); |
|
||||||
request.setAttribute(CsrfToken.class.getName(), this.token); |
|
||||||
return request; |
|
||||||
} |
|
||||||
|
|
||||||
private Message<String> message(String destination) { |
|
||||||
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(); |
|
||||||
return message(headers, destination); |
|
||||||
} |
|
||||||
|
|
||||||
private Message<String> message(SimpMessageHeaderAccessor headers, String destination) { |
|
||||||
headers.setSessionId("123"); |
|
||||||
headers.setSessionAttributes(new HashMap<>()); |
|
||||||
if (destination != null) { |
|
||||||
headers.setDestination(destination); |
|
||||||
} |
|
||||||
if (this.messageUser != null) { |
|
||||||
headers.setUser(this.messageUser); |
|
||||||
} |
|
||||||
return new GenericMessage<>("hi", headers.getMessageHeaders()); |
|
||||||
} |
|
||||||
|
|
||||||
private <T extends MessageChannel> T clientInboundChannel() { |
|
||||||
return (T) this.context.getBean("clientInboundChannel", MessageChannel.class); |
|
||||||
} |
|
||||||
|
|
||||||
private void loadConfig(Class<?>... configs) { |
|
||||||
this.context = new AnnotationConfigWebApplicationContext(); |
|
||||||
this.context.register(configs); |
|
||||||
this.context.setServletConfig(new MockServletConfig()); |
|
||||||
this.context.refresh(); |
|
||||||
} |
|
||||||
|
|
||||||
@Configuration |
|
||||||
@EnableWebSocketMessageBroker |
|
||||||
@EnableWebSecurity |
|
||||||
@Import(SyncExecutorConfig.class) |
|
||||||
static class MsmsRegistryCustomPatternMatcherConfig implements WebSocketMessageBrokerConfigurer { |
|
||||||
|
|
||||||
// @formatter:off
|
|
||||||
@Override |
|
||||||
public void registerStompEndpoints(StompEndpointRegistry registry) { |
|
||||||
registry |
|
||||||
.addEndpoint("/other") |
|
||||||
.setHandshakeHandler(testHandshakeHandler()); |
|
||||||
} |
|
||||||
// @formatter:on
|
|
||||||
|
|
||||||
@Override |
|
||||||
public void configureMessageBroker(MessageBrokerRegistry registry) { |
|
||||||
registry.setPathMatcher(new AntPathMatcher(".")); |
|
||||||
registry.enableSimpleBroker("/queue/", "/topic/"); |
|
||||||
registry.setApplicationDestinationPrefixes("/app"); |
|
||||||
} |
|
||||||
|
|
||||||
// @formatter:off
|
|
||||||
@Bean |
|
||||||
AuthorizationManager<Message<?>> authorizationManager(AuthorizationManagerMessageMatcherRegistry messages) { |
|
||||||
messages |
|
||||||
.simpDestMatchers("/app/a.*").permitAll() |
|
||||||
.anyMessage().denyAll(); |
|
||||||
|
|
||||||
return messages.build(); |
|
||||||
} |
|
||||||
// @formatter:on
|
|
||||||
|
|
||||||
@Bean |
|
||||||
TestHandshakeHandler testHandshakeHandler() { |
|
||||||
return new TestHandshakeHandler(); |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
@Configuration |
|
||||||
@EnableWebSocketMessageBroker |
|
||||||
@EnableWebSecurity |
|
||||||
@Import(SyncExecutorConfig.class) |
|
||||||
static class OverrideMsmsRegistryCustomPatternMatcherConfig implements WebSocketMessageBrokerConfigurer { |
|
||||||
|
|
||||||
// @formatter:off
|
|
||||||
@Override |
|
||||||
public void registerStompEndpoints(StompEndpointRegistry registry) { |
|
||||||
registry |
|
||||||
.addEndpoint("/other") |
|
||||||
.setHandshakeHandler(testHandshakeHandler()); |
|
||||||
} |
|
||||||
// @formatter:on
|
|
||||||
|
|
||||||
@Override |
|
||||||
public void configureMessageBroker(MessageBrokerRegistry registry) { |
|
||||||
registry.setPathMatcher(new AntPathMatcher(".")); |
|
||||||
registry.enableSimpleBroker("/queue/", "/topic/"); |
|
||||||
registry.setApplicationDestinationPrefixes("/app"); |
|
||||||
} |
|
||||||
|
|
||||||
// @formatter:off
|
|
||||||
@Bean |
|
||||||
AuthorizationManager<Message<?>> authorizationManager(AuthorizationManagerMessageMatcherRegistry messages) { |
|
||||||
messages |
|
||||||
.simpDestPathMatcher(new AntPathMatcher()) |
|
||||||
.simpDestMatchers("/app/a/*").permitAll() |
|
||||||
.anyMessage().denyAll(); |
|
||||||
return messages.build(); |
|
||||||
} |
|
||||||
// @formatter:on
|
|
||||||
|
|
||||||
@Bean |
|
||||||
TestHandshakeHandler testHandshakeHandler() { |
|
||||||
return new TestHandshakeHandler(); |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
@Configuration |
|
||||||
@EnableWebSocketMessageBroker |
|
||||||
@EnableWebSecurity |
|
||||||
@Import(SyncExecutorConfig.class) |
|
||||||
static class DefaultPatternMatcherConfig implements WebSocketMessageBrokerConfigurer { |
|
||||||
|
|
||||||
// @formatter:off
|
|
||||||
@Override |
|
||||||
public void registerStompEndpoints(StompEndpointRegistry registry) { |
|
||||||
registry |
|
||||||
.addEndpoint("/other") |
|
||||||
.setHandshakeHandler(testHandshakeHandler()); |
|
||||||
} |
|
||||||
// @formatter:on
|
|
||||||
|
|
||||||
@Override |
|
||||||
public void configureMessageBroker(MessageBrokerRegistry registry) { |
|
||||||
registry.enableSimpleBroker("/queue/", "/topic/"); |
|
||||||
registry.setApplicationDestinationPrefixes("/app"); |
|
||||||
} |
|
||||||
|
|
||||||
// @formatter:off
|
|
||||||
@Bean |
|
||||||
AuthorizationManager<Message<?>> authorizationManager(AuthorizationManagerMessageMatcherRegistry messages) { |
|
||||||
messages |
|
||||||
.simpDestMatchers("/app/a/*").permitAll() |
|
||||||
.anyMessage().denyAll(); |
|
||||||
|
|
||||||
return messages.build(); |
|
||||||
} |
|
||||||
// @formatter:on
|
|
||||||
|
|
||||||
@Bean |
|
||||||
TestHandshakeHandler testHandshakeHandler() { |
|
||||||
return new TestHandshakeHandler(); |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
@Configuration |
|
||||||
@EnableWebSocketMessageBroker |
|
||||||
@EnableWebSecurity |
|
||||||
@Import(SyncExecutorConfig.class) |
|
||||||
static class CustomExpressionConfig implements WebSocketMessageBrokerConfigurer { |
|
||||||
|
|
||||||
// @formatter:off
|
|
||||||
@Override |
|
||||||
public void registerStompEndpoints(StompEndpointRegistry registry) { |
|
||||||
registry |
|
||||||
.addEndpoint("/other") |
|
||||||
.setHandshakeHandler(testHandshakeHandler()); |
|
||||||
} |
|
||||||
// @formatter:on
|
|
||||||
|
|
||||||
@Override |
|
||||||
public void configureMessageBroker(MessageBrokerRegistry registry) { |
|
||||||
registry.enableSimpleBroker("/queue/", "/topic/"); |
|
||||||
registry.setApplicationDestinationPrefixes("/app"); |
|
||||||
} |
|
||||||
|
|
||||||
@Bean |
|
||||||
AuthorizationManager<Message<Object>> authorizationManager() { |
|
||||||
return (authentication, message) -> { |
|
||||||
Authentication auth = authentication.get(); |
|
||||||
return new AuthorizationDecision(auth != null && !"rob".equals(auth.getName())); |
|
||||||
}; |
|
||||||
} |
|
||||||
|
|
||||||
@Bean |
|
||||||
TestHandshakeHandler testHandshakeHandler() { |
|
||||||
return new TestHandshakeHandler(); |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
@Controller |
|
||||||
static class MyController { |
|
||||||
|
|
||||||
String authenticationPrincipal; |
|
||||||
|
|
||||||
MyCustomArgument myCustomArgument; |
|
||||||
|
|
||||||
@MessageMapping("/authentication") |
|
||||||
void authentication(@AuthenticationPrincipal String un) { |
|
||||||
this.authenticationPrincipal = un; |
|
||||||
} |
|
||||||
|
|
||||||
@MessageMapping("/myCustom") |
|
||||||
void myCustom(MyCustomArgument myCustomArgument) { |
|
||||||
this.myCustomArgument = myCustomArgument; |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
static class MyCustomArgument { |
|
||||||
|
|
||||||
MyCustomArgument(String notDefaultConstr) { |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
static class MyCustomArgumentResolver implements HandlerMethodArgumentResolver { |
|
||||||
|
|
||||||
@Override |
|
||||||
public boolean supportsParameter(MethodParameter parameter) { |
|
||||||
return parameter.getParameterType().isAssignableFrom(MyCustomArgument.class); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public Object resolveArgument(MethodParameter parameter, Message<?> message) { |
|
||||||
return new MyCustomArgument(""); |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
static class TestHandshakeHandler implements HandshakeHandler { |
|
||||||
|
|
||||||
Map<String, Object> attributes; |
|
||||||
|
|
||||||
@Override |
|
||||||
public boolean doHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, |
|
||||||
Map<String, Object> attributes) throws HandshakeFailureException { |
|
||||||
this.attributes = attributes; |
|
||||||
if (wsHandler instanceof SockJsWebSocketHandler) { |
|
||||||
// work around SPR-12716
|
|
||||||
SockJsWebSocketHandler sockJs = (SockJsWebSocketHandler) wsHandler; |
|
||||||
WebSocketServerSockJsSession session = (WebSocketServerSockJsSession) ReflectionTestUtils |
|
||||||
.getField(sockJs, "sockJsSession"); |
|
||||||
this.attributes = session.getAttributes(); |
|
||||||
} |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
@Configuration |
|
||||||
@EnableWebSecurity |
|
||||||
@EnableWebSocketMessageBroker |
|
||||||
@Import(SyncExecutorConfig.class) |
|
||||||
static class SockJsSecurityConfig implements WebSocketMessageBrokerConfigurer { |
|
||||||
|
|
||||||
@Override |
|
||||||
public void registerStompEndpoints(StompEndpointRegistry registry) { |
|
||||||
// @formatter:off
|
|
||||||
registry.addEndpoint("/other").setHandshakeHandler(testHandshakeHandler()) |
|
||||||
.withSockJS().setInterceptors(new HttpSessionHandshakeInterceptor()); |
|
||||||
registry.addEndpoint("/chat").setHandshakeHandler(testHandshakeHandler()) |
|
||||||
.withSockJS().setInterceptors(new HttpSessionHandshakeInterceptor()); |
|
||||||
// @formatter:on
|
|
||||||
} |
|
||||||
|
|
||||||
// @formatter:off
|
|
||||||
@Bean |
|
||||||
AuthorizationManager<Message<?>> authorizationManager(AuthorizationManagerMessageMatcherRegistry messages, |
|
||||||
SecurityCheck security) { |
|
||||||
AuthorizationManager<MessageAuthorizationContext<?>> beanResolver = |
|
||||||
(authentication, context) -> new AuthorizationDecision(security.check()); |
|
||||||
messages |
|
||||||
.simpDestMatchers("/permitAll/**").permitAll() |
|
||||||
.simpDestMatchers("/beanResolver/**").access(beanResolver) |
|
||||||
.anyMessage().denyAll(); |
|
||||||
return messages.build(); |
|
||||||
} |
|
||||||
// @formatter:on
|
|
||||||
|
|
||||||
@Override |
|
||||||
public void configureMessageBroker(MessageBrokerRegistry registry) { |
|
||||||
registry.enableSimpleBroker("/queue/", "/topic/"); |
|
||||||
registry.setApplicationDestinationPrefixes("/permitAll", "/denyAll"); |
|
||||||
} |
|
||||||
|
|
||||||
@Bean |
|
||||||
MyController myController() { |
|
||||||
return new MyController(); |
|
||||||
} |
|
||||||
|
|
||||||
@Bean |
|
||||||
TestHandshakeHandler testHandshakeHandler() { |
|
||||||
return new TestHandshakeHandler(); |
|
||||||
} |
|
||||||
|
|
||||||
@Bean |
|
||||||
SecurityCheck security() { |
|
||||||
return new SecurityCheck(); |
|
||||||
} |
|
||||||
|
|
||||||
static class SecurityCheck { |
|
||||||
|
|
||||||
private boolean check; |
|
||||||
|
|
||||||
boolean check() { |
|
||||||
this.check = !this.check; |
|
||||||
return this.check; |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
@Configuration |
|
||||||
@EnableWebSecurity |
|
||||||
@EnableWebSocketMessageBroker |
|
||||||
@Import(SyncExecutorConfig.class) |
|
||||||
static class NoInboundSecurityConfig implements WebSocketMessageBrokerConfigurer { |
|
||||||
|
|
||||||
@Override |
|
||||||
public void registerStompEndpoints(StompEndpointRegistry registry) { |
|
||||||
// @formatter:off
|
|
||||||
registry.addEndpoint("/other") |
|
||||||
.withSockJS().setInterceptors(new HttpSessionHandshakeInterceptor()); |
|
||||||
registry.addEndpoint("/chat") |
|
||||||
.withSockJS().setInterceptors(new HttpSessionHandshakeInterceptor()); |
|
||||||
// @formatter:on
|
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public void configureMessageBroker(MessageBrokerRegistry registry) { |
|
||||||
registry.enableSimpleBroker("/queue/", "/topic/"); |
|
||||||
registry.setApplicationDestinationPrefixes("/permitAll", "/denyAll"); |
|
||||||
} |
|
||||||
|
|
||||||
@Bean |
|
||||||
MyController myController() { |
|
||||||
return new MyController(); |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
@Configuration |
|
||||||
@Import(SockJsSecurityConfig.class) |
|
||||||
static class CsrfDisabledSockJsSecurityConfig { |
|
||||||
|
|
||||||
@Bean |
|
||||||
Consumer<List<ChannelInterceptor>> channelInterceptorCustomizer() { |
|
||||||
return (interceptors) -> interceptors.remove(1); |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
@Configuration |
|
||||||
@EnableWebSecurity |
|
||||||
@EnableWebSocketMessageBroker |
|
||||||
@Import(SyncExecutorConfig.class) |
|
||||||
static class WebSocketSecurityConfig implements WebSocketMessageBrokerConfigurer { |
|
||||||
|
|
||||||
@Override |
|
||||||
public void registerStompEndpoints(StompEndpointRegistry registry) { |
|
||||||
// @formatter:off
|
|
||||||
registry.addEndpoint("/websocket") |
|
||||||
.setHandshakeHandler(testHandshakeHandler()) |
|
||||||
.addInterceptors(new HttpSessionHandshakeInterceptor()); |
|
||||||
// @formatter:on
|
|
||||||
} |
|
||||||
|
|
||||||
@Bean |
|
||||||
AuthorizationManager<Message<?>> authorizationManager(AuthorizationManagerMessageMatcherRegistry messages) { |
|
||||||
// @formatter:off
|
|
||||||
messages |
|
||||||
.simpDestMatchers("/permitAll/**").permitAll() |
|
||||||
.anyMessage().denyAll(); |
|
||||||
// @formatter:on
|
|
||||||
return messages.build(); |
|
||||||
} |
|
||||||
|
|
||||||
@Bean |
|
||||||
TestHandshakeHandler testHandshakeHandler() { |
|
||||||
return new TestHandshakeHandler(); |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
@Configuration |
|
||||||
@EnableWebSecurity |
|
||||||
@EnableWebSocketMessageBroker |
|
||||||
@Import(SyncExecutorConfig.class) |
|
||||||
static class UsingLegacyConfigurerConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer { |
|
||||||
|
|
||||||
@Override |
|
||||||
public void registerStompEndpoints(StompEndpointRegistry registry) { |
|
||||||
// @formatter:off
|
|
||||||
registry.addEndpoint("/websocket") |
|
||||||
.setHandshakeHandler(testHandshakeHandler()) |
|
||||||
.addInterceptors(new HttpSessionHandshakeInterceptor()); |
|
||||||
// @formatter:on
|
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public void configureInbound(MessageSecurityMetadataSourceRegistry messages) { |
|
||||||
// @formatter:off
|
|
||||||
messages |
|
||||||
.simpDestMatchers("/permitAll/**").permitAll() |
|
||||||
.anyMessage().denyAll(); |
|
||||||
// @formatter:on
|
|
||||||
} |
|
||||||
|
|
||||||
@Bean |
|
||||||
TestHandshakeHandler testHandshakeHandler() { |
|
||||||
return new TestHandshakeHandler(); |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
@Configuration(proxyBeanMethods = false) |
|
||||||
@EnableWebSecurity |
|
||||||
@EnableWebSocketMessageBroker |
|
||||||
@Import(SyncExecutorConfig.class) |
|
||||||
static class SockJsProxylessSecurityConfig implements WebSocketMessageBrokerConfigurer { |
|
||||||
|
|
||||||
private ApplicationContext context; |
|
||||||
|
|
||||||
@Override |
|
||||||
public void registerStompEndpoints(StompEndpointRegistry registry) { |
|
||||||
// @formatter:off
|
|
||||||
registry.addEndpoint("/chat") |
|
||||||
.setHandshakeHandler(this.context.getBean(TestHandshakeHandler.class)) |
|
||||||
.withSockJS().setInterceptors(new HttpSessionHandshakeInterceptor()); |
|
||||||
// @formatter:on
|
|
||||||
} |
|
||||||
|
|
||||||
@Autowired |
|
||||||
void setContext(ApplicationContext context) { |
|
||||||
this.context = context; |
|
||||||
} |
|
||||||
|
|
||||||
// @formatter:off
|
|
||||||
@Bean |
|
||||||
AuthorizationManager<Message<?>> authorizationManager(AuthorizationManagerMessageMatcherRegistry messages) { |
|
||||||
messages |
|
||||||
.anyMessage().denyAll(); |
|
||||||
return messages.build(); |
|
||||||
} |
|
||||||
// @formatter:on
|
|
||||||
|
|
||||||
@Bean |
|
||||||
TestHandshakeHandler testHandshakeHandler() { |
|
||||||
return new TestHandshakeHandler(); |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
@Configuration |
|
||||||
static class SyncExecutorConfig { |
|
||||||
|
|
||||||
@Bean |
|
||||||
static SyncExecutorSubscribableChannelPostProcessor postProcessor() { |
|
||||||
return new SyncExecutorSubscribableChannelPostProcessor(); |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
@ -1,95 +0,0 @@ |
|||||||
/* |
|
||||||
* Copyright 2002-2022 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.intercept; |
|
||||||
|
|
||||||
import java.util.function.Supplier; |
|
||||||
|
|
||||||
import org.apache.commons.logging.Log; |
|
||||||
import org.apache.commons.logging.LogFactory; |
|
||||||
|
|
||||||
import org.springframework.core.log.LogMessage; |
|
||||||
import org.springframework.messaging.Message; |
|
||||||
import org.springframework.messaging.MessageChannel; |
|
||||||
import org.springframework.messaging.support.ChannelInterceptor; |
|
||||||
import org.springframework.security.access.AccessDeniedException; |
|
||||||
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; |
|
||||||
import org.springframework.security.authorization.AuthorizationDecision; |
|
||||||
import org.springframework.security.authorization.AuthorizationEventPublisher; |
|
||||||
import org.springframework.security.authorization.AuthorizationManager; |
|
||||||
import org.springframework.security.core.Authentication; |
|
||||||
import org.springframework.security.core.context.SecurityContextHolder; |
|
||||||
import org.springframework.util.Assert; |
|
||||||
|
|
||||||
/** |
|
||||||
* Authorizes {@link Message} resources using the provided {@link AuthorizationManager} |
|
||||||
* |
|
||||||
* @author Josh Cummings |
|
||||||
* @since 5.7 |
|
||||||
*/ |
|
||||||
public final class AuthorizationChannelInterceptor implements ChannelInterceptor { |
|
||||||
|
|
||||||
static final Supplier<Authentication> AUTHENTICATION_SUPPLIER = () -> { |
|
||||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); |
|
||||||
if (authentication == null) { |
|
||||||
throw new AuthenticationCredentialsNotFoundException( |
|
||||||
"An Authentication object was not found in the SecurityContext"); |
|
||||||
} |
|
||||||
return authentication; |
|
||||||
}; |
|
||||||
|
|
||||||
private final Log logger = LogFactory.getLog(this.getClass()); |
|
||||||
|
|
||||||
private final AuthorizationManager<Message<?>> preSendAuthorizationManager; |
|
||||||
|
|
||||||
private AuthorizationEventPublisher eventPublisher; |
|
||||||
|
|
||||||
/** |
|
||||||
* Creates a new instance |
|
||||||
* @param preSendAuthorizationManager the {@link AuthorizationManager} to use. Cannot |
|
||||||
* be null. |
|
||||||
* |
|
||||||
*/ |
|
||||||
public AuthorizationChannelInterceptor(AuthorizationManager<Message<?>> preSendAuthorizationManager) { |
|
||||||
Assert.notNull(preSendAuthorizationManager, "preSendAuthorizationManager cannot be null"); |
|
||||||
this.preSendAuthorizationManager = preSendAuthorizationManager; |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public Message<?> preSend(Message<?> message, MessageChannel channel) { |
|
||||||
this.logger.debug(LogMessage.of(() -> "Authorizing message send")); |
|
||||||
AuthorizationDecision decision = this.preSendAuthorizationManager.check(AUTHENTICATION_SUPPLIER, message); |
|
||||||
this.eventPublisher.publishAuthorizationEvent(AUTHENTICATION_SUPPLIER, message, decision); |
|
||||||
if (decision == null || !decision.isGranted()) { // default deny
|
|
||||||
this.logger.debug(LogMessage.of(() -> "Failed to authorize message with authorization manager " |
|
||||||
+ this.preSendAuthorizationManager + " and decision " + decision)); |
|
||||||
throw new AccessDeniedException("Access Denied"); |
|
||||||
} |
|
||||||
this.logger.debug(LogMessage.of(() -> "Authorized message send")); |
|
||||||
return message; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Use this {@link AuthorizationEventPublisher} to publish the |
|
||||||
* {@link AuthorizationManager} result. |
|
||||||
* @param eventPublisher |
|
||||||
*/ |
|
||||||
public void setAuthorizationEventPublisher(AuthorizationEventPublisher eventPublisher) { |
|
||||||
Assert.notNull(eventPublisher, "eventPublisher cannot be null"); |
|
||||||
this.eventPublisher = eventPublisher; |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
@ -1,75 +0,0 @@ |
|||||||
/* |
|
||||||
* Copyright 2002-2022 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.intercept; |
|
||||||
|
|
||||||
import java.util.Collections; |
|
||||||
import java.util.Map; |
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest; |
|
||||||
|
|
||||||
import org.springframework.messaging.Message; |
|
||||||
|
|
||||||
/** |
|
||||||
* An {@link Message} authorization context. |
|
||||||
* |
|
||||||
* @author Josh Cummings |
|
||||||
* @since 5.7 |
|
||||||
*/ |
|
||||||
public final class MessageAuthorizationContext<T> { |
|
||||||
|
|
||||||
private final Message<T> message; |
|
||||||
|
|
||||||
private final Map<String, String> variables; |
|
||||||
|
|
||||||
/** |
|
||||||
* Creates an instance. |
|
||||||
* @param message the {@link HttpServletRequest} to use |
|
||||||
*/ |
|
||||||
public MessageAuthorizationContext(Message<T> message) { |
|
||||||
this(message, Collections.emptyMap()); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Creates an instance. |
|
||||||
* @param message the {@link HttpServletRequest} to use |
|
||||||
* @param variables a map containing key-value pairs representing extracted variable |
|
||||||
* names and variable values |
|
||||||
*/ |
|
||||||
public MessageAuthorizationContext(Message<T> message, Map<String, String> variables) { |
|
||||||
this.message = message; |
|
||||||
this.variables = variables; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Returns the {@link HttpServletRequest}. |
|
||||||
* @return the {@link HttpServletRequest} to use |
|
||||||
*/ |
|
||||||
public Message<T> getMessage() { |
|
||||||
return this.message; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Returns the extracted variable values where the key is the variable name and the |
|
||||||
* value is the variable value. |
|
||||||
* @return a map containing key-value pairs representing extracted variable names and |
|
||||||
* variable values |
|
||||||
*/ |
|
||||||
public Map<String, String> getVariables() { |
|
||||||
return this.variables; |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
@ -1,136 +0,0 @@ |
|||||||
/* |
|
||||||
* Copyright 2002-2022 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.intercept; |
|
||||||
|
|
||||||
import java.util.ArrayList; |
|
||||||
import java.util.List; |
|
||||||
import java.util.function.Supplier; |
|
||||||
|
|
||||||
import org.apache.commons.logging.Log; |
|
||||||
import org.apache.commons.logging.LogFactory; |
|
||||||
|
|
||||||
import org.springframework.core.log.LogMessage; |
|
||||||
import org.springframework.messaging.Message; |
|
||||||
import org.springframework.security.authorization.AuthorizationDecision; |
|
||||||
import org.springframework.security.authorization.AuthorizationManager; |
|
||||||
import org.springframework.security.core.Authentication; |
|
||||||
import org.springframework.security.messaging.util.matcher.MessageMatcher; |
|
||||||
import org.springframework.security.messaging.util.matcher.MessageMatcherEntry; |
|
||||||
import org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher; |
|
||||||
import org.springframework.util.Assert; |
|
||||||
|
|
||||||
/** |
|
||||||
* An {@link AuthorizationManager} which delegates to a specific |
|
||||||
* {@link AuthorizationManager} based on a {@link MessageMatcher} evaluation. |
|
||||||
* |
|
||||||
* @author Josh Cummings |
|
||||||
* @since 5.7 |
|
||||||
*/ |
|
||||||
public final class MessageMatcherDelegatingAuthorizationManager implements AuthorizationManager<Message<?>> { |
|
||||||
|
|
||||||
private final Log logger = LogFactory.getLog(getClass()); |
|
||||||
|
|
||||||
private final List<MessageMatcherEntry<AuthorizationManager<MessageAuthorizationContext<?>>>> mappings; |
|
||||||
|
|
||||||
private MessageMatcherDelegatingAuthorizationManager( |
|
||||||
List<MessageMatcherEntry<AuthorizationManager<MessageAuthorizationContext<?>>>> mappings) { |
|
||||||
Assert.notEmpty(mappings, "mappings cannot be empty"); |
|
||||||
this.mappings = mappings; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Delegates to a specific {@link AuthorizationManager} based on a |
|
||||||
* {@link MessageMatcher} evaluation. |
|
||||||
* @param authentication the {@link Supplier} of the {@link Authentication} to check |
|
||||||
* @param message the {@link Message} to check |
|
||||||
* @return an {@link AuthorizationDecision}. If there is no {@link MessageMatcher} |
|
||||||
* matching the message, or the {@link AuthorizationManager} could not decide, then |
|
||||||
* null is returned |
|
||||||
*/ |
|
||||||
@Override |
|
||||||
public AuthorizationDecision check(Supplier<Authentication> authentication, Message<?> message) { |
|
||||||
if (this.logger.isTraceEnabled()) { |
|
||||||
this.logger.trace(LogMessage.format("Authorizing message")); |
|
||||||
} |
|
||||||
for (MessageMatcherEntry<AuthorizationManager<MessageAuthorizationContext<?>>> mapping : this.mappings) { |
|
||||||
MessageMatcher<?> matcher = mapping.getMessageMatcher(); |
|
||||||
MessageAuthorizationContext<?> authorizationContext = authorizationContext(matcher, message); |
|
||||||
if (authorizationContext != null) { |
|
||||||
AuthorizationManager<MessageAuthorizationContext<?>> manager = mapping.getEntry(); |
|
||||||
if (this.logger.isTraceEnabled()) { |
|
||||||
this.logger.trace(LogMessage.format("Checking authorization on message using %s", manager)); |
|
||||||
} |
|
||||||
return manager.check(authentication, authorizationContext); |
|
||||||
} |
|
||||||
} |
|
||||||
this.logger.trace("Abstaining since did not find matching MessageMatcher"); |
|
||||||
return null; |
|
||||||
} |
|
||||||
|
|
||||||
private MessageAuthorizationContext<?> authorizationContext(MessageMatcher<?> matcher, Message<?> message) { |
|
||||||
if (!matcher.matches((Message) message)) { |
|
||||||
return null; |
|
||||||
} |
|
||||||
if (matcher instanceof SimpDestinationMessageMatcher) { |
|
||||||
SimpDestinationMessageMatcher simp = (SimpDestinationMessageMatcher) matcher; |
|
||||||
return new MessageAuthorizationContext<>(message, simp.extractPathVariables(message)); |
|
||||||
} |
|
||||||
return new MessageAuthorizationContext<>(message); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Creates a builder for {@link MessageMatcherDelegatingAuthorizationManager}. |
|
||||||
* @return the new {@link MessageMatcherDelegatingAuthorizationManager.Builder} |
|
||||||
* instance |
|
||||||
*/ |
|
||||||
public static Builder builder() { |
|
||||||
return new Builder(); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* A builder for {@link MessageMatcherDelegatingAuthorizationManager}. |
|
||||||
*/ |
|
||||||
public static final class Builder { |
|
||||||
|
|
||||||
private final List<MessageMatcherEntry<AuthorizationManager<MessageAuthorizationContext<?>>>> mappings = new ArrayList<>(); |
|
||||||
|
|
||||||
/** |
|
||||||
* Maps a {@link MessageMatcher} to an {@link AuthorizationManager}. |
|
||||||
* @param matcher the {@link MessageMatcher} to use |
|
||||||
* @param manager the {@link AuthorizationManager} to use |
|
||||||
* @return the {@link MessageMatcherDelegatingAuthorizationManager.Builder} for |
|
||||||
* further customizations |
|
||||||
*/ |
|
||||||
public MessageMatcherDelegatingAuthorizationManager.Builder add(MessageMatcher<?> matcher, |
|
||||||
AuthorizationManager<MessageAuthorizationContext<?>> manager) { |
|
||||||
Assert.notNull(matcher, "matcher cannot be null"); |
|
||||||
Assert.notNull(manager, "manager cannot be null"); |
|
||||||
this.mappings.add(new MessageMatcherEntry<>(matcher, manager)); |
|
||||||
return this; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Creates a {@link MessageMatcherDelegatingAuthorizationManager} instance. |
|
||||||
* @return the {@link MessageMatcherDelegatingAuthorizationManager} instance |
|
||||||
*/ |
|
||||||
public MessageMatcherDelegatingAuthorizationManager build() { |
|
||||||
return new MessageMatcherDelegatingAuthorizationManager(this.mappings); |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
@ -1,44 +0,0 @@ |
|||||||
/* |
|
||||||
* Copyright 2002-2022 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.util.matcher; |
|
||||||
|
|
||||||
/** |
|
||||||
* A rich object for associating a {@link MessageMatcher} to another object. |
|
||||||
* |
|
||||||
* @author Josh Cummings |
|
||||||
* @since 5.7 |
|
||||||
*/ |
|
||||||
public class MessageMatcherEntry<T> { |
|
||||||
|
|
||||||
private final MessageMatcher<?> messageMatcher; |
|
||||||
|
|
||||||
private final T entry; |
|
||||||
|
|
||||||
public MessageMatcherEntry(MessageMatcher requestMatcher, T entry) { |
|
||||||
this.messageMatcher = requestMatcher; |
|
||||||
this.entry = entry; |
|
||||||
} |
|
||||||
|
|
||||||
public MessageMatcher<?> getMessageMatcher() { |
|
||||||
return this.messageMatcher; |
|
||||||
} |
|
||||||
|
|
||||||
public T getEntry() { |
|
||||||
return this.entry; |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
Loading…
Reference in new issue