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:
Rossen Stoyanchev
2014-05-08 19:18:53 -04:00
parent 3c7bb9c279
commit 97fb308b6b
13 changed files with 229 additions and 70 deletions

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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);
}
}