Allow use of @SendToUser even w/o authenticated user
Before this change, subscribing to a user destination and use of
@SendToUser annotation required an authenticated user.
This change makes it possible to subscribe to a user destination from
WebSocket sessions without an authenticated user. In such cases the
destination is associated with one session only rather than with a
user (and all their sessions).
It is then also possible to send a message to a user destination
via "/user/{sessionId}/.." rather than "/user/{user}/...".
That means @SendToUser works relying on the session id of the input
message, effectively sending a reply to destination private to the
session.
A key use case for this is handling an exception with an
@MessageExceptionHandler method and sending a reply with @SendToUser.
Issue: SPR-11309
This commit is contained in:
@@ -145,10 +145,18 @@ public class SendToMethodReturnValueHandler implements HandlerMethodReturnValueH
|
||||
|
||||
SendToUser sendToUser = returnType.getMethodAnnotation(SendToUser.class);
|
||||
if (sendToUser != null) {
|
||||
boolean broadcast = sendToUser.broadcast();
|
||||
String user = getUserName(message, headers);
|
||||
if (user == null) {
|
||||
if (sessionId == null) {
|
||||
throw new MissingSessionUserException(message);
|
||||
}
|
||||
user = sessionId;
|
||||
broadcast = false;
|
||||
}
|
||||
String[] destinations = getTargetDestinations(sendToUser, message, this.defaultUserDestinationPrefix);
|
||||
for (String destination : destinations) {
|
||||
if (sendToUser.broadcast()) {
|
||||
if (broadcast) {
|
||||
this.messagingTemplate.convertAndSendToUser(user, destination, returnValue);
|
||||
}
|
||||
else {
|
||||
@@ -168,13 +176,11 @@ public class SendToMethodReturnValueHandler implements HandlerMethodReturnValueH
|
||||
|
||||
protected String getUserName(Message<?> message, MessageHeaders headers) {
|
||||
Principal principal = SimpMessageHeaderAccessor.getUser(headers);
|
||||
if (principal == null) {
|
||||
throw new MissingSessionUserException(message);
|
||||
if (principal != null) {
|
||||
return (principal instanceof DestinationUserNameProvider ?
|
||||
((DestinationUserNameProvider) principal).getDestinationUserName() : principal.getName());
|
||||
}
|
||||
if (principal instanceof DestinationUserNameProvider) {
|
||||
return ((DestinationUserNameProvider) principal).getDestinationUserName();
|
||||
}
|
||||
return principal.getName();
|
||||
return null;
|
||||
}
|
||||
|
||||
protected String[] getTargetDestinations(Annotation annotation, Message<?> message, String defaultPrefix) {
|
||||
|
||||
@@ -107,14 +107,15 @@ public class DefaultUserDestinationResolver implements UserDestinationResolver {
|
||||
return null;
|
||||
}
|
||||
|
||||
Set<String> targetDestinations = new HashSet<String>();
|
||||
Set<String> resolved = new HashSet<String>();
|
||||
for (String sessionId : info.getSessionIds()) {
|
||||
targetDestinations.add(getTargetDestination(destination,
|
||||
info.getDestinationWithoutPrefix(), sessionId, info.getUser()));
|
||||
String d = getTargetDestination(destination, info.getDestinationWithoutPrefix(), sessionId, info.getUser());
|
||||
if (d != null) {
|
||||
resolved.add(d);
|
||||
}
|
||||
}
|
||||
|
||||
return new UserDestinationResult(destination,
|
||||
targetDestinations, info.getSubscribeDestination(), info.getUser());
|
||||
return new UserDestinationResult(destination, resolved, info.getSubscribeDestination(), info.getUser());
|
||||
}
|
||||
|
||||
private DestinationInfo parseUserDestination(Message<?> message) {
|
||||
@@ -134,17 +135,13 @@ public class DefaultUserDestinationResolver implements UserDestinationResolver {
|
||||
if (!checkDestination(destination, this.destinationPrefix)) {
|
||||
return null;
|
||||
}
|
||||
if (principal == null) {
|
||||
logger.error("Ignoring message, no principal info available");
|
||||
return null;
|
||||
}
|
||||
if (sessionId == null) {
|
||||
logger.error("Ignoring message, no session id available");
|
||||
return null;
|
||||
}
|
||||
destinationWithoutPrefix = destination.substring(this.destinationPrefix.length()-1);
|
||||
subscribeDestination = destination;
|
||||
user = principal.getName();
|
||||
user = (principal != null ? principal.getName() : null);
|
||||
sessionIds = Collections.singleton(sessionId);
|
||||
}
|
||||
else if (SimpMessageType.MESSAGE.equals(messageType)) {
|
||||
@@ -153,11 +150,12 @@ public class DefaultUserDestinationResolver implements UserDestinationResolver {
|
||||
}
|
||||
int startIndex = this.destinationPrefix.length();
|
||||
int endIndex = destination.indexOf('/', startIndex);
|
||||
Assert.isTrue(endIndex > 0, "Expected destination pattern \"/principal/{userId}/**\"");
|
||||
Assert.isTrue(endIndex > 0, "Expected destination pattern \"/user/{userId}/**\"");
|
||||
destinationWithoutPrefix = destination.substring(endIndex);
|
||||
subscribeDestination = this.destinationPrefix.substring(0, startIndex-1) + destinationWithoutPrefix;
|
||||
user = destination.substring(startIndex, endIndex);
|
||||
user = StringUtils.replace(user, "%2F", "/");
|
||||
user = user.equals(sessionId) ? null : user;
|
||||
sessionIds = (sessionId != null ?
|
||||
Collections.singleton(sessionId) : this.userSessionRegistry.getSessionIds(user));
|
||||
}
|
||||
@@ -186,14 +184,16 @@ public class DefaultUserDestinationResolver implements UserDestinationResolver {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the target destination to use. Provided as input are the original source
|
||||
* destination, as well as the same destination with the target prefix removed.
|
||||
* This methods determines the translated destination to use based on the source
|
||||
* destination, the source destination with the user prefix removed, a session
|
||||
* id, and the user for the session (if known).
|
||||
*
|
||||
* @param sourceDestination the source destination from the input message
|
||||
* @param sourceDestinationWithoutPrefix the source destination with the target prefix removed
|
||||
* @param sessionId an active user session id
|
||||
* @param user the user
|
||||
* @return the target destination
|
||||
* @param sourceDestination the source destination of the input message
|
||||
* @param sourceDestinationWithoutPrefix the source destination without the user prefix
|
||||
* @param sessionId the id of the session for the target message
|
||||
* @param user the user associated with the session, or {@code null}
|
||||
*
|
||||
* @return a target destination, or {@code null}
|
||||
*/
|
||||
protected String getTargetDestination(String sourceDestination,
|
||||
String sourceDestinationWithoutPrefix, String sessionId, String user) {
|
||||
|
||||
@@ -171,12 +171,16 @@ public class UserDestinationMessageHandler implements MessageHandler, SmartLifec
|
||||
}
|
||||
Set<String> destinations = result.getTargetDestinations();
|
||||
if (destinations.isEmpty()) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("No target destinations, message=" + message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (SimpMessageType.MESSAGE.equals(SimpMessageHeaderAccessor.getMessageType(message.getHeaders()))) {
|
||||
SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.wrap(message);
|
||||
initHeaders(headerAccessor);
|
||||
headerAccessor.setNativeHeader(SimpMessageHeaderAccessor.ORIGINAL_DESTINATION, result.getSubscribeDestination());
|
||||
String header = SimpMessageHeaderAccessor.ORIGINAL_DESTINATION;
|
||||
headerAccessor.setNativeHeader(header, result.getSubscribeDestination());
|
||||
message = MessageBuilder.createMessage(message.getPayload(), headerAccessor.getMessageHeaders());
|
||||
}
|
||||
for (String destination : destinations) {
|
||||
|
||||
@@ -45,7 +45,6 @@ public class UserDestinationResult {
|
||||
Assert.notNull(sourceDestination, "'sourceDestination' must not be null");
|
||||
Assert.notNull(targetDestinations, "'targetDestinations' must not be null");
|
||||
Assert.notNull(subscribeDestination, "'subscribeDestination' must not be null");
|
||||
Assert.notNull(user, "'user' must not be null");
|
||||
|
||||
this.sourceDestination = sourceDestination;
|
||||
this.targetDestinations = targetDestinations;
|
||||
|
||||
@@ -186,4 +186,13 @@ public class NativeMessageHeaderAccessor extends MessageHeaderAccessor {
|
||||
setModified(true);
|
||||
}
|
||||
|
||||
public List<String> removeNativeHeader(String name) {
|
||||
Assert.state(isMutable(), "Already immutable");
|
||||
Map<String, List<String>> nativeHeaders = getNativeHeaders();
|
||||
if (nativeHeaders == null) {
|
||||
return null;
|
||||
}
|
||||
return nativeHeaders.remove(name);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user