I have a web application that uses Kafka and Spring WebSocket for emitting different events to a mobile app. Events are triggered based on the logic where mobile app is online or offline.
The logic works perfectly fine if an event is triggered when mobile app is already online. But if it's offline, the triggered event is stored in a postgres table and marked with status PENDING. So when the mobile app comes back online, these table searches for the message and then triggers the event.
In order to update the mobile status, I am using Socket Sessions, and ChannelInterceptor which updates the mobile app status on SUBSCRIBE and DISCONNECT event.
I have logs at every possible place, and all the logs are executed when the mobile app comes back online. There's no error anywhere. The last log is right before :
messagingTemplate.convertAndSendToUser();
Logs :
Device Status in request : true
Send Mode : IMMEDIATE
inside immediate never expire >>>>>
Enter: SocketService.send() with argument[s] = [{Status=200, Response={PayloadId=1415, PayloadExpiryDateTime=0, PayloadPriority=1, EventType=NEW_MESSAGE, TemporaryMessage={Messages={Data=[{Title=Message1, DataToSend={Message=Message1, Data=bZijp8pcSKwnbRhnm2khEUkaonv3XpZ6PnS0IRP+ab4p1ub2Cmuuylga6z8RASwPWbTbxbZH5ickk4HcWKeEM1Qq8PHnmAEj/VmnLmhP9UbITDrFbzWaeUcAhdWuFzD1QKfp4lmjsFl6LeswN6x+tgFi+mimURK9EaKuKiQKnP3sZHfw6Bk2o/jH4ik8hoKetO2GRxNOKs/8H9NYUS4hLP/RP3vJttUtHo3fiJd3WwWcxT+w+YZneDFmc28ECjpoJ0ZzgV7NRJlhKBcOMl7V6eV2Rax7yNC2k7zyHH/ylkhkq74heWAtde/S43S6WLTMbtc2+t1MgHNOevsxEyI/ghBE1wJzgBIdvlhFX4M1jam+AYHyXhF7CNC/Hr/i+dpw3/SogNQwIidW8FZ5EWwBJP2nYv0eZ0owK/8zwEs3FZWF9fRUeUGn7VwFNFDsf4P1FUbeWcwHA847iHBuUF2qDVtLNgQtyejGKxhn3+a8yoKyU96cpteOmGmbZPuyU3xRk401gkk4a0noS3SOjWO8B0OKyq782K3gRUoVssPV8Hg=, ReqNr=9.0000001E7}}]}, Playlists={Data=[{Title=TemporaryPlaylist, DataToSend={Message=Temporary Playlist, Data=G7Prf2ZRUh+MSBNTjLH2BT7nNB1o8zXgz/mn9leZ7Q4UoZH1MzMrJj3ISvdkQZNAbVCnYQjh5XhOnUr1kGrZpWJ/lHUyJi1coc7fdMxe0fkgBJk9SySvaFN/TvFQaUlv2wRWIoPVXmLVhukFgzLsDeVWmuzNaWZXTQCemIzrK6SaCJ+FfFAzgxG2Z7QxWkVx, ReqNr=9.0000002E7}}]}, ShowCommands={Data=[{DataToSend={Type=ShowPlaylistRequest, ShowAll=[{CallWord=t2212161322}]}}]}}}, Message=Success}, message, 108]
Sending to Producer : {"Status":200,"Response":{"PayloadId":1415,"PayloadExpiryDateTime":0,"PayloadPriority":1,"EventType":"NEW_MESSAGE","TemporaryMessage":{"Messages":{"Data":[{"Title":"Message1","DataToSend":{"Message":"Message1","Data":"bZijp8pcSKwnbRhnm2khEUkaonv3XpZ6PnS0IRP+ab4p1ub2Cmuuylga6z8RASwPWbTbxbZH5ickk4HcWKeEM1Qq8PHnmAEj/VmnLmhP9UbITDrFbzWaeUcAhdWuFzD1QKfp4lmjsFl6LeswN6x+tgFi+mimURK9EaKuKiQKnP3sZHfw6Bk2o/jH4ik8hoKetO2GRxNOKs/8H9NYUS4hLP/RP3vJttUtHo3fiJd3WwWcxT+w+YZneDFmc28ECjpoJ0ZzgV7NRJlhKBcOMl7V6eV2Rax7yNC2k7zyHH/ylkhkq74heWAtde/S43S6WLTMbtc2+t1MgHNOevsxEyI/ghBE1wJzgBIdvlhFX4M1jam+AYHyXhF7CNC/Hr/i+dpw3/SogNQwIidW8FZ5EWwBJP2nYv0eZ0owK/8zwEs3FZWF9fRUeUGn7VwFNFDsf4P1FUbeWcwHA847iHBuUF2qDVtLNgQtyejGKxhn3+a8yoKyU96cpteOmGmbZPuyU3xRk401gkk4a0noS3SOjWO8B0OKyq782K3gRUoVssPV8Hg=","ReqNr":9.0000001E7}}]},"Playlists":{"Data":[{"Title":"TemporaryPlaylist","DataToSend":{"Message":"Temporary Playlist","Data":"G7Prf2ZRUh+MSBNTjLH2BT7nNB1o8zXgz/mn9leZ7Q4UoZH1MzMrJj3ISvdkQZNAbVCnYQjh5XhOnUr1kGrZpWJ/lHUyJi1coc7fdMxe0fkgBJk9SySvaFN/TvFQaUlv2wRWIoPVXmLVhukFgzLsDeVWmuzNaWZXTQCemIzrK6SaCJ+FfFAzgxG2Z7QxWkVx","ReqNr":9.0000002E7}}]},"ShowCommands":{"Data":[{"DataToSend":{"Type":"ShowPlaylistRequest","ShowAll":[{"CallWord":"t2212161322"}]}}]}}},"Message":"Success"}
Exit: SocketService.send() with result = null
Received from Kafka : {"Status":200,"Response":{"PayloadId":1415,"PayloadExpiryDateTime":0,"PayloadPriority":1,"EventType":"NEW_MESSAGE","TemporaryMessage":{"Messages":{"Data":[{"Title":"Message1","DataToSend":{"Message":"Message1","Data":"bZijp8pcSKwnbRhnm2khEUkaonv3XpZ6PnS0IRP+ab4p1ub2Cmuuylga6z8RASwPWbTbxbZH5ickk4HcWKeEM1Qq8PHnmAEj/VmnLmhP9UbITDrFbzWaeUcAhdWuFzD1QKfp4lmjsFl6LeswN6x+tgFi+mimURK9EaKuKiQKnP3sZHfw6Bk2o/jH4ik8hoKetO2GRxNOKs/8H9NYUS4hLP/RP3vJttUtHo3fiJd3WwWcxT+w+YZneDFmc28ECjpoJ0ZzgV7NRJlhKBcOMl7V6eV2Rax7yNC2k7zyHH/ylkhkq74heWAtde/S43S6WLTMbtc2+t1MgHNOevsxEyI/ghBE1wJzgBIdvlhFX4M1jam+AYHyXhF7CNC/Hr/i+dpw3/SogNQwIidW8FZ5EWwBJP2nYv0eZ0owK/8zwEs3FZWF9fRUeUGn7VwFNFDsf4P1FUbeWcwHA847iHBuUF2qDVtLNgQtyejGKxhn3+a8yoKyU96cpteOmGmbZPuyU3xRk401gkk4a0noS3SOjWO8B0OKyq782K3gRUoVssPV8Hg=","ReqNr":9.0000001E7}}]},"Playlists":{"Data":[{"Title":"TemporaryPlaylist","DataToSend":{"Message":"Temporary Playlist","Data":"G7Prf2ZRUh+MSBNTjLH2BT7nNB1o8zXgz/mn9leZ7Q4UoZH1MzMrJj3ISvdkQZNAbVCnYQjh5XhOnUr1kGrZpWJ/lHUyJi1coc7fdMxe0fkgBJk9SySvaFN/TvFQaUlv2wRWIoPVXmLVhukFgzLsDeVWmuzNaWZXTQCemIzrK6SaCJ+FfFAzgxG2Z7QxWkVx","ReqNr":9.0000002E7}}]},"ShowCommands":{"Data":[{"DataToSend":{"Type":"ShowPlaylistRequest","ShowAll":[{"CallWord":"t2212161322"}]}}]}}},"Message":"Success"}
Sending to Socket Client.......
But the front-end never gets the message.
Below are some code snippets:
Channel Interceptor:
public class SocketInterceptor implements ChannelInterceptor {
#Autowired
SocketSessionService socketSessionService;
private final Logger log = LoggerFactory.getLogger(SocketInterceptor.class);
#Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
MessageHeaders headers = message.getHeaders();
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
MultiValueMap<String, String> multiValueMap = headers.get(StompHeaderAccessor.NATIVE_HEADERS,MultiValueMap.class);
if (multiValueMap !=null) {
if (multiValueMap.containsKey("destination")) {
String destination = multiValueMap.get("destination").get(0);
log.info("Destination in interceptor : " + destination);
}
}
if (accessor.getCommand()!=null) {
log.info("Client Method : " + accessor.getCommand().name());
if (accessor.getCommand().equals(StompCommand.CONNECT) || accessor.getCommand().equals(StompCommand.SUBSCRIBE) || accessor.getCommand().equals(StompCommand.SEND)) {
if (!multiValueMap.containsKey("access-token")) {
log.error("Token Error " + "No Token was provided in + " + accessor.getCommand().name());
throw new MessagingException("Forbidden: No Token was provided in header for Socket Command: " + accessor.getCommand().name());
}
String token = multiValueMap.get("access-token").get(0);
if (JwtTokenUtil.verifyToken(JwtTokenUtil.decrypt(token))) {
log.info("Token Verified Successfully >>>>>>>>>>>>>>>>>>>> ");
Claims claims = JwtTokenUtil.verifyJwtToken(JwtTokenUtil.decrypt(token));
accessor.setHeader("access-token", token);
Principal principal = new UsernamePasswordAuthenticationToken(claims.getId(), null, null);
accessor.setUser(principal);
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
claims.getId(), null, null);
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
if (accessor.getCommand().equals(StompCommand.SUBSCRIBE)){
log.info("Storing Socket Session on Socket Command SUBSCRIBE ");
SocketSessionModel model=new SocketSessionModel();
model.setSessionId(accessor.getSessionId());
model.setDeviceId(multiValueMap.get("destination").get(0));
model.setAccessToken(token);
model.setCurrentStatus(SocketSessionStatus.ONLINE);
model.setReportedOnlineAt(ZonedDateTime.now().with(ZoneOffset.UTC));
model.setReportedOfflineAt(null);
socketSessionService.onSubscribe(model);
}
ObjectMapper objectMapper= new ObjectMapper();
try {
log.info("Returning back the frame : " + objectMapper.writeValueAsString(message.getPayload()));
} catch (Exception e){
e.printStackTrace();
}
return new GenericMessage<>(message.getPayload(), accessor.getMessageHeaders());
}
throw new MessagingException("Token either Expired or Invalid");
}
}
return message;
}
}
WebSocket Config :
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
Logger log= LoggerFactory.getLogger(WebSocketConfig.class);
#Autowired
SocketSessionService socketSessionService;
#Override
public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {
stompEndpointRegistry.addEndpoint("/ws")
.setAllowedOrigins("*")
.withSockJS();
}
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/user")
.setTaskScheduler(customTaskScheduler())
.setHeartbeatValue(new long[]{25000,10000});
config.setApplicationDestinationPrefixes("/app");
config.setUserDestinationPrefix("/user");
}
#Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(socketInterceptor());
}
#Bean
public SocketInterceptor socketInterceptor() {
return new SocketInterceptor();
}
#Bean
public TaskScheduler customTaskScheduler(){
return new ThreadPoolTaskScheduler();
}
#EventListener(SessionDisconnectEvent.class)
public void handleWsDisconnectListener(SessionDisconnectEvent event) {
Optional<String> sessionId= getSessionIdFromEvent(event);
sessionId.ifPresent(s -> socketSessionService.onDisconnect(s));
}
private Optional<String> getSessionIdFromEvent(AbstractSubProtocolEvent event) {
String sessionId = null;
System.out.println("Session Id in event : " + (event.getMessage().getHeaders()).get("simpSessionId"));
Object sessionIdAsObject = (event.getMessage().getHeaders()).get("simpSessionId");
if (nonNull(sessionIdAsObject) && sessionIdAsObject.getClass().equals(String.class)) {
sessionId = (String) sessionIdAsObject;
}
return Optional.ofNullable(StringUtils.isEmpty(sessionId) ? null : sessionId);
}
}
Kafka Producer Config :
#Service
public class KafkaProducerConfig {
#Autowired
private KafkaTemplate<String, String> kafkaTemplate;
#Async
public void send(String data, String kafkaTopic, String key) {
kafkaTemplate.send(kafkaTopic, key, data);
}
#Async
public void send(String topic, String data ){
kafkaTemplate.send(topic, data);
}
}
KafkaConsumer Config :
#EnableKafka
#Configuration
public class KafkaConsumerConfig {
#Value("${spring.kafka.bootstrap-servers}")
private String bootstrapServers;
public ConsumerFactory<String, String> consumerFactory(String groupId) {
Map<String, Object> props = new HashMap<>();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
return new DefaultKafkaConsumerFactory<>(props);
}
#Bean
public ConcurrentKafkaListenerContainerFactory<String, String> rawKafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory("something"));
return factory;
}
}
Kafka Listener :
This is the last step where socket emits the messages to clients. The last line of log mentioned here is executed.
#Component
public class KafkaListenerForSocket {
private final Logger log= LoggerFactory.getLogger(KafkaListenerForSocket.class);
#Autowired
SimpMessagingTemplate messagingTemplate;
#KafkaListener(topics = KafkaTopicConstants.MESSAGE, containerFactory = "rawKafkaListenerContainerFactory")
public void listenToMessages(ConsumerRecord<String, String> consumerRecord) throws IOException {
ObjectMapper objectMapper=new ObjectMapper();
String destination="/queue/message";
BaseSocketResponse response=objectMapper.readValue(consumerRecord.value(), BaseSocketResponse.class);
messagingTemplate.convertAndSendToUser(consumerRecord.key(),destination,response);
}
public void send(Object data, String key){
log.info("Bypassing Kafka and sending directly");
String destination="/queue/message";
log.info("sending to destination : " + "/user/"+key+destination);
messagingTemplate.convertAndSendToUser(key, destination, data);
}
}
BaseSocketResponse.Java class :
public class BaseSocketResponse<T> {
#JsonProperty(value = "Status", required = true)
private Integer status;
#JsonProperty(value = "Message", required = true)
private String statusMessage = "";
#JsonProperty(value = "Response", required = true)
private T response;
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public String getStatusMessage() {
return statusMessage;
}
public void setStatusMessage(String statusMessage) {
this.statusMessage = statusMessage;
}
public T getResponse() {
return response;
}
public void setResponse(T response) {
this.response = response;
}
}
front-end code :
var stompClient = null;
function setConnected(connected) {
$("#connect").prop("disabled", connected);
$("#disconnect").prop("disabled", !connected);
if (connected) {
$("#conversation").show();
}
else {
$("#conversation").hide();
}
$("#userinfo").html("");
}
function connect() {
let header={"access-token" : $("#token").val()};
var socket = new SockJS('/services/toplight/ws');
stompClient = Stomp.over(socket);
stompClient.connect(header, function (frame) {
if(frame.command == "CONNECTED") {
console.log("Inside frame command : >>>>> " + frame);
setConnected(true);
subscribe(stompClient)
}
}, (error) => {
console.log("Inside Connect error : " + error );
onConnectError(error);
});
}
function subscribe(stompClient){
var token=$("#token").val();
var header={"access-token": token};
console.log("Subscribing to Destination : " + '/user/'+$("#name").val()+'/queue/message');
stompClient.subscribe('/user/'+$("#name").val()+'/queue/message' , greeting, header);
var greeting=function(message){
var body=JSON.parse(message.body);
if(body.Response.EventType=="NEW_MESSAGE"){
showNewMessage(response);
}
if(body.Response.EventType=="DEVICE_UPDATE_RESPONSE"){
showDeviceUpdate(response);
}
if(body.Response.EventType=="MESSAGE_UPDATE_RESPONSE"){
showMessageUpdate(response);
}
};
}
function onConnectError(error){
console.log("Error : >>>>> " + error);
$("#greetings").append("<h3><tr><td>" + error + "</h3>");
}
I am using SockJS + STOMP + Spring WebSocket with a configuration that receives user credentials during the handshake. The credentials are received in the configureClientInboundChannel method:
#Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new ChannelInterceptor() {
#Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message,
StompHeaderAccessor.class);
if (accessor != null && (StompCommand.CONNECT.equals(accessor.getCommand())
|| StompCommand.SEND.equals(accessor.getCommand()))) {
List<String> auth = accessor.getNativeHeader("Authorization");
System.out.printf("Authorization: %s%n", auth.get(0));
}
return message;
}
});
}
I would like to add a Principal based on the token I receive in the header. I have a handler that registers Principals:
public class PrincipalHandshakeHandler extends DefaultHandshakeHandler {
#Override
protected Principal determineUser(ServerHttpRequest request,
WebSocketHandler handler, Map<String, Object> attributes) {
System.out.println(attributes);
return new Principal() {
#Override
public String getName() {
return userId;
}
};
}
}
But how do I get pass the token I get in configureClientInboundChannel to the above handshake handler? Basically, the token should go there in place of userId.
I have problem with #DeleteMapping.
Situation is like below.
If I request to /v1/cache/{cacheEntry} with method DELETE,
It respond with 404, but body was empty. no message, no spring default json 404 response message.
If i request to /v1/cache/{cacheEntry} with method POST,
It respond with 405 and body was below. (This action is correct, not a bug.)
If I change #DeleteMapping to #PostMapping, and request /v1/cache/{cacheEntry} with method POST, It respond success with code 200.
{
"timestamp": 1643348039913,
"status": 405,
"error": "Method Not Allowed",
"message": "",
"path": "/v1/cache/{cacheEntry}"
}
// Controller
#Slf4j
#RestController
#RequestMapping("/v1/cache")
#RequiredArgsConstructor
public class CacheController {
private final CacheService cacheService;
#PostMapping("/{cacheEntry}")
public CacheClearResponse clearCacheEntry(#PathVariable("cacheEntry") CacheChannels cacheEntry) {
try {
log.info("Cache entry :: " + cacheEntry);
cacheService.evictCacheEntry(cacheEntry);
return CacheClearResponse.builder()
.result(
RequestResult.builder()
.code(9200)
.message("SUCCESS")
.build()
)
.common(
Common.builder().build()
)
.date(LocalDateTime.now())
.build();
} catch (Exception e) {
e.printStackTrace();
StringWriter sw = new StringWriter();
e.printStackTrace(new PrintWriter(sw));
return CacheClearResponse.builder()
.result(
RequestResult.builder()
.code(9999)
.message(sw.toString())
.build()
)
.common(
Common.builder().build()
)
.date(LocalDateTime.now())
.build();
}
}
}
}
// CacheService
#Service
#RequiredArgsConstructor
public class CacheService {
private final CacheManager cacheManager;
public void evictCacheEntry(CacheChannels cacheEntry) {
Cache cache = cacheManager.getCache(cacheEntry.getCacheName());
if (cache != null) {
cache.clear();
}
}
public void evictCache(CacheChannels cacheEntry, String cacheKey) {
Cache cache = cacheManager.getCache(cacheEntry.getCacheName());
if (cache != null) {
cache.evict(cacheKey);
}
}
}
// Enum
#Getter
#AllArgsConstructor
public enum CacheChannels {
CACHE_TEN_MIN(Names.CACHE_TEN_MIN, Duration.ofMinutes(10)),
CACHE_HALF_HR(Names.CACHE_HALF_HR, Duration.ofMinutes(30)),
CACHE_ONE_HR(Names.CACHE_ONE_HR, Duration.ofHours(1)),
CACHE_THREE_HR(Names.CACHE_THREE_HR, Duration.ofHours(3)),
CACHE_SIX_HR(Names.CACHE_SIX_HR, Duration.ofHours(6)),
CACHE_ONE_DAY(Names.CACHE_ONE_DAY, Duration.ofDays(1));
private final String cacheName;
private final Duration cacheTTL;
public static CacheChannels from(String value) {
return Arrays.stream(values())
.filter(cacheChannel -> cacheChannel.cacheName.equalsIgnoreCase(value))
.findAny()
.orElse(null);
}
public static class Names {
public static final String CACHE_TEN_MIN = "cache10Minutes";
public static final String CACHE_HALF_HR = "cache30Minutes";
public static final String CACHE_ONE_HR = "cache1Hour";
public static final String CACHE_THREE_HR = "cache3Hours";
public static final String CACHE_SIX_HR = "cache6Hours";
public static final String CACHE_ONE_DAY = "cache1Day";
}
}
// Converter
#Slf4j
public class StringToCacheChannelConverter implements Converter<String, CacheChannels> {
#Override
public CacheChannels convert(String source) {
log.info("Convert Target: " + source);
return CacheChannels.from(source);
}
}
// Security Config
#Configuration
#EnableWebSecurity
#Order(1)
public class APISecurityConfig extends WebSecurityConfigurerAdapter {
#Value("${spring.security.auth-token-header-name:Authorization}")
private String apiKeyHeader;
#Value("${spring.security.secret}")
private String privateApiKey;
#Override
protected void configure(HttpSecurity http) throws Exception {
APIKeyAuthFilter filter = new APIKeyAuthFilter(apiKeyHeader);
filter.setAuthenticationManager(new AuthenticationManager() {
#Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
String requestedApiKey = (String) authentication.getPrincipal();
if (!privateApiKey.equals(requestedApiKey)) {
throw new BadCredentialsException("The API Key was not found or not the expected value");
}
authentication.setAuthenticated(true);
return authentication;
}
});
http
.csrf().disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilter(filter)
.authorizeRequests()
.antMatchers("/v1/cache/**")
.authenticated();
}
}
// Filter
#Slf4j
public class APIKeyAuthFilter extends AbstractPreAuthenticatedProcessingFilter {
private String apiKeyHeader;
public APIKeyAuthFilter(String apiKeyHeader) {
this.apiKeyHeader = apiKeyHeader;
}
#Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest httpServletRequest) {
log.info("Check authenticated.");
return httpServletRequest.getHeader(apiKeyHeader);
}
#Override
protected Object getPreAuthenticatedCredentials(HttpServletRequest httpServletRequest) {
return "N/A";
}
}
// Web Config
#Configuration
public class WebConfig implements WebMvcConfigurer {
#Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new StringToCacheChannelConverter());
}
#Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new HiddenHttpMethodFilter();
}
}
This can be expected the controller was loaded, endpoint was mapped.
I tried change #DeleteMapping to #PostMapping and it was successfully respond against to POST request.
What am I missing?
I found reason why received 404 without any messages.
My tomcat is on remote server. It configured with security-constraint and disable DELETE method for all enpoints.
I just comment out it and It work properly with delete method.
I am trying to obtain the currently authenticated user in the controller for websockets. The problem is, I cannot access the user using SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getId().
I have tried to give Principal as a parameter to the method but it returns principal null.
Security configuration:
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/connect").setAllowedOrigins("*");
}
#Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic/messages");
registry.setApplicationDestinationPrefixes("/ws");
}
}
Controller for websocket:
#Controller
public class MessageController {
#Autowired
private Consumer consumer;
#Autowired
private Utils utils;
#Autowired
private PersonService personService;
#Autowired
SimpMessagingTemplate simpMessagingTemplate;
String destination = "/topic/messages";
ExecutorService executorService =
Executors.newFixedThreadPool(1);
Future<?> submittedTask;
#MessageMapping("/start")
public void startTask(Principal principal){
// Here, I would like to get the logged in user
// If I use principal like this: principal.getName() => NullPointerException
if ( submittedTask != null ){
simpMessagingTemplate.convertAndSend(destination,
"Task already started");
return;
}
simpMessagingTemplate.convertAndSendToUser(sha.getUser().getName(), destination,
"Started task");
submittedTask = executorService.submit(() -> {
while(true){
simpMessagingTemplate.convertAndSend(destination,
// "The calculated value " + val + " is equal to : " + max);
}
});
}
How can I get the authenticated user? I needed it to check when to start the task for the web socket
Try to implement ChannelInterceptor, that need to be registrated in Config file (class that implements WebSocketMessageBrokerConfigurer)
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
private final ChannelInterceptor serverPushInBoundInterceptor;
#Autowired
public WebSocketConfig(#Qualifier("serverPushInBoundInterceptor") ChannelInterceptor serverPushInBoundInterceptor) {
this.serverPushInBoundInterceptor = serverPushInBoundInterceptor;
}
....
#Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(serverPushInBoundInterceptor);
}
}
#Component("serverPushInBoundInterceptor")
public class ServerPushInBoundInterceptor implements ChannelInterceptor {
private static final Logger log = LoggerFactory.getLogger(ServerPushInBoundInterceptor.class);
#Override
#SuppressWarnings("NullableProblems")
public Message<?> postReceive(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT.equals(Objects.requireNonNull(accessor).getCommand())) {
List<String> authorization = accessor.getNativeHeader("Authorization");
if (authorization != null && !authorization.isEmpty()) {
String auth = authorization.get(0).split(" ")[1];
System.out.println(auth);
try {
// find Principal
Principal principal = ...
accessor.setUser(new UsernamePasswordAuthenticationToken(principal, principal.getCredentials(), principal.getAuthorities()));
} catch (Exception exc) {
log.error("preSend", exc);
}
}
}
return message;
}
}
I have a spring application.
I need to put a value to the initial handshake.
The url looks like: ws://localhost:8080/chat?key=value
I need this key=value in my Websocket Handler.
How can I access it?
Websocket Configuration:
#Configuration
#EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
#Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
// alle origins erlauben
registry.addHandler(chatWebSocketController(), "/chat").addInterceptors(new HttpSessionHandshakeInterceptor())
.setAllowedOrigins("*");
}
#Bean
public ChatWebSocketController chatWebSocketController() {
return new ChatWebSocketController();
}
}
Websocket Handler method:
#Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
if (session.getAttributes().containsKey("key")) {
List<String> userMap = session.getHandshakeHeaders().get("key");
JwtTokenUtil jwtTokenUtil = new JwtTokenUtil();
String token = userMap.get(0);
if (jwtTokenUtil.validateToken(token)) {
User userToStore = new User(jwtTokenUtil.getUsernameFromToken(token));
userUsernameMap.put(session, userToStore);
LOGGER.info("User with name " + jwtTokenUtil.getUsernameFromToken(token) + "and IP "
+ session.getRemoteAddress() + " successfully connected");
sendConnectMessage(session, userToStore);
}
} else {
session.close(CloseStatus.POLICY_VIOLATION);
}
}
Found the solution by myself. You have to write your own HandshakeInterceptor, there you have access to the http parameter. so you can put this to your attribbutes map.
public class HttpHandshakeInterceptor implements HandshakeInterceptor {
#Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Map<String, Object> attributes) throws Exception {
if (request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
HttpSession session = servletRequest.getServletRequest().getSession();
attributes.put("sessionId", session.getId());
attributes.put("key", servletRequest.getServletRequest().getParameterMap().get("key"));
}
return true;
}
#Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Exception exception) {
//nothing to do
}
}