Spring Boot 2 WebSockets connection lost unexpectedly - spring-boot

I have a Spring boot 1.5 + Angular5 application utilizing Websockets via SockJS, and was recently forced to upgrade to Spring boot 2.2.
Following the upgrade, my websocket is being closed after either a random period of time, or when a write to the websocket happens. When using Spring Boot 1.5, everything works perfectly.
Below is the configuration in Spring, using spring-boot-starter-websocket version: '2.2.4.RELEASE'
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfiguration extends AbstractWebSocketMessageBrokerConfigurer {
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/api/socket")
.setAllowedOrigins("*")
.withSockJS();
}
#Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/app")
.enableSimpleBroker("/nightly");
}
}
I've also added the following security rules:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/api/socket/**")
.cors().and()
.headers().frameOptions().disable().and()
.csrf().disable()
.authorizeRequests()
.anyRequest().permitAll();
}
Client code:
initWebSocket() {
const ws = new SockJS('/api/socket');
this.stompClient = Stomp.over(ws);
const that = this;
this.stompClient.connect({}, () => {
that.stompClient.subscribe('/nightly', (message) => {
this._rootStore.dispatch(new UpdateNightlyAction(message));
});
});
}
When the connection is lost, the client logs the following:
POST https://<url>/api/socket/231/i0rsgjlx/xhr?t=1600673163228 404
Whoops! Lost connection to https://<url>/api/socket
I went through different scenarios of Websockets not working in Spring Boot 2 and nothing seemed to help. In 1.5 it works just fine. What am I missing here?

beacuse of springboot2.0^ is not allow cors param allowedOrigins = "*" , you can overwrite AllowedOriginPatterns equals "*"
boot1.5 ->
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
StompWebSocketEndpointRegistration registration = registry.addEndpoint("/webSocket");
registration.setAllowedOrigins("*");
registration.withSockJS();
}
boot2.0^->
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
StompWebSocketEndpointRegistration registration = registry.addEndpoint("/webSocket");
// boot2.0^ AllowedOrigins = * is not allown
registration.setAllowedOriginPatterns("*");
registration.withSockJS();
}
I had a same error with yours, even search for a long time ,but there is no
answers,so i check the debug log find this problem,i fixed it with this method,it works!

Related

Spring - Sock.js - websockets: blocked by CORS policy

I am implementing websockets to Spring App with sock.js + stomp.js on the client app.
When trying to connect I am getting the error:
Access to XMLHttpRequest at 'http://localhost:8080/ws/tracker/info?t=...' from origin 'null' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
And in my WebsocketConfiguration :
#Configuration
#EnableWebSocketMessageBroker
#Order(Ordered.HIGHEST_PRECEDENCE + 99)
public class WebsocketConfiguration implements WebSocketMessageBrokerConfigurer {
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOrigins("*")
.withSockJS();
}
#Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic/", "/queue/");
registry.setApplicationDestinationPrefixes("/app");
}
}
Client libraries:
<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.4.0/sockjs.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.js"></script>
Client connection:
var socket = new SockJS('http://localhost:8080/websocket/tracker');
I have already implemented websockets in one of the earlier projects, so this is all copied from it, though I can't remember (nor find) how to solve this error.
Check if you have ClientForwardController, mapping should be like this:
#GetMapping(value = {"/{path:[^\\.]*}", "/{path:^(?!websocket).*}/**/{path:[^\\.]*}"})
public String forward() {
return "forward:/";
}

Spring Boot I can't switch keycloak and basic authentication

NB: I'm using Spring Boot 2.1.10 and Keycloak 6.0.1
I wish I could choose between basic authentication and SSO at launch time for a web application (MVC).
So I first integrated Spring Security + Keycloak with keycloak-spring-boot-starter
#SpringBootApplication
#EnableWebSecurity
public class KcApplication {
public static void main(String[] args) {
SpringApplication.run(KcApplication.class, args);
}
}
Then I defined a "sso" Spring profile and a default config:
application.properties goes like this:
spring.application.name=#artifactId#
server.port: 8081
keycloak.enabled=false
spring.main.allow-bean-definition-overriding: true
and application-sso.yml goes like this:
keycloak:
enabled: true
auth-server-url: http://localhost:8180/auth
realm: SpringBootRealm
resource: spring-app
credentials:
secret: 0c8940a4-2065-4810-a366-02877802e762
principal-attribute: preferred_username
Then I got two different security configurers:
#Configuration #Profile("!sso")
public class BasicAuthConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.authorizeRequests()
.antMatchers("/customers*").authenticated()
.anyRequest().permitAll()
.and().httpBasic() //DEBUG can't force
.and().logout().logoutSuccessUrl("/");
}
}
#Configuration #Profile("sso")
#ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
public class KeycloakAuthConfig extends KeycloakWebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.authorizeRequests()
.antMatchers("/customers*").authenticated()
.anyRequest().permitAll();
}
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
KeycloakAuthenticationProvider authProvider = keycloakAuthenticationProvider();
authProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
auth.authenticationProvider(authProvider);
}
#Bean
public KeycloakSpringBootConfigResolver keycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
#Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
}
Everithing launches smoothly in both cases, and the "sso" profile behaves rightly: entering the /consumers path first turns to a redirection to Keycloak and coming back when authenticated.
But I can't get the default profile to log me in. When entering /consumers I get an anonymousUser, not being asked to form login.
I guess that the issue comes from something I missed, so I put above as many things as possible. Does anyone knows why I can't login, despite the fact that the right configurer was run at debug?
Thank you
Well, it took the weekend for the session to be reset, then it worked!
Proof that it's probably the logout that bugs instead... I'm not even sad :-(

Spring Security: Multiple OpenID Connect Clients for Different Paths?

Using Spring Boot 2.1.5 and Spring Security 5, I'm trying to use two different OpenID clients (based in Keycloak). Here is what we have in application.properties.
spring.security.oauth2.client.registration.keycloak-endusersclient.client-id=endusersclient
spring.security.oauth2.client.registration.keycloak-endusersclient.client-secret=7b41aaa4-277f-47cf-9eab-91afacd55d2c
spring.security.oauth2.client.provider.keycloak-endusersclient.issuer-uri=https://mydomain/auth/realms/endusersrealm
spring.security.oauth2.client.registration.keycloak-employeesclient.client-id=employeesclient
spring.security.oauth2.client.registration.keycloak-employeesclient.client-secret=7b41aaa4-277f-47cf-9eab-91afacd55d2d
spring.security.oauth2.client.provider.keycloak-employeesclient.issuer-uri=https://mydomain/auth/realms/employeesrealm
You can see from the snippet above, we are trying to use one OpenID client for endusers (customers) and another for employees.
In the security configuration class, we see how to configure security on different patterns as follows:
public class OpenIDConnectSecurityConfig extends
WebSecurityConfigurerAdapter
{
#Override
protected void configure(HttpSecurity http) throws Exception {
// avoid multiple concurrent sessions
http.sessionManagement().maximumSessions(1);
http.authorizeRequests()
.antMatchers("/endusers/**").authenticated()
.antMatchers("/employees/**").authenticated()
.anyRequest().permitAll().and()
.oauth2Login()
.successHandler(new OpenIDConnectAuthenticationSuccessHandler())
.and()
.logout().logoutSuccessUrl("/");
What I don't understand is how to configure each OpenID client to fire on a separate URL pattern. In the example above, we would like to see the endusers client be used when hitting URL's starting with "/endusers", and to use the employees client when hitting URL's starting with "/employees".
Can this be done?
You need to use AuthenticationManagerResolver for the multi-tenant case, in which endusersclient and employeesclient are your tenants.
public class CustomAuthenticationManagerResolver implements AuthenticationManagerResolver<HttpServletRequest> {
#Override
public AuthenticationManager resolve(HttpServletRequest request) {
return fromTenant();
}
private AuthenticationManager fromTenant(HttpServletRequest request) {
String[] pathParts = request.getRequestURI().split("/");
//TODO find your tanent from the path and return the auth manager
}
// And in your class, it should be like below
private CustomAuthenticationManagerResolver customAuthenticationManagerResolver;
http.authorizeRequests()
.antMatchers("/endusers/**").authenticated()
.antMatchers("/employees/**").authenticated()
.anyRequest().permitAll().and().oauth2ResourceServer().authenticationManagerResolver(this.customAuthenticationManagerResolver);
For Opaque Token (Multitenant Configuration)
#Component
public class CustomAuthenticationManagerResolver implements AuthenticationManagerResolver {
#Override
public AuthenticationManager resolve(HttpServletRequest request) {
String tenantId = request.getHeader("tenant");
OpaqueTokenIntrospector opaqueTokenIntrospector;
if (tenantId.equals("1")) {
opaqueTokenIntrospector = new NimbusOpaqueTokenIntrospector(
"https://test/authorize/oauth2/introspect",
"test",
"test"
);
} else {
opaqueTokenIntrospector = new NimbusOpaqueTokenIntrospector(
"https://test/authorize/oauth2/introspect",
"test",
"test");
}
return new OpaqueTokenAuthenticationProvider(opaqueTokenIntrospector)::authenticate;
}
}
Web Security Configuration
#Autowired
private CustomAuthenticationManagerResolver customAuthenticationManagerResolver;
#Override
public void configure(HttpSecurity http) throws Exception {
http.anyRequest()
.authenticated().and().oauth2ResourceServer()
.authenticationEntryPoint(restEntryPoint).authenticationManagerResolver(customAuthenticationManagerResolver);
}

CORS error when accessing static resource

Despite having the following config, accessing http://localhost:8080/rooms/rooms.json gives me a CORS error - No 'Access-Control-Allow-Origin' header is present on the requested resource.
I have no problem to request any other path which is mapped by controller. What is the problem with static resources? How to allow cors request or exclude the resource paths without spring security?
Spring Boot 2.0.5
Spring Boot Web Starter 2.0.5
#Configuration
#EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {
#Override
public void addResourceHandlers(final ResourceHandlerRegistry registry) {
registry.addResourceHandler("/rooms/**")
.addResourceLocations("classpath:/rooms/")
.setCachePeriod(3600)
.resourceChain(true)
.addResolver(new PathResourceResolver());
}
#Override
public void addCorsMappings(final CorsRegistry registry) {
registry.addMapping("/**");
}
}
I got it working with the following configuration bean:
#Configuration
public class StaticResourcesCorsConfig
{
#Bean
public WebMvcConfigurer corsConfigurer()
{
return new WebMvcConfigurer() {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedOrigins("*");
}
};
}
}
Note, that Spring Boot will only send the Access-Control-Allow-Origin header back on a GET request, if the Origin-header is present on the request.
Update addCorsMappings like below it could work
#Configuration
#EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {
#Override
public void addResourceHandlers(final ResourceHandlerRegistry registry) {
registry.addResourceHandler("/rooms/**")
.addResourceLocations("classpath:/rooms/")
.setCachePeriod(3600)
.resourceChain(true)
.addResolver(new PathResourceResolver());
}
#Override
public void addCorsMappings(final CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:8080")
.allowedMethods("POST", "GET")
//.allowedHeaders("header1", "header2", "header3")
//.exposedHeaders("header1", "header2")
.allowCredentials(true).maxAge(3600);
}
}
Add #CrossOrigin(value = "*") to your controller class. You can replace * with any particular URL in case to allow that origin only.
#CrossOrigin(origins = "http://localhost")
#GetMapping("/rooms/")
public Object rooms() {
// your implementation
}
You can do in this way.
https://www.viator.com/orion/nova/public/mmblite/styles-53929dcb.css
Experienced the same problem actually, but found the root cause and a solution.
Your request was most probably cached by intermediary: load balancer, CDN or caching HTTP server in front of your application as regular non-CORS request. Then you have sent request with Origin:, but the intermediary returned you the same cached response because from point of view of the cache responses by default are identified by /path + METHOD + Host:-header which were the same. To tell caches that the request with Origin: and the regular request (without Origin: need to be cached as independent entries in any cache we need Vary: Origin header in both responses. This was fixed/implemented in Spring 5.2.x (in my case it was Spring 5.1.9), in your case it was 5.0.9 (as dependency of Spring Boot 2.0.5.). Once I upgraded to Spring 5.2.0 all was fixed once caches on the intermediary had expired. I recommend to upgrade beyond 5.2.6 (cause there were further changes in CORS handling, which are nice to have).
here is the line (which made the difference) they (Pivotal) commited into Spring: https://github.com/spring-projects/spring-framework/commit/d27b5d0ab6e8b91a77e272ad57ae83c7d81d810b#r36264428
and their bug description: https://github.com/spring-projects/spring-framework/issues/22273

Send User-specific message with spring

I have an endpoint (/create) which has some logic and it takes 3-4 min to process so I used rabbitmq and as soon as the endpoint receive the request it takes the body and post the message in rabbitmq, the listener listens to the message and process the request now I want to notify the user that his request is successfully processed.
Is websocket correct choice for this requirement
Is there other better way through which i can achieve my goal?
So I went forward with websocket since I am using oauth based authentication I am unable to get web-socket work
Here is my code I have written:
SocketConfig.java
#Configuration
#EnableWebSocketMessageBroker
public class SocketConfig implements WebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic","/secured/queue");
//config.setApplicationDestinationPrefixes("/app");
//config.setUserDestinationPrefix("/secured/user");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/secured/messagereg").setAllowedOrigins("*").withSockJS();
}
SocketHandler.java
#Configuration
public class SocketHandler extends AbstractSecurityWebSocketMessageBrokerConfigurer {
#Override
protected boolean sameOriginDisabled() {
return true;
}
#Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages
.simpDestMatchers("/secured/**", "/secured/**/**").authenticated()
.anyMessage().authenticated();
}
}
WebSecurityConfig.java
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
#Profile("!test")
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private Auth0PropertyConfig config;
#Override
protected void configure(HttpSecurity http) throws Exception {
JwtWebSecurityConfigurer
.forRS256(config.getAudience(), config.getIssuer())
.configure(http)
.cors()
.and()
.csrf().disable()
.authorizeRequests()
.anyRequest().authenticated()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
;
}
}
clientCode
const socket = new SockJs("/secured/messagereg/?access_token="+token);
this.setState({ clientRef: Stomp.over(socket) }, () =>
this.state.clientRef.connect({},
frame => {
this.setState({ connection: true });
this.state.clientRef.subscribe("/user/secured/queue/reply", message => {
console.log("asd received ----------" + message.body);
this.setState(prevs => ({
message: [...prevs.message, message]
}));
});
},
error => {
console.log("Stomp protocol error." + error);
}
)
);
I am getting 401 unauthorized while connecting to socket.
In my opinion: a push messaging pattern (for example using STOMP) is suitable for this scenario, but that ultimately depends on your architectural principles. You could also poll the server for result (using REST API) which has both advantages (shared security architecture) and disadvantages (client code, traffic, and reaction-time overheads).
Answer:
In order to get your code working, I think you need one more method in your SocketConfig.java, which will hook into your OAUTH filter (or whatever method you may have in place).
Important - websocket auth does not reuse existing Spring Security context. That's why you need to implement auth again, for example in the SocketConfig class using the WebSocketMessageBrokerConfigurer's method configureClientInboundChannel.
The following example assumes you have already obtained the OAUTH token previously, and it's only used to reauthenticate the websocket connection. Setting the user reference in StompHeaderAccessor (3rd last line) will enable your code to send a message to the correct user.
It also requires that the OAUTH token is set in the message header, as opposed to the endpoint parameter in your example. I think that may be safer for websocks messaging as the message itself is encrypted on protocol level if you use wss.
#Autowired
private YourOauthService auth;
#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 (StompCommand.CONNECT.equals(accessor.getCommand())) {
String token = accessor.removeNativeHeader("Authorization").get(0);
Authentication user = auth.getAuthentication(token);
accessor.setUser(user);
}
return message;
}
});
}
I found some more interesting examples in https://robertleggett.wordpress.com/2015/05/27/websockets-with-spring-spring-security/

Resources