I'm trying to implement Oauth2 login with Dagger2. Once the access_token gets expired, I have successfully generated new access_token through the refresh_token, but the Authenticator goes on infinite loop once refresh_token is also expired.
This is my Network module, where I defined, Authenticator and Interceptor in OkHttp Client
#Module
public class NetworkModule
{
#Provides
#Singleton
OkHttpClient provideOkHttpClient(TokenAuthenticator tokenAuthenticator, TokenInceptor tokenInceptor, SharedManager sharedManager)
{
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
logging.setLevel(HttpLoggingInterceptor.Level.BODY);
OkHttpClient.Builder httpClient = new OkHttpClient.Builder();
// adding socket time for read/write/reconnect
httpClient.connectTimeout(30, TimeUnit.SECONDS);
httpClient.writeTimeout(30, TimeUnit.SECONDS);
httpClient.readTimeout(30, TimeUnit.SECONDS);
// setting the accept type of the request to application/json
httpClient.addNetworkInterceptor(new Interceptor()
{
#Override
public Response intercept(Chain chain) throws IOException {
Request.Builder requestBuilder = chain.request().newBuilder();
requestBuilder.header("Accept", "application/json");
return chain.proceed(requestBuilder.build());
}
});
httpClient.addInterceptor(logging).addInterceptor(tokenInceptor);
httpClient.authenticator(tokenAuthenticator);
return httpClient.build();
}
}
#Provides
Retrofit provideRetrofit(OkHttpClient okHttpClient){
return new Retrofit.Builder()
.baseUrl(ApiConstants.API_BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.client(okHttpClient)
.build();
}
#Provides
#Singleton
ApiService provideApiService(Retrofit retrofit, TokenService apiServiceHolder)
{
ApiService apiService = retrofit.create(ApiService.class);
apiServiceHolder.setApiService(apiService);
return apiService;
}
#Provides
#Singleton
public SharedPreferences providePreferences(Application application)
{
return application.getSharedPreferences(Constants.APP_PREFERENCES, Context.MODE_PRIVATE);
}
#Provides
#Singleton
public SharedManager provideSharedManager(SharedPreferences sharedPreferences)
{
return new SharedManager(sharedPreferences);
}
#Provides
#Singleton
public TokenAuthenticator tokenAuthenticator(TokenService tokenService, SharedManager sharedManager)
{
return new TokenAuthenticator(tokenService, sharedManager);
}
#Provides
#Singleton
public TokenInceptor tokenInceptor(SharedManager sharedManager)
{
return new TokenInceptor(sharedManager);
}
#Provides
#Singleton
public TokenService apiServiceHolder()
{
return new TokenService();
}
}
Here's the Interceptor
#Singleton
public class TokenInceptor implements Interceptor
{
SharedManager sharedManager;
#Inject
public TokenInceptor(SharedManager sharedManager)
{
this.sharedManager = sharedManager;
}
#Override
public Response intercept(Chain chain) throws IOException
{
Request request = chain.request();
// we don't need header in login/register so, we remove the header from these api request endpoints
if(request.url().encodedPath().contains("/token/client") && request.method().equalsIgnoreCase("POST"))
{
return chain.proceed(request);
}
// then we add the authenticator to other api requests
HttpUrl url = request.url();
Request.Builder urlBuilder = request.newBuilder().addHeader(ApiConstants.AUTHORIZATION, sharedManager.getBearer()).url(url);
Request apiRequest = urlBuilder.build();
return chain.proceed(apiRequest);
}
}
Here's the Authenticator
#Singleton
public class TokenAuthenticator implements Authenticator
{
private SharedManager sharedManager;
private TokenService tokenService;
#Inject
public TokenAuthenticator(#NonNull TokenService apiServiceHolder, SharedManager sharedManager)
{
this.tokenService = apiServiceHolder;
this.sharedManager = sharedManager;
}
#Nullable
#Override
public Request authenticate(Route route, Response response) throws IOException
{
if(!response.request().header(ApiConstants.AUTHORIZATION).equals(sharedManager.getBearer()))
{
return null;
}
retrofit2.Response<TokenResponse> tokenResponse = tokenService.getApiService().refreshToken(sharedManager.getRefresh()).execute();
TokenResponse responseData = tokenResponse.body();
if(tokenResponse.isSuccessful() && responseData!= null)
{
TokenResponse responseRequest = (TokenResponse) tokenResponse.body();
String new_token = responseRequest.getAccess();
sharedManager.saveAccessToken(new_token);
return response.request().newBuilder().header(ApiConstants.AUTHORIZATION,sharedManager.getBearer()).build();
}
else
{
// As per my assumption, the refresh token might expire here
Log.e("refresh_token","expired");
}
return null;
}
}
Here's the TokenService class
public class TokenService
{
ApiService apiService = null;
#Nullable
public ApiService getApiService() {
return apiService;
}
public void setApiService(ApiService apiService) {
this.apiService = apiService;
}
}
Here's SharedManager class
public class SharedManager
{
private SharedPreferences sharedPreferences;
#Inject
public SharedManager(SharedPreferences sharedPreferences)
{this.sharedPreferences = sharedPreferences;};
public void saveAccessToken(String token)
{
sharedPreferences.edit().putString(ApiConstants.ACCESS_TOKEN, token).commit();
}
public void saveRefreshToken(String token)
{
sharedPreferences.edit().putString(ApiConstants.REFRESH, token).commit();
}
public String getAccessToken()
{
return sharedPreferences.getString(ApiConstants.ACCESS_TOKEN, "");
}
public String getRefresh()
{
return sharedPreferences.getString(ApiConstants.REFRESH, "");
}
public String getBearer()
{
return "Bearer "+getAccessToken();
}
public void clearAll()
{
sharedPreferences.edit().clear().commit();
}
}
Here's ApiService interface
public interface ApiService
{
// client login
#POST("token/client")
#FormUrlEncoded
Call<ResponseBody> loginUser(#Field("email") String email,
#Field("password") String password);
// method for refresh token
#POST("token/refresh")
#FormUrlEncoded
Call<TokenResponse> refreshToken(#Field("refresh") String refresh);
// get agent
#GET("agent")
Call<ResponseBody> getAgentTour();
}
Can anyone trace out the faults in the code here ? The code structure changed while posting in stack.
A standard refresh token grant message will return an error code of invalid_grant when the refresh token finally expires.
{
"error": "invalid_grant",
"error_description": "An optional description message that varies between vendors"
}
At this point you should do two things:
For any in flight API calls, throw an exception with an error code such as 'login_required', that your error handling code can silently ignore
Then perform a login redirect to start a new user session
SAMPLE CODE OF MINE
A something to compare against, I have an AppAuth code sample that you can run and which allows simulation of token expiry events:
Android Code to Handle Invalid Grant
Android Code Sample Blog Post
Of course you would need to translate this behaviour to your own Dagger based coding preferences ...
Related
I'm experiencing this difficulty and I still don't understand what's happening, I have a scenario where an api uses a feing client to obtain a user's profile from an external service, until this moment everything was going well, but when I solved it add the fallback, spring starts to inject the SecurityContextHolder.getContext() always with a null value, and with that the interceptor cannot get the token of the user who made the primary call of the application.
appliction.properties
application.authserver.url=http://localhost:8088/auth
feign.circuitbreaker.enabled=true
Interceptor Class
#Component
#Slf4j
public class FeingClientConfig {
#Value("${app.name}")
private String appName;
#Bean
public RequestInterceptor requestInterceptor() {
return new RequestInterceptor() {
#Override
public void apply(RequestTemplate template) {
if (SecurityContextHolder.getContext().getAuthentication() != null) {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof Jwt) {
var jwt = (Jwt) principal;
log.info("-------------------- service integration -----------------");
log.info(String.format(" Endpoint: %s", template.method() + " " + template.feignTarget().url() + template.url()));
log.info(String.format(" User: %s", jwt.getClaim("user_name").toString()));
log.info(String.format(" Bearer token: %s", jwt.getTokenValue().substring(0, 30) + "....."));
log.info("----------------------------------------------------------");
template.header("Authorization", String.format("Bearer %s", jwt.getTokenValue()));
template.header("x-app-name", appName);
}
}
}
};
}
}
Client
#FeignClient(url = "${application.authserver.url}", name = "authClient",
fallbackFactory = AuthClientFallBackFactory.class)
public interface AuthClient {
#GetMapping("/user/profile")
public UserProfile getProfile();
}
Fallback
#Slf4j
#Component
public class AuthClientFallBackFactory implements FallbackFactory<AuthClient> {
#Override
public AuthClient create(Throwable cause) {
return new AuthClient() {
#Override
public UserProfile getProfile() {
log.error("*** Fallback: error on get profile.", cause);
return new UserProfile();
}
};
}
}
If the feign.circuitbreaker.enabled property was set to false, this ignores the use of the fallback, but the SecurityContextHolder.getContext() object is inject correctly
What am I doing wrong?
Using in this project:
JDK 11
Spring Boot [2.4.2]
I am creating android application which uses retrofit. I have used spring as rest api. I have used authentication with JWT. I have use two interceptor here RequestInterceptor and ResponseInterceptor. The scenario of calling BASE_URL/hello api with expire JWT is as below
client call /hello with expired accesstoken in header using RequestInterceptor
server check token and response with code 401/403
client check response code and call /refresh using ResponseInterceptor with refreshtoken in header
Server check refreshtoken and response with new accesstoken
now the problem is how to call again /hello. I want this for each request. How can i predict which request has been made last.
Here is the code:
part of the code where /hello is called
btnNext.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View view) {
Call<HelloResponse> call= RetrofitFactoryWithJwt.getRetrofitInstance(getApplicationContext()).helloUser();
call.enqueue(new Callback<HelloResponse>() {
#Override
public void onResponse(Call<HelloResponse> call, Response<HelloResponse> response) {
Log.d(TAG,"after call in enque");
if(response.code()==200)
{
Log.d(TAG,response.body().getSuccess());
}
else
{
Log.d(TAG,"problem in response:"+response.code());
}
}
#Override
public void onFailure(Call<HelloResponse> call, Throwable t) {
Log.d(TAG,"onfailure"+t.getMessage());
}
});
Intent intent = new Intent( getApplicationContext(), MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
}
});
RequestInterceptor.java
public class RequestInterceptor implements Interceptor {
Context context;
String TAG="heyrequest";
public RequestInterceptor(Context context)
{
this.context=context;
}
#Override
public Response intercept(Chain chain) throws IOException {
Request originalRequest = chain.request();
//if url is /refresh add refresh token in header instead of accesstoken
if(originalRequest.url().encodedPath().equalsIgnoreCase("/refresh"))
{
SharedPreferences preferences = context.getSharedPreferences("tokens", MODE_PRIVATE);
String refreshvalue=preferences.getString("refreshtoken","");
// rewrite the request
Request newRequest=originalRequest.newBuilder()
.addHeader("Authorization","Bearer "+refreshvalue)
.build();
return chain.proceed(newRequest);
}
//for context we have use requestinterceptor context construction
SharedPreferences preferences = context.getSharedPreferences("tokens", MODE_PRIVATE);
String tokenvalue=preferences.getString("accesstoken","");
// rewrite the request
Request newRequest=originalRequest.newBuilder()
.addHeader("Authorization","Bearer "+tokenvalue)
.build();
return chain.proceed(newRequest);
}
}
ResponseInterceptor.java
public class ResponseInterceptor implements Interceptor {
Context context;
String TAG="heyresponse";
String accesstoken=null;
Response response=null;
public ResponseInterceptor(Context context)
{
this.context=context;
}
#Override
public Response intercept(final Chain chain) throws IOException {
final Request request=chain.request();
response = chain.proceed(request);
if(response.code()==401 || response.code()==403)
{
accesstoken=getNewToken();
}
return chain.proceed(request);
}
public String getNewToken()
{
Call<RefreshResponse> call= RetrofitFactoryWithJwt.getRetrofitInstance(context).refreshToken();
call.enqueue(new Callback<RefreshResponse>() {
#Override
public void onResponse(Call<RefreshResponse> call, retrofit2.Response<RefreshResponse> response1) {
Log.d(TAG,"in refreshtoken call");
if(response1.code()==200)
{
accesstoken=response1.body().getAccesstoken();
Log.d(TAG,accesstoken);
SharedPreferences preferences = context.getSharedPreferences("tokens", MODE_PRIVATE);
preferences.edit().putString("accesstoken", accesstoken).apply();
}
else
{
Log.d(TAG,"problem in response:"+response1.code());
}
}
#Override
public void onFailure(Call<RefreshResponse> call, Throwable t) {
Log.d(TAG,"onfailure:"+t.getMessage());
}
});
return accesstoken;
}
}
I solved this problem by using authenticator for handle the response and interceptor for adding header in request
I am signing JWT with private key (authorization server) and I am using public key (resource server) to "verify" it...
How can I know whether the JWT has not been compromised? Or how can I do that?
The code is from resource server
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
Resource resource = new ClassPathResource("public.txt");
String publicKey = null;
try {
publicKey = IOUtils.toString(resource.getInputStream());
} catch (final IOException e) {
throw new RuntimeException(e);
}
converter.setVerifierKey(publicKey);
return converter;
}
Spring Security will do the verification of the token based on configurations in authorization server.
For a standalone verification, the code would be like:
RsaVerifier verifier = new RsaVerifier(RSAPublicKey);
Jwt tokenDecoded = JwtHelper.decodeAndVerify(token, verifier);
Map<String, Object> claimsMap = (Map<String, Object>) new
ObjectMapper().readValue(tokenDecoded.getClaims(), Map.class);
//Verify the claims then
// 1 Verify if the token has not already expired
// 2 Verify the issuance date ( should be before this date )
// 3 Verify if the issuer of this token is contained in verified authorities.
// 4 Verify if the token was issued for this client
// 5 Verify if the token contained any expected claims...
But the above is implemented by Spring Security for Oauth2 authentication process, client application just needs to provide configurations.
The trigger is OAuth2AuthenticationProcessingFilter in the Spring security filter chain. This filter is added when resources are protected by Oauth2 security.
In your application, the authorization server configuration would look like ( only relevant indicative configuration extracts below)
#Configuration
#EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
...
#Bean
public JwtAccessTokenConverter accessTokenConverter() {
RSAPemKeyPairLoader keyPairLoader = new RSAPemKeyPairLoader();
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(...);
converter.setVerifierKey(...);
return converter;
}
#Bean
#Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(...);
defaultTokenServices.setAccessTokenValiditySeconds(...);
defaultTokenServices.setRefreshTokenValiditySeconds(...);
return defaultTokenServices;
}
}
In your application, the Resource Server configuration would be like:
#EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
...
}
}
To trace in Spring implementation where the requested token is intercepted and verified look at the Spring OAUTH2 implementation - flow details below, where Authentication object, an instance of OAuth2Authentication would be attempted to be created for successful requests.
All below Code extracts are from spring-security-oauth2-2.0.8.RELEASE implementations.
public class OAuth2AuthenticationManager implements AuthenticationManager {
....
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (authentication == null) {
throw new InvalidTokenException("Invalid token (token not found)");
}
String token = (String) authentication.getPrincipal();
OAuth2Authentication auth = tokenServices.loadAuthentication(token);
...
}
}
The loadAuthentication would be basically verifying the access token and attempting to convert it into OAuth2Authentication
public class DefaultTokenServices implements AuthorizationServerTokenServices ...{
public OAuth2Authentication loadAuthentication(String accessTokenValue) throws AuthenticationException, InvalidTokenException {
OAuth2AccessToken accessToken = tokenStore.readAccessToken(accessTokenValue);
...
}
}
JwtTokenStore would create OAuth2AccessToken and in the process decode and verify the String token.
public class JwtTokenStore implements TokenStore {
public OAuth2AccessToken readAccessToken(String tokenValue) {
OAuth2AccessToken accessToken = convertAccessToken(tokenValue);
if (jwtTokenEnhancer.isRefreshToken(accessToken)) {
throw new InvalidTokenException("Encoded token is a refresh token");
}
return accessToken;
}
private OAuth2AccessToken convertAccessToken(String tokenValue) {
return jwtTokenEnhancer.extractAccessToken(tokenValue, jwtTokenEnhancer.decode(tokenValue));
}
}
JWTAccessTokenConverter does the decoding and extraction of token claims.
public class JwtAccessTokenConverter implements AccessTokenConverter {
protected Map<String, Object> decode(String token) {
try {
Jwt jwt = JwtHelper.decodeAndVerify(token, verifier);
String content = jwt.getClaims();
Map<String, Object> map = objectMapper.parseMap(content);
if (map.containsKey(EXP) && map.get(EXP) instanceof Integer) {
Integer intValue = (Integer) map.get(EXP);
map.put(EXP, new Long(intValue));
}
return map;
}
catch (Exception e) {
throw new InvalidTokenException("Cannot convert access token to JSON", e);
}
}
JwtHelper would do the decoding and request verification.
public static Jwt decodeAndVerify(String token, SignatureVerifier verifier) {
Jwt jwt = decode(token);
jwt.verifySignature(verifier);
return jwt;
}
JwttImpl invokes the verifier.
public void verifySignature(SignatureVerifier verifier) {
verifier.verify(signingInput(), crypto);
}
For example, RSA Signature verifier would finally do the verification:
public class RsaVerifier implements SignatureVerifier {
public void verify(byte[] content, byte[] sig) {
try {
Signature signature = Signature.getInstance(algorithm);
signature.initVerify(key);
signature.update(content);
if (!signature.verify(sig)) {
throw new InvalidSignatureException("RSA Signature did not match content");
}
}
catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
}
}
I'm trying to test a Jersey filter with the Jersey Test Framework and I need it to be done over HTTPS.
I know how to configure the ssl context on the Client but I can't seem to find info on how to run the Grizzly server over HTTPS.
The test:
#Test
public void testPeerTokenOK() {
SSLContext sslContext = getSslContext();
Client client = ClientBuilder.newBuilder().hostnameVerifier((s, session) -> true).sslContext(sslContext).build();
WebTarget target = target().path(URI);
Response response = client.target(target.getUri())
.request(MediaType.APPLICATION_JSON)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON + "; charset=" + StandardCharsets.UTF_8.name())
assertEquals(Status.OK.getStatusCode(), response.getStatus());
}
The resource:
#Path(URI)
public static class TestResource {
#GET
#Singleton
#Consumes(MediaType.APPLICATION_JSON)
#Produces(MediaType.APPLICATION_JSON)
public Response get(EntityPayLoad payload) throws Exception {
if (payload != null && payload instanceof EntityPayLoad) {
return Response.ok(payload).build();
} else {
return Response.status(Status.BAD_REQUEST.getStatusCode()).build();
}
}
}
The constructor:
#Override
protected Application configure() {
ResourceConfig rc = new ResourceConfig();
rc.register(SpringLifecycleListener.class);
rc.register(RequestContextFilter.class);
rc.register(new JacksonFeature());
rc.register(new ObjectMapperContextResolver());
rc.registerClasses(TestResource.class);
rc.register(AccessTokenFilter.class);
rc.register(PeerTokenFilter.class);
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("filterContext.xml");
rc.property("contextConfig", applicationContext);
return rc;
}
The relevant maven dependency:
<dependency>
<groupId>org.glassfish.jersey.test-framework.providers</groupId>
<artifactId>jersey-test-framework-provider-grizzly2</artifactId>
<version>2.25</version>
<scope>test</scope>
</dependency>
This question was also asked and answered here: Configure JettyTestContainer with SSL for JerseyTest From version 2.33 onwards it is possible to configure the JerseyTest with your custom ssl configuration. See here for an example:
public class SecuredJerseyTest extends JerseyTest {
#Override
protected TestContainerFactory getTestContainerFactory() {
return new GrizzlyTestContainerFactory();
}
#Path("hello")
public static class TestResource {
#GET
public String hello() {
return "hello";
}
}
#Override
protected Application configure() {
return new ResourceConfig(TestResource.class);
}
#Override
protected URI getBaseUri() {
return UriBuilder
.fromUri("https://localhost")
.port(getPort())
.build();
}
#Override
protected Optional<SSLContext> getSslContext() {
SSLContext sslContext = ... // your initialised server sslContext
return Optional.of(sslContext);
}
#Override
protected Optional<SSLParameters> getSslParameters() {
serverSslParameters = new SSLParameters();
serverSslParameters.setNeedClientAuth(false);
return Optional.of(serverSslParameters);
}
#Test
public void testHello() {
SSLContext sslContext = ... // your initialised client sslContext
Client client = ClientBuilder.newBuilder()
.sslContext(sslContext)
.build();
WebTarget target = client.target(getBaseUri()).path("hello");
String s = target.request().get(String.class);
Assert.assertEquals("hello", s);
}
}
I have added custom token based authentication for my spring-web app and extending the same for spring websocket as shown below
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic", "/queue");
config.setApplicationDestinationPrefixes("/app");
config.setUserDestinationPrefix("/user");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/gs-guide-websocket").setAllowedOrigins("*").withSockJS();
}
#Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.setInterceptors(new ChannelInterceptorAdapter() {
#Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor =
MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
String jwtToken = accessor.getFirstNativeHeader("Auth-Token");
if (!StringUtils.isEmpty(jwtToken)) {
Authentication auth = tokenService.retrieveUserAuthToken(jwtToken);
SecurityContextHolder.getContext().setAuthentication(auth);
accessor.setUser(auth);
//for Auth-Token '12345token' the user name is 'user1' as auth.getName() returns 'user1'
}
}
return message;
}
});
}
}
The client side code to connect to the socket is
var socket = new SockJS('http://localhost:8080/gs-guide-websocket');
stompClient = Stomp.over(socket);
stompClient.connect({'Auth-Token': '12345token'}, function (frame) {
stompClient.subscribe('/user/queue/greetings', function (greeting) {
alert(greeting.body);
});
});
And from my controller I am sending message as
messagingTemplate.convertAndSendToUser("user1", "/queue/greetings", "Hi User1");
For the auth token 12345token the user name is user1. But when I send a message to user1, its not received at the client end. Is there anything I am missing with this?
In your Websocket controller you should do something like this :
#Controller
public class GreetingController {
#Autowired
private SimpMessagingTemplate messagingTemplate;
#MessageMapping("/hello")
public void greeting(Principal principal, HelloMessage message) throws Exception {
Greeting greeting = new Greeting();
greeting.setContent("Hello!");
messagingTemplate.convertAndSendToUser(message.getToUser(), "/queue/reply", greeting);
}
}
On the client side, your user should subscribe to topic /user/queue/reply.
You must also add some destination prefixes :
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic", "/queue" ,"/user");
config.setApplicationDestinationPrefixes("/app");
config.setUserDestinationPrefix("/user");
}
/*...*/
}
When your server receive a message on the /app/hello queue, it should send a message to the user in your dto. User must be equal to the user's principal.
I think the only problem in your code is that your "/user" is not in your destination prefixes. Your greetings messages are blocked because you sent them in a queue that begin with /user and this prefixe is not registered.
You can check the sources at git repo :
https://github.com/simvetanylen/test-spring-websocket
Hope it works!
In my previous project I sent messages to one specific user; in detail I wrote the following:
CLIENT SIDE:
function stompConnect(notificationTmpl)
{
var socket = new SockJS('/comm-svr');
stompClient = Stomp.over(socket);
var theUserId
stompClient.connect({userId:theUserId}, function (frame) {
debug('Connected: ' + frame);
stompClient.subscribe('/topic/connect/'+theUserId, function (data) {
//Handle data
}
});
}
SERVER SIDE
Spring websocket listener:
#Component
public class WebSocketSessionListener
{
private static final Logger logger = LoggerFactory.getLogger(WebSocketSessionListener.class.getName());
private List<String> connectedClientId = new ArrayList<String>();
#EventListener
public void connectionEstablished(SessionConnectedEvent sce)
{
MessageHeaders msgHeaders = sce.getMessage().getHeaders();
Principal princ = (Principal) msgHeaders.get("simpUser");
StompHeaderAccessor sha = StompHeaderAccessor.wrap(sce.getMessage());
List<String> nativeHeaders = sha.getNativeHeader("userId");
if( nativeHeaders != null )
{
String userId = nativeHeaders.get(0);
connectedClientId.add(userId);
if( logger.isDebugEnabled() )
{
logger.debug("Connessione websocket stabilita. ID Utente "+userId);
}
}
else
{
String userId = princ.getName();
connectedClientId.add(userId);
if( logger.isDebugEnabled() )
{
logger.debug("Connessione websocket stabilita. ID Utente "+userId);
}
}
}
#EventListener
public void webSockectDisconnect(SessionDisconnectEvent sde)
{
MessageHeaders msgHeaders = sde.getMessage().getHeaders();
Principal princ = (Principal) msgHeaders.get("simpUser");
StompHeaderAccessor sha = StompHeaderAccessor.wrap(sde.getMessage());
List<String> nativeHeaders = sha.getNativeHeader("userId");
if( nativeHeaders != null )
{
String userId = nativeHeaders.get(0);
connectedClientId.remove(userId);
if( logger.isDebugEnabled() )
{
logger.debug("Connessione websocket stabilita. ID Utente "+userId);
}
}
else
{
String userId = princ.getName();
connectedClientId.remove(userId);
if( logger.isDebugEnabled() )
{
logger.debug("Connessione websocket stabilita. ID Utente "+userId);
}
}
}
public List<String> getConnectedClientId()
{
return connectedClientId;
}
public void setConnectedClientId(List<String> connectedClientId)
{
this.connectedClientId = connectedClientId;
}
}
Spring websocket message sender:
#Autowired
private SimpMessagingTemplate msgTmp;
private void propagateDvcMsg( WebDeviceStatusInfo device )
{
String msg = "";
String userId =((Principal)SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getName()
msgTmp.convertAndSend("/topic/connect"+userId, msg);
}
I hope it's useful