All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.springframework.messaging.simp.user.DefaultUserDestinationResolver Maven / Gradle / Ivy

There is a newer version: 6.1.6
Show newest version
/*
 * Copyright 2002-2018 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
 *
 *      http://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.messaging.simp.user;

import java.security.Principal;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import org.apache.commons.logging.Log;

import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.simp.SimpLogging;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.util.Assert;
import org.springframework.util.PathMatcher;
import org.springframework.util.StringUtils;

/**
 * A default implementation of {@code UserDestinationResolver} that relies
 * on a {@link SimpUserRegistry} to find active sessions for a user.
 *
 * 

When a user attempts to subscribe, e.g. to "/user/queue/position-updates", * the "/user" prefix is removed and a unique suffix added based on the session * id, e.g. "/queue/position-updates-useri9oqdfzo" to ensure different users can * subscribe to the same logical destination without colliding. * *

When sending to a user, e.g. "/user/{username}/queue/position-updates", the * "/user/{username}" prefix is removed and a suffix based on active session id's * is added, e.g. "/queue/position-updates-useri9oqdfzo". * * @author Rossen Stoyanchev * @author Brian Clozel * @since 4.0 */ public class DefaultUserDestinationResolver implements UserDestinationResolver { private static final Log logger = SimpLogging.forLogName(DefaultUserDestinationResolver.class); private final SimpUserRegistry userRegistry; private String prefix = "/user/"; private boolean removeLeadingSlash = false; /** * Create an instance that will access user session id information through * the provided registry. * @param userRegistry the registry, never {@code null} */ public DefaultUserDestinationResolver(SimpUserRegistry userRegistry) { Assert.notNull(userRegistry, "SimpUserRegistry must not be null"); this.userRegistry = userRegistry; } /** * Return the configured {@link SimpUserRegistry}. */ public SimpUserRegistry getSimpUserRegistry() { return this.userRegistry; } /** * The prefix used to identify user destinations. Any destinations that do not * start with the given prefix are not be resolved. *

The default prefix is "/user/". * @param prefix the prefix to use */ public void setUserDestinationPrefix(String prefix) { Assert.hasText(prefix, "Prefix must not be empty"); this.prefix = (prefix.endsWith("/") ? prefix : prefix + "/"); } /** * Return the configured prefix for user destinations. */ public String getDestinationPrefix() { return this.prefix; } /** * Use this property to indicate whether the leading slash from translated * user destinations should be removed or not. This depends on the * destination prefixes the message broker is configured with. *

By default this is set to {@code false}, i.e. * "do not change the target destination", although * {@link org.springframework.messaging.simp.config.AbstractMessageBrokerConfiguration * AbstractMessageBrokerConfiguration} may change that to {@code true} * if the configured destinations do not have a leading slash. * @param remove whether to remove the leading slash * @since 4.3.14 */ public void setRemoveLeadingSlash(boolean remove) { this.removeLeadingSlash = remove; } /** * Whether to remove the leading slash from target destinations. * @since 4.3.14 */ public boolean isRemoveLeadingSlash() { return this.removeLeadingSlash; } /** * Provide the {@code PathMatcher} in use for working with destinations * which in turn helps to determine whether the leading slash should be * kept in actual destinations after removing the * {@link #setUserDestinationPrefix userDestinationPrefix}. *

By default actual destinations have a leading slash, e.g. * {@code /queue/position-updates} which makes sense with brokers that * support destinations with slash as separator. When a {@code PathMatcher} * is provided that supports an alternative separator, then resulting * destinations won't have a leading slash, e.g. {@code * jms.queue.position-updates}. * @param pathMatcher the PathMatcher used to work with destinations * @since 4.3 * @deprecated as of 4.3.14 this property is no longer used and is replaced * by {@link #setRemoveLeadingSlash(boolean)} that indicates more explicitly * whether to keep the leading slash which may or may not be the case * regardless of how the {@code PathMatcher} is configured. */ @Deprecated public void setPathMatcher(@Nullable PathMatcher pathMatcher) { // Do nothing } @Override @Nullable public UserDestinationResult resolveDestination(Message message) { ParseResult parseResult = parse(message); if (parseResult == null) { return null; } String user = parseResult.getUser(); String sourceDestination = parseResult.getSourceDestination(); Set targetSet = new HashSet<>(); for (String sessionId : parseResult.getSessionIds()) { String actualDestination = parseResult.getActualDestination(); String targetDestination = getTargetDestination( sourceDestination, actualDestination, sessionId, user); if (targetDestination != null) { targetSet.add(targetDestination); } } String subscribeDestination = parseResult.getSubscribeDestination(); return new UserDestinationResult(sourceDestination, targetSet, subscribeDestination, user); } @Nullable private ParseResult parse(Message message) { MessageHeaders headers = message.getHeaders(); String sourceDestination = SimpMessageHeaderAccessor.getDestination(headers); if (sourceDestination == null || !checkDestination(sourceDestination, this.prefix)) { return null; } SimpMessageType messageType = SimpMessageHeaderAccessor.getMessageType(headers); if (messageType != null) { switch (messageType) { case SUBSCRIBE: case UNSUBSCRIBE: return parseSubscriptionMessage(message, sourceDestination); case MESSAGE: return parseMessage(headers, sourceDestination); } } return null; } @Nullable private ParseResult parseSubscriptionMessage(Message message, String sourceDestination) { MessageHeaders headers = message.getHeaders(); String sessionId = SimpMessageHeaderAccessor.getSessionId(headers); if (sessionId == null) { logger.error("No session id. Ignoring " + message); return null; } int prefixEnd = this.prefix.length() - 1; String actualDestination = sourceDestination.substring(prefixEnd); if (isRemoveLeadingSlash()) { actualDestination = actualDestination.substring(1); } Principal principal = SimpMessageHeaderAccessor.getUser(headers); String user = (principal != null ? principal.getName() : null); Set sessionIds = Collections.singleton(sessionId); return new ParseResult(sourceDestination, actualDestination, sourceDestination, sessionIds, user); } private ParseResult parseMessage(MessageHeaders headers, String sourceDest) { int prefixEnd = this.prefix.length(); int userEnd = sourceDest.indexOf('/', prefixEnd); Assert.isTrue(userEnd > 0, "Expected destination pattern \"/user/{userId}/**\""); String actualDest = sourceDest.substring(userEnd); String subscribeDest = this.prefix.substring(0, prefixEnd - 1) + actualDest; String userName = sourceDest.substring(prefixEnd, userEnd); userName = StringUtils.replace(userName, "%2F", "/"); String sessionId = SimpMessageHeaderAccessor.getSessionId(headers); Set sessionIds; if (userName.equals(sessionId)) { userName = null; sessionIds = Collections.singleton(sessionId); } else { sessionIds = getSessionIdsByUser(userName, sessionId); } if (isRemoveLeadingSlash()) { actualDest = actualDest.substring(1); } return new ParseResult(sourceDest, actualDest, subscribeDest, sessionIds, userName); } private Set getSessionIdsByUser(String userName, @Nullable String sessionId) { Set sessionIds; SimpUser user = this.userRegistry.getUser(userName); if (user != null) { if (sessionId != null && user.getSession(sessionId) != null) { sessionIds = Collections.singleton(sessionId); } else { Set sessions = user.getSessions(); sessionIds = new HashSet<>(sessions.size()); for (SimpSession session : sessions) { sessionIds.add(session.getId()); } } } else { sessionIds = Collections.emptySet(); } return sessionIds; } protected boolean checkDestination(String destination, String requiredPrefix) { return destination.startsWith(requiredPrefix); } /** * This method determines how to translate the source "user" destination to an * actual target destination for the given active user session. * @param sourceDestination the source destination from the input message. * @param actualDestination a subset of the destination without any user prefix. * @param sessionId the id of an active user session, never {@code null}. * @param user the target user, possibly {@code null}, e.g if not authenticated. * @return a target destination, or {@code null} if none */ @SuppressWarnings("unused") @Nullable protected String getTargetDestination(String sourceDestination, String actualDestination, String sessionId, @Nullable String user) { return actualDestination + "-user" + sessionId; } @Override public String toString() { return "DefaultUserDestinationResolver[prefix=" + this.prefix + "]"; } /** * A temporary placeholder for a parsed source "user" destination. */ private static class ParseResult { private final String sourceDestination; private final String actualDestination; private final String subscribeDestination; private final Set sessionIds; @Nullable private final String user; public ParseResult(String sourceDest, String actualDest, String subscribeDest, Set sessionIds, @Nullable String user) { this.sourceDestination = sourceDest; this.actualDestination = actualDest; this.subscribeDestination = subscribeDest; this.sessionIds = sessionIds; this.user = user; } public String getSourceDestination() { return this.sourceDestination; } public String getActualDestination() { return this.actualDestination; } public String getSubscribeDestination() { return this.subscribeDestination; } public Set getSessionIds() { return this.sessionIds; } @Nullable public String getUser() { return this.user; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy